From e89852e6e9b60f33e69f05960346d419f26bca7a Mon Sep 17 00:00:00 2001 From: Won-hee Cho <89635107+wcho21@users.noreply.github.com> Date: Fri, 22 Dec 2023 09:10:33 +0900 Subject: [PATCH] add evaluated type system (#39) --- src/evaluator/environment/index.test.ts | 31 +-- src/evaluator/environment/index.ts | 12 +- src/evaluator/evaluated/index.test.ts | 82 +++++++ src/evaluator/evaluated/index.ts | 95 ++++++++ src/evaluator/index.test.ts | 303 ++++++++++++++++++------ src/evaluator/index.ts | 123 ++++++---- src/index.test.ts | 10 +- src/index.ts | 2 +- 8 files changed, 512 insertions(+), 146 deletions(-) create mode 100644 src/evaluator/evaluated/index.test.ts create mode 100644 src/evaluator/evaluated/index.ts diff --git a/src/evaluator/environment/index.test.ts b/src/evaluator/environment/index.test.ts index cef2761..91741c3 100644 --- a/src/evaluator/environment/index.test.ts +++ b/src/evaluator/environment/index.test.ts @@ -1,49 +1,52 @@ +import type { Evaluated } from "../evaluated"; import Environment from "./"; describe("set()", () => { it("set name and value", () => { const env = new Environment(); + const varName = "foo"; + const varValue = {} as Evaluated; - expect(() => env.set("foo", 42)).not.toThrow(); + expect(() => env.set(varName, varValue)).not.toThrow(); }); }); describe("get()", () => { it("get value after setting the value", () => { const env = new Environment(); + const varName = "foo"; + const varValue = {} as Evaluated; - env.set("foo", 42); - const value = env.get("foo"); + env.set(varName, varValue); - expect(value).toBe(42); + expect(env.get(varName)).toBe(varValue); }); it("get null if not found", () => { const env = new Environment(); + const varNameNotSet = "foo"; - const value = env.get("foo"); - - expect(value).toBe(null); + expect(env.get(varNameNotSet)).toBe(null); }); }); describe("linked environment", () => { it("set super environment and get via sub environment", () => { + const varNameInSuper = "foo"; + const varValueInSuper = {} as Evaluated; + const superEnv = new Environment(); - superEnv.set("foo", 42); + superEnv.set(varNameInSuper, varValueInSuper); const subEnv = new Environment(superEnv); - const value = subEnv.get("foo"); - - expect(value).toBe(42); + expect(subEnv.get(varNameInSuper)).toBe(varValueInSuper); }); it("get null if not found even in super environment", () => { const superEnv = new Environment(); const subEnv = new Environment(superEnv); - const value = subEnv.get("foo"); - - expect(value).toBe(null); + const varNameSetNowhere = "foo"; + expect(subEnv.get(varNameSetNowhere)).toBe(null); }); }); diff --git a/src/evaluator/environment/index.ts b/src/evaluator/environment/index.ts index c95652f..a27a929 100644 --- a/src/evaluator/environment/index.ts +++ b/src/evaluator/environment/index.ts @@ -1,6 +1,8 @@ +import type { Evaluated } from "../evaluated"; + export interface EnvironmentType { - get: (name: string) => unknown; - set: (name: string, value: any) => unknown; + get: (name: string) => Evaluated | null; + set: (name: string, value: Evaluated) => void; } export default class Environment implements EnvironmentType { @@ -12,7 +14,7 @@ export default class Environment implements EnvironmentType { this.table = new Map; } - get(name: string): unknown { + get(name: string): Evaluated | null { // return if found in current environment const fetched = this.table.get(name); if (fetched !== undefined) { @@ -26,9 +28,7 @@ export default class Environment implements EnvironmentType { return this.superEnvironment.get(name); } - set(name: string, value: any): unknown { + set(name: string, value: Evaluated): void { this.table.set(name, value); - - return value; } } diff --git a/src/evaluator/evaluated/index.test.ts b/src/evaluator/evaluated/index.test.ts new file mode 100644 index 0000000..45f024f --- /dev/null +++ b/src/evaluator/evaluated/index.test.ts @@ -0,0 +1,82 @@ +import { + makeEvaluatedNumber, + makeEvaluatedString, + makeEvaluatedBoolean, + makeEvaluatedFunction, + makeEvaluatedEmpty, +} from "./"; +import type { + FunctionExpression +} from "../../parser/syntax-tree"; +import type Environment from "../environment"; + +describe("makeEvaluatedNumber()", () => { + it("make number value", () => { + const evaluated = makeEvaluatedNumber(42); + + expect(evaluated.type).toBe("number"); + expect(evaluated.value).toBe(42); + expect(evaluated.representation).toBe("42"); + }); +}); + +describe("makeEvaluatedString()", () => { + it("make nonempty string value", () => { + const evaluated = makeEvaluatedString("foo bar"); + + expect(evaluated.type).toBe("string"); + expect(evaluated.value).toBe("foo bar"); + expect(evaluated.representation).toBe("'foo bar'"); + }); + + it("make empty string value", () => { + const evaluated = makeEvaluatedString(""); + + expect(evaluated.type).toBe("string"); + expect(evaluated.value).toBe(""); + expect(evaluated.representation).toBe("''"); + }); +}); + +describe("makeEvaluatedBoolean()", () => { + it("make true boolean value", () => { + const evaluated = makeEvaluatedBoolean(true); + + expect(evaluated.type).toBe("boolean"); + expect(evaluated.value).toBe(true); + expect(evaluated.representation).toBe("참"); + }); + + it("make false boolean value", () => { + const evaluated = makeEvaluatedBoolean(false); + + expect(evaluated.type).toBe("boolean"); + expect(evaluated.value).toBe(false); + expect(evaluated.representation).toBe("거짓"); + }); +}); + +describe("makeEvaluatedFunction()", () => { + it("make function value", () => { + const parametersMock = [] as FunctionExpression["parameter"]; + const bodyMock = {} as FunctionExpression["body"]; + const environmentMock = {} as Environment; + + const evaluated = makeEvaluatedFunction(parametersMock, bodyMock, environmentMock); + + expect(evaluated.type).toBe("function"); + expect(evaluated.parameters).toBe(parametersMock); + expect(evaluated.body).toBe(bodyMock); + expect(evaluated.environment).toBe(environmentMock); + expect(evaluated.representation).toBe("(함수)"); + }); +}); + +describe("makeEvaluatedEmpty()", () => { + it("make empty value", () => { + const evaluated = makeEvaluatedEmpty(); + + expect(evaluated.type).toBe("empty"); + expect(evaluated.representation).toBe("(비어있음)"); + }); +}); diff --git a/src/evaluator/evaluated/index.ts b/src/evaluator/evaluated/index.ts new file mode 100644 index 0000000..38b8e8f --- /dev/null +++ b/src/evaluator/evaluated/index.ts @@ -0,0 +1,95 @@ +import type { + FunctionExpression, +} from "../../parser/syntax-tree"; +import type Environment from "../environment"; + +interface EvaluatedBase { + readonly type: string; + readonly representation: string; +} + +export interface EvaluatedNumber extends EvaluatedBase { + readonly type: "number"; + readonly value: number; +} + +export interface EvaluatedString extends EvaluatedBase { + readonly type: "string"; + readonly value: string; +} + +export interface EvaluatedBoolean extends EvaluatedBase { + readonly type: "boolean"; + readonly value: boolean; +} + +export interface EvaluatedFunction extends EvaluatedBase { + readonly type: "function"; + readonly parameters: FunctionExpression["parameter"]; + readonly body: FunctionExpression["body"]; + readonly environment: Environment; +} + +// 'empty' represents the result of running statement (e.g., branching) in REPL +export interface EvaluatedEmpty extends EvaluatedBase { + readonly type: "empty"; +} + +export type Evaluated = + EvaluatedPrimitive | + EvaluatedFunction | + EvaluatedEmpty; +export type EvaluatedPrimitive = + EvaluatedNumber | + EvaluatedString | + EvaluatedBoolean; + +export type MakeEvaluatedNumber = (value: number) => EvaluatedNumber; +export const makeEvaluatedNumber: MakeEvaluatedNumber = value => ({ + type: "number", + value, + get representation() { + return `${value}`; + }, +}); + +export type MakeEvaluatedString = (value: string) => EvaluatedString; +export const makeEvaluatedString: MakeEvaluatedString = value => ({ + type: "string", + value, + get representation() { + return `'${value}'`; + }, +}); + +export type MakeEvaluatedBoolean = (value: boolean) => EvaluatedBoolean; +export const makeEvaluatedBoolean: MakeEvaluatedBoolean = value => ({ + type: "boolean", + value, + get representation() { + return value ? "참" : "거짓"; + }, +}); + +export type MakeEvaluatedFunction = ( + parameters: FunctionExpression["parameter"], + body: FunctionExpression["body"], + environment: Environment +) => EvaluatedFunction; +export const makeEvaluatedFunction: MakeEvaluatedFunction = (parameters, body, environment) => ({ + type: "function", + parameters, + body, + environment, + get representation() { + return "(함수)"; + }, +}); + +export type MakeEvaluatedEmpty = () => EvaluatedEmpty; +export const makeEvaluatedEmpty: MakeEvaluatedEmpty = () => ({ + type: "empty", + get representation() { + return "(비어있음)"; + }, +}); diff --git a/src/evaluator/index.test.ts b/src/evaluator/index.test.ts index 4737010..2cc6ec0 100644 --- a/src/evaluator/index.test.ts +++ b/src/evaluator/index.test.ts @@ -1,15 +1,56 @@ import Lexer from "../lexer"; import Parser from "../parser"; +import type { EvaluatedPrimitive, EvaluatedFunction, EvaluatedEmpty } from "./evaluated"; import Evaluator from "./"; import Environment from "./environment"; +const evaluateInput = (input: string) => { + const lexer = new Lexer(input); + const parser = new Parser(lexer); + const program = parser.parseProgram(); + + const evaluator = new Evaluator(); + const environment = new Environment(); + const evaluated = evaluator.evaluate(program, environment); + + return evaluated; +}; + +const testEvaluatingPrimitive = ({ input, expected }: { input: string, expected: any }): void => { + const evaluated = evaluateInput(input) as EvaluatedPrimitive; + + expect(evaluated.value).toBe(expected); +}; + +const testEvaluatingFunction = ({ input, expectedParamsLength }: { input: string, expectedParamsLength: number }): void => { + const evaluated = evaluateInput(input) as EvaluatedFunction; + + expect(evaluated).toHaveProperty("parameters"); + expect(evaluated.parameters.length).toBe(expectedParamsLength); + expect(evaluated).toHaveProperty("body"); + expect(evaluated).toHaveProperty("environment"); +}; + +const testEvaluatingEmpty = ({ input }: { input: string }): void => { + const evaluated = evaluateInput(input) as EvaluatedEmpty; + + expect(evaluated.type).toBe("empty"); +}; + describe("evaluate()", () => { - describe("simple expression", () => { + describe("single numbers", () => { const cases = [ { input: "5", expected: 5 }, { input: "-5", expected: -5 }, { input: "--5", expected: 5 }, { input: "+5", expected: 5 }, + ]; + + it.each(cases)("evaluate $input", testEvaluatingPrimitive); + }); + + describe("simple arithmetic expressions", () => { + const cases = [ { input: "100+25", expected: 125 }, { input: "100-25", expected: 75 }, { input: "100*25", expected: 2500 }, @@ -17,15 +58,72 @@ describe("evaluate()", () => { { input: "100+25+4", expected: 129 }, { input: "100+25-4", expected: 121 }, { input: "100+25*4", expected: 200 }, + ]; + + it.each(cases)("evaluate $input", testEvaluatingPrimitive); + }); + + describe("left associativity of arithmetic operation", () => { + const cases = [ + { input: "100-25-4", expected: 71 }, + { input: "100/25/4", expected: 1 }, + ]; + + it.each(cases)("evaluate $input", testEvaluatingPrimitive); + }); + + describe("grouped arithmetic expressions", () => { + const cases = [ + { input: "100-(25-4)", expected: 79 }, + { input: "12-(34-56)", expected: 34 }, + { input: "12*(12/6)", expected: 24 }, + { input: "12+((30+4)-3*(12/(56-50)))", expected: 40 }, + ]; + + it.each(cases)("evaluate $input", testEvaluatingPrimitive); + }); + + describe("arithmetic expressions with floating point number", () => { + const cases = [ + { input: "0.75 + 1.25", expected: 2 }, + { input: "2.5 / 0.5", expected: 5 }, + ]; + + it.each(cases)("evaluate $input", testEvaluatingPrimitive); + }); + + describe("single boolean", () => { + const cases = [ { input: "참", expected: true }, { input: "거짓", expected: false }, - { input: "'foo bar'", expected: "foo bar" }, + ]; + + it.each(cases)("evaluate $input", testEvaluatingPrimitive); + }); + + describe("logical not expressions", () => { + const cases = [ { input: "!참", expected: false }, { input: "!거짓", expected: true }, { input: "!!참", expected: true }, { input: "!!거짓", expected: false }, + ]; - /* test case for comparison expressions */ + it.each(cases)("evaluate $input", testEvaluatingPrimitive); + }); + + describe("single string", () => { + const cases = [ + { input: "''", expected: "" }, + { input: "'foo bar'", expected: "foo bar" }, + { input: "'한글 단어'", expected: "한글 단어" }, + ]; + + it.each(cases)("evaluate $input", testEvaluatingPrimitive); + }); + + describe("boolean comparison", () => { + const cases = [ { input: "참 == 참", expected: true }, { input: "거짓 == 참", expected: false }, { input: "참 == 거짓", expected: false }, @@ -34,6 +132,13 @@ describe("evaluate()", () => { { input: "거짓 != 참", expected: true }, { input: "참 != 거짓", expected: true }, { input: "거짓 != 거짓", expected: false }, + ]; + + it.each(cases)("evaluate $input", testEvaluatingPrimitive); + }); + + describe("number comparison", () => { + const cases = [ { input: "2 > 1", expected: true }, { input: "1 > 1", expected: false }, { input: "1 > 2", expected: false }, @@ -46,37 +151,44 @@ describe("evaluate()", () => { { input: "2 <= 1", expected: false }, { input: "1 <= 1", expected: true }, { input: "1 <= 2", expected: true }, - { input: "!(1 == 1)", expected: false }, - { input: "!!(1 == 1)", expected: true }, - - /* test case for left associativity */ - { input: "100-25-4", expected: 71 }, - { input: "100/25/4", expected: 1 }, + ]; - /* test case for grouped expression */ - { input: "100-(25-4)", expected: 79 }, - { input: "12-(34-56)", expected: 34 }, - { input: "12*(12/6)", expected: 24 }, - { input: "12+((30+4)-3*(12/(56-50)))", expected: 40 }, + it.each(cases)("evaluate $input", testEvaluatingPrimitive); + }); - /* test case for floating point numbers */ - { input: "0.75 + 1.25", expected: 2 }, - { input: "2.5 / 0.5", expected: 5 }, + describe("string comparison", () => { + const cases = [ + { input: "'사과' == '사과'", expected: true }, + { input: "'사과' != '사과'", expected: false }, + { input: "'사과' == '바나나'", expected: false }, + { input: "'사과' != '바나나'", expected: true }, + { input: "'B' > 'A'", expected: true }, + { input: "'A' > 'A'", expected: false }, + { input: "'A' > 'B'", expected: false }, + { input: "'B' >= 'A'", expected: true }, + { input: "'A' >= 'A'", expected: true }, + { input: "'A' >= 'B'", expected: false }, + { input: "'B' < 'A'", expected: false }, + { input: "'A' < 'A'", expected: false }, + { input: "'A' < 'B'", expected: true }, + { input: "'B' <= 'A'", expected: false }, + { input: "'A' <= 'A'", expected: true }, + { input: "'A' <= 'B'", expected: true }, ]; - it.each(cases)("evaluate $input", ({ input, expected }) => { - const lexer = new Lexer(input); - const parser = new Parser(lexer); - const program = parser.parseProgram(); - const evaluator = new Evaluator(); - const environment = new Environment(); - const evaluated = evaluator.evaluate(program, environment); + it.each(cases)("evaluate $input", testEvaluatingPrimitive); + }); - expect(evaluated).toBe(expected); - }); + describe("logical not operation to boolean expression", () => { + const cases = [ + { input: "!(1 == 1)", expected: false }, + { input: "!!(1 == 1)", expected: true }, + ]; + + it.each(cases)("evaluate $input", testEvaluatingPrimitive); }); - describe("branch statements", () => { + describe("branch statements yielding something", () => { const cases = [ { name: "simple if statement with boolean literal predicate", @@ -110,79 +222,118 @@ describe("evaluate()", () => { }, ]; - it.each(cases)("evaluate $input", ({ input, expected }) => { - const lexer = new Lexer(input); - const parser = new Parser(lexer); - const program = parser.parseProgram(); - const evaluator = new Evaluator(); - const environment = new Environment(); - const evaluated = evaluator.evaluate(program, environment); + it.each(cases)("evaluate $input", testEvaluatingPrimitive); + }); + + describe("branch statements yielding nothing", () => { + const cases = [ + { + name: "simple if statement with boolean literal predicate", + input: "만약 거짓 { 3 }", + }, + { + name: "simple if statement with boolean expression predicate", + input: "만약 1 == 2 { 4 }", + }, + { + name: "simple if statement with variable comparison predicate", + input: "사과 = 3 바나나 = 4 만약 사과 > 바나나 { 5 }", + }, + ]; - expect(evaluated).toBe(expected); - }); + it.each(cases)("evaluate $input", testEvaluatingEmpty); }); - describe("variable statements", () => { + describe("nested branch statements yielding something", () => { const cases = [ - { name: "integer variable with number literal", input: "foo = 42 foo", expected: 42 }, - { name: "integer variable with arithmetic expression", input: "foo = 42 * (8 / 4) + (1 - (2 - 1)) foo", expected: 84 }, - { name: "two integer variables with number literal", input: "foo = 42 bar = foo + 1 bar", expected: 43 }, - { name: "arithmetic expression with variables", input: "foo = 42 bar = 43 baz = 44 qux = (bar * (baz - foo))", expected: 86 }, - { name: "Korean integer variable with number literal", input: "변수 = 42 변수", expected: 42 }, + { + name: "nested if statements", + input: "만약 참 { 만약 참 { 만약 참 { 1 } } }", + expected: 1, + }, + { + name: "nested if-else statements", + input: "만약 거짓 { 0 } 아니면 { 만약 참 { 만약 거짓 { 1 } 아니면 { 2 } } 아니면 { 3 } }", + expected: 2, + }, + ]; + + it.each(cases)("evaluate $input", testEvaluatingPrimitive); + }); + + describe("nested branch statements yielding nothing", () => { + const cases = [ + { + name: "nested if statements", + input: "만약 참 { 만약 참 { 만약 거짓 { 1 } } }", + }, + { + name: "nested if and if-else statements", + input: "만약 거짓 { 만약 참 { 만약 거짓 { 0 } 아니면 { 1 } } 아니면 { 2 } }", + }, ]; - it.each(cases)("evaluate $name", ({ input, expected }) => { - const lexer = new Lexer(input); - const parser = new Parser(lexer); - const program = parser.parseProgram(); - const evaluator = new Evaluator(); - const environment = new Environment(); - const evaluated = evaluator.evaluate(program, environment); + it.each(cases)("evaluate $input", testEvaluatingEmpty); + }); + + describe("variable statements", () => { + const cases = [ + { + name: "integer variable with number literal", + input: "foo = 42 foo", + expected: 42 + }, + { + name: "integer variable with arithmetic expression", + input: "foo = 42 * (8 / 4) + (1 - (2 - 1)) foo", + expected: 84 + }, + { + name: "two integer variables with number literal", + input: "foo = 42 bar = foo + 1 bar", + expected: 43 + }, + { + name: "arithmetic expression with variables", + input: "foo = 42 bar = 43 baz = 44 qux = (bar * (baz - foo))", + expected: 86 + }, + { + name: "Korean integer variable with number literal", + input: "변수 = 42 변수", + expected: 42 + }, + ]; - expect(evaluated).toBe(expected); - }); + it.each(cases)("evaluate $name", testEvaluatingPrimitive); }); describe("function expressions", () => { const cases = [ { name: "simple function expression", - input: "함수 () { 1 }" + input: "함수 () { 1 }", + expectedParamsLength: 0, + }, + { + name: "simple function expression", + input: "함수 (사과, 바나나, 포도) { 1 }", + expectedParamsLength: 3, }, ]; - it.each(cases)("evaluate $name", ({ input }) => { - const lexer = new Lexer(input); - const parser = new Parser(lexer); - const program = parser.parseProgram(); - const evaluator = new Evaluator(); - const environment = new Environment(); - const evaluated = evaluator.evaluate(program, environment); - - expect(evaluated).not.toBeUndefined(); - expect(evaluated).toHaveProperty("parameters"); - expect(evaluated).toHaveProperty("body"); - expect(evaluated).toHaveProperty("environment"); - }); + it.each(cases)("evaluate $name", testEvaluatingFunction); }); describe("call expressions", () => { const cases = [ { name: "function call with function literal", - input: "함수(바나나) { 바나나 + 1 }(42)" + input: "함수(바나나) { 바나나 + 1 }(42)", + expected: 43, }, ]; - it.each(cases)("evaluate $name", ({ input }) => { - const lexer = new Lexer(input); - const parser = new Parser(lexer); - const program = parser.parseProgram(); - const evaluator = new Evaluator(); - const environment = new Environment(); - const evaluated = evaluator.evaluate(program, environment); - - expect(evaluated).toBe(43); - }); + it.each(cases)("evaluate $name", testEvaluatingPrimitive); }); }); diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index 3a58b83..490b595 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -1,103 +1,122 @@ import type { Program, Block, BranchStatement, Node, Expression } from "../parser"; +import { + makeEvaluatedNumber, + makeEvaluatedBoolean, + makeEvaluatedString, + makeEvaluatedFunction, + makeEvaluatedEmpty, +} from "./evaluated"; +import type { + Evaluated, + EvaluatedNumber, + EvaluatedBoolean, + EvaluatedFunction, +} from "./evaluated"; import Environment from "./environment"; -// TODO: fix any return type to specific ones (by implement value system) export default class Evaluator { - private evaluateProgram(node: Program, env: Environment): any { - let evaluated; - - for (const statement of node.statements) { - evaluated = this.evaluate(statement, env); + private evaluateProgram(node: Program, env: Environment): Evaluated { + if (node.statements.length === 0) { + return makeEvaluatedEmpty(); } + const evaluatedStatements = node.statements.map(statement => this.evaluate(statement, env)); + const evaluated = evaluatedStatements[evaluatedStatements.length-1]; + return evaluated; } - private evaluateBlock(node: Block, env: Environment): any { - let evaluated; - - for (const statement of node.statements) { - evaluated = this.evaluate(statement, env); + private evaluateBlock(node: Block, env: Environment): Evaluated { + if (node.statements.length === 0) { + throw new Error(`block cannot be empty`); } + const evaluatedStatements = node.statements.map(statement => this.evaluate(statement, env)); + const evaluated = evaluatedStatements[evaluatedStatements.length-1]; + return evaluated; } - private evaluatePrefixNumberExpression(prefix: string, operand: number): any { + private evaluatePrefixNumberExpression(prefix: string, operand: EvaluatedNumber): EvaluatedNumber { if (prefix === "+") { return operand; } if (prefix === "-") { - return -operand; + return makeEvaluatedNumber(-operand.value); } throw new Error(`bad prefix ${prefix}`); } - private evaluatePrefixBooleanExpression(prefix: string, operand: boolean): any { + private evaluatePrefixBooleanExpression(prefix: string, operand: EvaluatedBoolean): EvaluatedBoolean { if (prefix === "!") { - return !operand; + return makeEvaluatedBoolean(!operand.value); } throw new Error(`bad prefix ${prefix}`); } - private evaluateBranchStatement(node: BranchStatement, env: Environment): any { + private evaluateBranchStatement(node: BranchStatement, env: Environment): Evaluated { const predicate = this.evaluate(node.predicate, env); + if (predicate.type !== "boolean") { + throw new Error(`expected boolean expression predicate, but received ${predicate.type}`); + } - if (predicate) { + if (predicate.value) { const consequence = this.evaluate(node.consequence, env); return consequence; } // early return if no else block if (typeof node.alternative === "undefined") { - return undefined; + return makeEvaluatedEmpty(); } const alternative = this.evaluate(node.alternative, env); return alternative; } - private evaluateInfixExpression(infix: string, left: unknown, right: unknown): any { + private evaluateInfixExpression(infix: string, left: Evaluated, right: Evaluated): Evaluated { // type matching order is important: more inclusive case first if ( - (typeof left === "boolean" && typeof right === "boolean") || - (typeof left === "number" && typeof right === "number") + (left.type === "boolean" && right.type === "boolean") || + (left.type === "number" && right.type === "number") || + (left.type === "string" && right.type === "string") ) { if (infix === "==") { - return left === right; + return makeEvaluatedBoolean(left.value == right.value); } if (infix === "!=") { - return left !== right; + return makeEvaluatedBoolean(left.value != right.value); } if (infix === ">") { - return left > right; + return makeEvaluatedBoolean(left.value > right.value); } if (infix === "<") { - return left < right; + return makeEvaluatedBoolean(left.value < right.value); } if (infix === ">=") { - return left >= right; + return makeEvaluatedBoolean(left.value >= right.value); } if (infix === "<=") { - return left <= right; + return makeEvaluatedBoolean(left.value <= right.value); } } - if (typeof left === "number" && typeof right === "number") { + if (left.type === "number" && right.type === "number") { if (infix === "+") { - return left + right; + return makeEvaluatedNumber(left.value + right.value); } if (infix === "-") { - return left - right; + return makeEvaluatedNumber(left.value - right.value); } if (infix === "*") { - return left * right; + return makeEvaluatedNumber(left.value * right.value); } if (infix === "/") { - return left / right; + // TODO: guard division by zero + return makeEvaluatedNumber(left.value / right.value); } throw new Error(`bad infix ${infix} for number operands`); @@ -106,73 +125,88 @@ export default class Evaluator { throw new Error(`bad infix ${infix}, with left '${left}' and right '${right}'`); } - evaluate(node: Node, env: Environment): any { + evaluate(node: Node, env: Environment): Evaluated { if (node.type === "program") { return this.evaluateProgram(node, env); } + if (node.type === "block") { return this.evaluateBlock(node, env); } + if (node.type === "branch statement") { return this.evaluateBranchStatement(node, env); } + if (node.type === "expression statement") { return this.evaluate(node.expression, env); } + if (node.type === "number node") { - return node.value; + return makeEvaluatedNumber(node.value); } + if (node.type === "boolean node") { - return node.value; + return makeEvaluatedBoolean(node.value); } + if (node.type === "string node") { - return node.value; + return makeEvaluatedString(node.value); } + if (node.type === "infix expression") { const left = this.evaluate(node.left, env); const right = this.evaluate(node.right, env); return this.evaluateInfixExpression(node.infix, left, right); } + if (node.type === "prefix expression") { const subExpression = this.evaluate(node.expression, env); + if ( (node.prefix === "+" || node.prefix === "-") && - typeof subExpression == "number" + subExpression.type == "number" ) { return this.evaluatePrefixNumberExpression(node.prefix, subExpression); } - if (node.prefix === "!" && typeof subExpression === "boolean") { + if (node.prefix === "!" && subExpression.type === "boolean") { return this.evaluatePrefixBooleanExpression(node.prefix, subExpression); } throw new Error(`bad prefix expression: prefix: '${node.prefix}' with type: '${typeof subExpression}'`); } + if (node.type === "function expression") { const parameters = node.parameter; const body = node.body; - return { parameters, body, environment: env }; + return makeEvaluatedFunction(parameters, body, env); } + if (node.type === "call") { const functionToCall = this.evaluate(node.functionToCall, env); + if (functionToCall.type !== "function") { + throw new Error(`expected function but received ${functionToCall.type}`); + } const callArguments = this.parseCallArguments(node.callArguments, env); const value = this.evaluateFunctionCall(functionToCall, callArguments); return value; } - if (node.type === "assignment") { - const varValue = this.evaluate(node.right, env); + if (node.type === "assignment") { if (node.left.type !== "identifier") { throw new Error(`expected identifier on left value, but received ${typeof node.left.type}`); } const varName = node.left.value; + const varValue = this.evaluate(node.right, env); env.set(varName, varValue); - return varValue; + return varValue; // evaluated value of assignment is the evaluated value of variable } + if (node.type === "identifier") { const varName = node.value; const value = env.get(varName); @@ -185,9 +219,10 @@ export default class Evaluator { } const exhaustiveCheck: never = node; + return exhaustiveCheck; } - private parseCallArguments(callArguments: Expression[], env: Environment): any[] { + private parseCallArguments(callArguments: Expression[], env: Environment): Evaluated[] { const values = []; for (const arg of callArguments) { const value = this.evaluate(arg, env); @@ -196,7 +231,7 @@ export default class Evaluator { return values; } - private evaluateFunctionCall(functionToCall: any, callArguments: any[]): any { + private evaluateFunctionCall(functionToCall: EvaluatedFunction, callArguments: Evaluated[]): Evaluated { const functionEnv = new Environment(functionToCall.environment); for (let i = 0; i < functionToCall.parameters.length; ++i) { const name = functionToCall.parameters[i].value; diff --git a/src/index.test.ts b/src/index.test.ts index f5981f0..f227f53 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -17,23 +17,23 @@ it("execute 2.5/0.5", () => { }); it("execute 참", () => { - expect(execute("참")).toBe("true"); // TODO(?): display as "참" + expect(execute("참")).toBe("참"); }); it("execute 1 == 2", () => { - expect(execute("1 == 2")).toBe("false"); + expect(execute("1 == 2")).toBe("거짓"); }); it("execute 2 > 1 == 참", () => { - expect(execute("2 > 1 == 참")).toBe("true"); + expect(execute("2 > 1 == 참")).toBe("참"); }); it("execute 1 != 1 == 거짓", () => { // note that comparison is left associative - expect(execute("1 != 1 == 거짓")).toBe("true"); + expect(execute("1 != 1 == 거짓")).toBe("참"); }); it("execute 거짓 == (1 < 1+1)", () => { - expect(execute("거짓 == (1 < 1+1)")).toBe("false"); + expect(execute("거짓 == (1 < 1+1)")).toBe("거짓"); }); it("execute 만약 1 == 1 { 2 } 아니면 { 3 }", () => { diff --git a/src/index.ts b/src/index.ts index 5850d2a..278f9ac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,5 +11,5 @@ export const execute = (input: string): string => { const environment = new Environment(); const evaluated = evaluator.evaluate(parsed, environment); - return String(evaluated); + return String(evaluated.representation); };