-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implementation of Chapter 7: [Evaluating Expressions](https://craftinginterpreters.com/evaluating-expressions.html).
- Loading branch information
Showing
5 changed files
with
289 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,7 @@ | |
"gravlax", | ||
"Vanderkam", | ||
"endregion", | ||
"autofix" | ||
"autofix", | ||
"quickfix" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,8 @@ | ||
{ | ||
"$schema": "https://unpkg.com/knip@latest/schema.json", | ||
"entry": ["src/index.ts!"], | ||
// See https://github.com/webpro/knip/issues/450 | ||
"exclude": ["classMembers"], | ||
"ignoreExportsUsedInFile": true, | ||
"project": ["src/**/*.ts!"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import { describe, expect, it, vi } from "vitest"; | ||
|
||
import { Interpreter, stringify } from "./interpreter.js"; | ||
import { parse } from "./parser.js"; | ||
import { Scanner } from "./scanner.js"; | ||
|
||
function parseText(text: string) { | ||
return parse(new Scanner(text).scanTokens()); | ||
} | ||
|
||
function evaluate(text: string) { | ||
const expr = parseText(text); | ||
return expr && new Interpreter().evaluate(expr); | ||
} | ||
|
||
describe("interpreter", () => { | ||
it("should evaluate an arithmetic expression", () => { | ||
expect(evaluate("1 + 2 * 3")).toEqual(7); | ||
expect(evaluate("3 - 1 / 2")).toEqual(2.5); | ||
}); | ||
|
||
it("should evaluate comparison operators", () => { | ||
expect(evaluate("12 > 6")).toBe(true); | ||
expect(evaluate("12 > 12")).toBe(false); | ||
expect(evaluate("12 == 12")).toBe(true); | ||
expect(evaluate("12 != 12")).toBe(false); | ||
expect(evaluate("0 == nil")).toBe(false); | ||
expect(evaluate("nil == nil")).toBe(true); | ||
expect(evaluate("2 >= 2")).toBe(true); | ||
expect(evaluate("2 >= 3")).toBe(false); | ||
expect(evaluate("2 <= 3")).toBe(true); | ||
expect(evaluate("2 < 3")).toBe(true); | ||
}); | ||
|
||
it("should evaluate unary operators", () => { | ||
expect(evaluate("-12")).toBe(-12); | ||
expect(evaluate("-(1 + 2)")).toBe(-3); | ||
expect(evaluate("!nil")).toBe(true); | ||
expect(evaluate("!!nil")).toBe(false); | ||
}); | ||
|
||
it("should concatenate strings", () => { | ||
expect(evaluate(`"hello" + " " + "world"`)).toEqual("hello world"); | ||
}); | ||
|
||
it("should evaluate truthiness", () => { | ||
expect(evaluate("!true")).toEqual(false); | ||
expect(evaluate("!12")).toEqual(false); | ||
expect(evaluate("!!nil")).toEqual(false); | ||
expect(evaluate("!!0")).toEqual(true); | ||
expect(evaluate(`!!""`)).toEqual(true); | ||
}); | ||
|
||
it("should report an error on mixed + operands", () => { | ||
expect(() => evaluate(`"12" + 13`)).toThrowError( | ||
"Operands must be two numbers or two strings.", | ||
); | ||
}); | ||
|
||
it("should report an error on non-numeric operands", () => { | ||
expect(() => evaluate(`"12" / 13`)).toThrowError( | ||
"Operand must be a number.", | ||
); | ||
}); | ||
|
||
it("should interpret and stringify output", () => { | ||
const log = vi.spyOn(console, "log").mockImplementation(() => undefined); | ||
const expr = parseText("1 + 2"); | ||
expect(expr).not.toBeNull(); | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
new Interpreter().interpret(expr!); | ||
expect(log).toHaveBeenCalledWith("3"); | ||
}); | ||
|
||
it("should interpret and report an error", () => { | ||
const error = vi | ||
.spyOn(console, "error") | ||
.mockImplementation(() => undefined); | ||
const expr = parseText("1 - nil"); | ||
expect(expr).not.toBeNull(); | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
new Interpreter().interpret(expr!); | ||
expect(error).toHaveBeenCalledWith("Operand must be a number.\n[line 1]"); | ||
}); | ||
}); | ||
|
||
describe("stringify", () => { | ||
it("should stringify a null value", () => { | ||
expect(stringify(null)).toEqual("nil"); | ||
}); | ||
|
||
it("should stringify a numbers", () => { | ||
expect(stringify(123)).toEqual("123"); | ||
expect(stringify(-123)).toEqual("-123"); | ||
expect(stringify(1.25)).toEqual("1.25"); | ||
expect(stringify(-0.125)).toEqual("-0.125"); | ||
}); | ||
|
||
it("should stringify booleans", () => { | ||
expect(stringify(true)).toEqual("true"); | ||
expect(stringify(false)).toEqual("false"); | ||
}); | ||
|
||
it("should stringify strings", () => { | ||
expect(stringify("")).toEqual(``); | ||
expect(stringify("hello")).toEqual(`hello`); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import { | ||
Binary, | ||
Expr, | ||
ExpressionVisitor, | ||
Grouping, | ||
Literal, | ||
Unary, | ||
visitExpr, | ||
} from "./ast.js"; | ||
import { runtimeError } from "./main.js"; | ||
import { Token } from "./token.js"; | ||
|
||
// XXX using eslint quickfix to implement this interface did not work at all. | ||
|
||
export class Interpreter implements ExpressionVisitor<unknown> { | ||
binary(expr: Binary): unknown { | ||
const left = this.evaluate(expr.left); | ||
const right = this.evaluate(expr.right); | ||
const { operator } = expr; | ||
switch (operator.type) { | ||
case "-": | ||
checkNumberOperand(operator, left); | ||
checkNumberOperand(operator, right); | ||
return left - right; | ||
case "/": | ||
checkNumberOperand(operator, left); | ||
checkNumberOperand(operator, right); | ||
return left / right; | ||
case "*": | ||
checkNumberOperand(operator, left); | ||
checkNumberOperand(operator, right); | ||
return left * right; | ||
case "+": | ||
// This looks kinda funny! | ||
if (typeof left === "number" && typeof right === "number") { | ||
return left + right; | ||
} else if (typeof left === "string" && typeof right === "string") { | ||
return left + right; | ||
} | ||
throw new RuntimeError( | ||
operator, | ||
"Operands must be two numbers or two strings.", | ||
); | ||
|
||
case ">": | ||
checkNumberOperand(operator, left); | ||
checkNumberOperand(operator, right); | ||
return left > right; | ||
case ">=": | ||
checkNumberOperand(operator, left); | ||
checkNumberOperand(operator, right); | ||
return left >= right; | ||
case "<": | ||
checkNumberOperand(operator, left); | ||
checkNumberOperand(operator, right); | ||
return left < right; | ||
case "<=": | ||
checkNumberOperand(operator, left); | ||
checkNumberOperand(operator, right); | ||
return left <= right; | ||
case "==": | ||
return isEqual(left, right); | ||
case "!=": | ||
return !isEqual(left, right); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
evaluate(expr: Expr): unknown { | ||
return visitExpr(expr, this); | ||
} | ||
|
||
grouping(expr: Grouping): unknown { | ||
return this.evaluate(expr.expr); | ||
} | ||
|
||
interpret(expr: Expr): void { | ||
try { | ||
const value = this.evaluate(expr); | ||
console.log(stringify(value)); | ||
} catch (e) { | ||
if (e instanceof RuntimeError) { | ||
runtimeError(e); | ||
} | ||
} | ||
} | ||
|
||
literal(expr: Literal): unknown { | ||
return expr.value; | ||
} | ||
|
||
unary(expr: Unary): unknown { | ||
const right = this.evaluate(expr.right); | ||
switch (expr.operator.type) { | ||
case "-": | ||
checkNumberOperand(expr.operator, right); | ||
return -right; | ||
case "!": | ||
return !isTruthy(right); | ||
} | ||
} | ||
} | ||
|
||
export class RuntimeError extends Error { | ||
token: Token; | ||
constructor(token: Token, message: string) { | ||
super(message); | ||
this.token = token; | ||
} | ||
} | ||
|
||
function checkNumberOperand( | ||
operator: Token, | ||
operand: unknown, | ||
): asserts operand is number { | ||
if (typeof operand === "number") { | ||
return; | ||
} | ||
throw new RuntimeError(operator, "Operand must be a number."); | ||
} | ||
|
||
// TODO: would be nice if this worked! | ||
// function checkNumberOperands( | ||
// operator: Token, | ||
// operands: [unknown, unknown], | ||
// ): asserts operands is [number, number] { | ||
// const [left, right] = operands; | ||
// checkNumberOperand(operator, left); | ||
// checkNumberOperand(operator, right); | ||
// } | ||
|
||
export function isTruthy(val: unknown): boolean { | ||
if (val === null) { | ||
return false; | ||
} | ||
if (typeof val === "boolean") { | ||
return val; | ||
} | ||
return true; | ||
} | ||
|
||
export function isEqual(a: unknown, b: unknown): boolean { | ||
return a === b; | ||
} | ||
|
||
export function stringify(val: unknown): string { | ||
if (val === null) { | ||
return "nil"; | ||
} | ||
return String(val); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters