From 581627dc65fda5ba2ba142141ce896524df2deac Mon Sep 17 00:00:00 2001 From: "W. Cho" Date: Thu, 21 Dec 2023 15:30:02 +0900 Subject: [PATCH 1/4] add call syntax node --- src/evaluator/index.ts | 3 ++- src/parser/syntax-tree/expression/index.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index 5533125..a1f891c 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -176,7 +176,8 @@ export default class Evaluator { return value; } - const exhaustiveCheck: never = node; + // TODO: uncomment + // const exhaustiveCheck: never = node; } } diff --git a/src/parser/syntax-tree/expression/index.ts b/src/parser/syntax-tree/expression/index.ts index e73c6a3..e5ea74d 100644 --- a/src/parser/syntax-tree/expression/index.ts +++ b/src/parser/syntax-tree/expression/index.ts @@ -8,6 +8,7 @@ export type Expression = PrefixExpression | InfixExpression | FunctionExpression | + Call | Assignment; export interface Identifier { @@ -49,6 +50,12 @@ export interface FunctionExpression { body: Block; } +export interface Call { + type: "call"; + functionToCall: Identifier | FunctionExpression; + callArguments: Expression[]; +} + export interface Assignment { type: "assignment"; left: Identifier; @@ -94,6 +101,12 @@ export const makeFunctionExpression = (body: FunctionExpression["body"], paramet body, }); +export const makeCall = (functionToCall: Call["functionToCall"], callArguments: Call["callArguments"]): Call => ({ + type: "call", + functionToCall, + callArguments, +}); + export const makeAssignment = (left: Assignment["left"], right: Assignment["right"]): Assignment => ({ type: "assignment", left, From 1e930305a351dc85ec87d9e90d4ec12c4ee65d78 Mon Sep 17 00:00:00 2001 From: "W. Cho" Date: Thu, 21 Dec 2023 16:14:16 +0900 Subject: [PATCH 2/4] parse call expression --- src/parser/index.test.ts | 110 +++++++++++++++++++++++++++++++++++++++ src/parser/index.ts | 57 ++++++++++++++++++-- 2 files changed, 164 insertions(+), 3 deletions(-) diff --git a/src/parser/index.test.ts b/src/parser/index.test.ts index 8fcb8df..4aab415 100644 --- a/src/parser/index.test.ts +++ b/src/parser/index.test.ts @@ -819,6 +819,116 @@ describe("parseProgram()", () => { it.each(cases)("parse $name", testParsing); }); + describe("calls", () => { + const cases: { name: string, input: string, expected: Program }[] = [ + { + name: "call function without arguments", + input: "과일()", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "call", + functionToCall: { type: "identifier", value: "과일" }, + callArguments: [], + }, + }, + ], + }, + }, + { + name: "call function with identifier arguments", + input: "과일(사과, 바나나, 포도)", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "call", + functionToCall: { type: "identifier", value: "과일" }, + callArguments: [ + { type: "identifier", value: "사과" }, + { type: "identifier", value: "바나나" }, + { type: "identifier", value: "포도" }, + ], + }, + }, + ], + }, + }, + { + name: "call function with expression arguments", + input: "과일(1, 2+3)", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "call", + functionToCall: { type: "identifier", value: "과일" }, + callArguments: [ + { type: "number node", value: 1 }, + { + type: "infix expression", + infix: "+", + left: { type: "number node", value: 2 }, + right: { type: "number node", value: 3 }, + }, + ], + }, + }, + ], + }, + }, + { + name: "call function with function literal", + input: "함수(사과, 바나나){사과 + 바나나}(1, 2)", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "call", + functionToCall: { + type: "function expression", + parameter: [ + { type: "identifier", value: "사과" }, + { type: "identifier", value: "바나나" }, + ], + body: { + type: "block", + statements: [ + { + type: "expression statement", + expression: { + type: "infix expression", + infix: "+", + left: { type: "identifier", value: "사과" }, + right: { type: "identifier", value: "바나나" }, + }, + }, + ], + }, + }, + callArguments: [ + { type: "number node", value: 1 }, + { type: "number node", value: 2 }, + ], + }, + }, + ], + }, + }, + ]; + + it.each(cases)("parse $name", testParsing); + }); + describe("branch statements", () => { const cases: { name: string, input: string, expected: Program }[] = [ { diff --git a/src/parser/index.ts b/src/parser/index.ts index 511a4fc..a6f767c 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -11,6 +11,7 @@ import { makePrefixExpression, makeInfixExpression, makeFunctionExpression, + makeCall, } from "./syntax-tree"; import type { Program, @@ -22,6 +23,8 @@ import type { BranchStatement, ExpressionStatement, Expression, + FunctionExpression, + Call, Identifier, InfixExpression, } from "./syntax-tree"; @@ -36,6 +39,7 @@ const bindingPower = { summative: 50, productive: 60, prefix: 70, + call: 80, }; const getBindingPower = (infix: string): BindingPower => { switch (infix) { @@ -54,6 +58,8 @@ const getBindingPower = (infix: string): BindingPower => { case "*": case "/": return bindingPower.productive; + case "(": // when '(' is used infix operator, it behaves as call operator + return bindingPower.call; default: return bindingPower.lowest; } @@ -272,14 +278,21 @@ export default class Parser { } private parseInfixExpression(left: Expression): Expression | null { - let token = this.buffer.read(); + const token = this.buffer.read(); + this.buffer.next(); // eat infix token + + if (token.type === "group delimiter" && token.value === "(") { + if (left.type !== "function expression" && left.type !== "identifier") { + throw new Error(`expected function expression or identifier, but received ${left.type}`); + } + return this.parseCall(left); + } + if (token.type !== "operator") { return null; } const infix = token.value; - this.buffer.next(); // eat infix token - if (infix === "=" && left.type === "identifier") { return this.parseAssignment(left); } @@ -300,6 +313,44 @@ export default class Parser { return null; } + private parseCall(functionToCall: Identifier | FunctionExpression): Call { + const callArguments = this.parseCallArguments(); + + return makeCall(functionToCall, callArguments); + } + + private parseCallArguments(): Expression[] { + const maybeExpressionOrGroupEnd = this.buffer.read(); + if (maybeExpressionOrGroupEnd.type === "group delimiter" && maybeExpressionOrGroupEnd.value === ")") { + this.buffer.next(); + + return []; + } + + const firstArgument = this.parseExpression(bindingPower.lowest); + + const callArguments = [firstArgument]; + while (true) { + const maybeComma = this.buffer.read(); + if (maybeComma.type !== "separator") { + break; + } + this.buffer.next(); + + const argument = this.parseExpression(bindingPower.lowest); + callArguments.push(argument); + } + + // expect ')' + const maybeGroupEnd = this.buffer.read(); + this.buffer.next(); + if (maybeGroupEnd.type !== "group delimiter" || maybeGroupEnd.value !== ")") { + throw new Error(`expect ) but received ${maybeGroupEnd.type}`); + } + + return callArguments; + } + private parseAssignment(left: Identifier): Expression { const infix = "="; const infixBindingPower = getBindingPower(infix); From fcc617dbf4d008612e5df08583445f307f9552ba Mon Sep 17 00:00:00 2001 From: "W. Cho" Date: Thu, 21 Dec 2023 18:42:54 +0900 Subject: [PATCH 3/4] fix token eating logic while parsing --- src/parser/index.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ src/parser/index.ts | 11 ++++++++--- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/parser/index.test.ts b/src/parser/index.test.ts index 4aab415..3dbd0a3 100644 --- a/src/parser/index.test.ts +++ b/src/parser/index.test.ts @@ -1015,4 +1015,42 @@ describe("parseProgram()", () => { it.each(cases)("parse $name", testParsing); }); + + describe("complex expression", () => { + const cases: { name: string, input: string, expected: Program }[] = [ + { + name: "assignment and arithmetic expression", + input: "변수1 = 1 ((변수1 + 변수1) * 변수1)", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "assignment", + left: { type: "identifier", value: "변수1" }, + right: { type: "number node", value: 1 }, + }, + }, + { + type: "expression statement", + expression: { + type: "infix expression", + infix: "*", + left: { + type: "infix expression", + infix: "+", + left: { type: "identifier", value: "변수1" }, + right: { type: "identifier", value: "변수1" }, + }, + right: { type: "identifier", value: "변수1" }, + }, + }, + ], + }, + }, + ]; + + it.each(cases)("parse $name", testParsing); + }); }); diff --git a/src/parser/index.ts b/src/parser/index.ts index a6f767c..c23626e 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -278,13 +278,15 @@ export default class Parser { } private parseInfixExpression(left: Expression): Expression | null { + // note: do not eat token and just return null if not parsable const token = this.buffer.read(); - this.buffer.next(); // eat infix token if (token.type === "group delimiter" && token.value === "(") { if (left.type !== "function expression" && left.type !== "identifier") { - throw new Error(`expected function expression or identifier, but received ${left.type}`); + return null; } + + this.buffer.next(); // eat infix token return this.parseCall(left); } @@ -294,7 +296,9 @@ export default class Parser { const infix = token.value; if (infix === "=" && left.type === "identifier") { - return this.parseAssignment(left); + this.buffer.next(); // eat infix token + const a= this.parseAssignment(left); + return a; } if ( infix === "+" || @@ -308,6 +312,7 @@ export default class Parser { infix === ">=" || infix === "<=" ) { + this.buffer.next(); // eat infix token return this.parseArithmeticInfixExpression(left, infix); } return null; From 077efc308e87992a861b3aa9f06aa6a0064f8dcb Mon Sep 17 00:00:00 2001 From: "W. Cho" Date: Thu, 21 Dec 2023 18:43:22 +0900 Subject: [PATCH 4/4] evaluate call expression --- src/evaluator/environment/index.test.ts | 21 +++++++++++++++ src/evaluator/environment/index.ts | 16 ++++++++++-- src/evaluator/index.test.ts | 20 +++++++++++++++ src/evaluator/index.ts | 34 ++++++++++++++++++++++--- src/index.test.ts | 8 ++++-- 5 files changed, 92 insertions(+), 7 deletions(-) diff --git a/src/evaluator/environment/index.test.ts b/src/evaluator/environment/index.test.ts index 89f8539..cef2761 100644 --- a/src/evaluator/environment/index.test.ts +++ b/src/evaluator/environment/index.test.ts @@ -26,3 +26,24 @@ describe("get()", () => { expect(value).toBe(null); }); }); + +describe("linked environment", () => { + it("set super environment and get via sub environment", () => { + const superEnv = new Environment(); + superEnv.set("foo", 42); + const subEnv = new Environment(superEnv); + + const value = subEnv.get("foo"); + + expect(value).toBe(42); + }); + + 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); + }); +}); diff --git a/src/evaluator/environment/index.ts b/src/evaluator/environment/index.ts index c0a1a59..c95652f 100644 --- a/src/evaluator/environment/index.ts +++ b/src/evaluator/environment/index.ts @@ -4,14 +4,26 @@ export interface EnvironmentType { } export default class Environment implements EnvironmentType { + private readonly superEnvironment: Environment | null; private readonly table: Map; - constructor() { + constructor(superEnvironment: Environment | null = null) { + this.superEnvironment = superEnvironment; this.table = new Map; } get(name: string): unknown { - return this.table.get(name) ?? null; + // return if found in current environment + const fetched = this.table.get(name); + if (fetched !== undefined) { + return fetched; + } + + // return value in super environment + if (this.superEnvironment === null) { + return null; + } + return this.superEnvironment.get(name); } set(name: string, value: any): unknown { diff --git a/src/evaluator/index.test.ts b/src/evaluator/index.test.ts index 827bf50..4737010 100644 --- a/src/evaluator/index.test.ts +++ b/src/evaluator/index.test.ts @@ -165,4 +165,24 @@ describe("evaluate()", () => { expect(evaluated).toHaveProperty("environment"); }); }); + + describe("call expressions", () => { + const cases = [ + { + name: "function call with function literal", + input: "함수(바나나) { 바나나 + 1 }(42)" + }, + ]; + + 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); + }); + }); }); diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index a1f891c..3a58b83 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -1,4 +1,4 @@ -import type { Program, Block, BranchStatement, Node } from "../parser"; +import type { Program, Block, BranchStatement, Node, Expression } from "../parser"; import Environment from "./environment"; // TODO: fix any return type to specific ones (by implement value system) @@ -153,6 +153,14 @@ export default class Evaluator { const body = node.body; return { parameters, body, environment: env }; } + if (node.type === "call") { + const functionToCall = this.evaluate(node.functionToCall, env); + + 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); @@ -176,8 +184,28 @@ export default class Evaluator { return value; } - // TODO: uncomment - // const exhaustiveCheck: never = node; + const exhaustiveCheck: never = node; + } + + private parseCallArguments(callArguments: Expression[], env: Environment): any[] { + const values = []; + for (const arg of callArguments) { + const value = this.evaluate(arg, env); + values.push(value); + } + return values; + } + + private evaluateFunctionCall(functionToCall: any, callArguments: any[]): any { + const functionEnv = new Environment(functionToCall.environment); + for (let i = 0; i < functionToCall.parameters.length; ++i) { + const name = functionToCall.parameters[i].value; + const value = callArguments[i]; + functionEnv.set(name, value); + } + + const value = this.evaluate(functionToCall.body, functionEnv); + return value; } } diff --git a/src/index.test.ts b/src/index.test.ts index 9782257..f5981f0 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -44,6 +44,10 @@ it("execute 만약 1 != 1 { 2 } 아니면 { 3 }", () => { expect(execute("만약 1 != 1 { 2 } 아니면 { 3 }")).toBe("3"); }); -it("execute assignment", () => { - expect(execute("변수1 = 4 변수2 = 9 (변수2 - 변수1) * 변수1")).toBe("20"); +it("execute single assignment", () => { + expect(execute("변수1 = 4 변수1")).toBe("4"); +}); + +it("execute assignment and calculation", () => { + expect(execute("변수1 = 4 변수2 = 9 ((변수1 - 변수2) * 변수1)")).toBe("-20"); });