From 1e930305a351dc85ec87d9e90d4ec12c4ee65d78 Mon Sep 17 00:00:00 2001 From: "W. Cho" Date: Thu, 21 Dec 2023 16:14:16 +0900 Subject: [PATCH] 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);