diff --git a/src/evaluator/index.test.ts b/src/evaluator/index.test.ts index f09cf7e..827bf50 100644 --- a/src/evaluator/index.test.ts +++ b/src/evaluator/index.test.ts @@ -142,4 +142,27 @@ describe("evaluate()", () => { expect(evaluated).toBe(expected); }); }); + + describe("function expressions", () => { + const cases = [ + { + name: "simple function expression", + input: "함수 () { 1 }" + }, + ]; + + 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"); + }); + }); }); diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index 2dfb450..5533125 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -148,6 +148,11 @@ export default class Evaluator { 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 }; + } if (node.type === "assignment") { const varValue = this.evaluate(node.right, env); diff --git a/src/lexer/index.test.ts b/src/lexer/index.test.ts index 900ac75..301fbaf 100644 --- a/src/lexer/index.test.ts +++ b/src/lexer/index.test.ts @@ -8,6 +8,7 @@ import { groupDelimiter, blockDelimiter, keyword, + separator, illegal, end, } from "./token"; @@ -21,6 +22,7 @@ import type { GroupDelimiter, BlockDelimiter, Keyword, + Separator, Illegal, End, } from "./token"; @@ -122,11 +124,20 @@ describe("getToken()", () => { const cases: { input: string, expected: Keyword }[] = [ { input: "만약", expected: keyword("만약") }, { input: "아니면", expected: keyword("아니면") }, + { input: "함수", expected: keyword("함수") }, ]; it.each(cases)("get group delimiter token '$input'", testLexing); }); + describe("separator", () => { + const cases: { input: string, expected: Separator }[] = [ + { input: ",", expected: separator(",") }, + ]; + + it.each(cases)("get separator token '$input'", testLexing); + }); + describe("illegal", () => { const cases: { input: string, expected: Illegal }[] = [ { input: "$", expected: illegal("$") }, @@ -194,6 +205,22 @@ describe("getToken()", () => { end, ] }, + { + input: "함수(사과, 바나나) { 사과 + 바나나 }", + expectedTokens:[ + keyword("함수"), + groupDelimiter("("), + identifier("사과"), + separator(","), + identifier("바나나"), + groupDelimiter(")"), + blockDelimiter("{"), + identifier("사과"), + operator("+"), + identifier("바나나"), + blockDelimiter("}"), + ], + }, ]; it.each(cases)("get tokens from input '$input'", ({ input, expectedTokens }) => { diff --git a/src/lexer/index.ts b/src/lexer/index.ts index 44544a6..ce1e10b 100644 --- a/src/lexer/index.ts +++ b/src/lexer/index.ts @@ -37,6 +37,12 @@ export default class Lexer { return Token.blockDelimiter(delimiter); } + case ",": + { + const separator = this.charBuffer.pop() as typeof char; + return Token.separator(separator); + } + case "!": { this.charBuffer.pop(); @@ -95,7 +101,7 @@ export default class Lexer { return Token.booleanLiteral(read); } - if (read === "만약" || read === "아니면") { + if (read === "만약" || read === "아니면" || read === "함수") { return Token.keyword(read); } diff --git a/src/lexer/token/index.test.ts b/src/lexer/token/index.test.ts index fb193ae..12f7f83 100644 --- a/src/lexer/token/index.test.ts +++ b/src/lexer/token/index.test.ts @@ -5,6 +5,7 @@ import { booleanLiteral, stringLiteral, groupDelimiter, + separator, keyword, } from "./"; import type { @@ -14,6 +15,7 @@ import type { BooleanLiteral, StringLiteral, GroupDelimiter, + Separator, Keyword, } from "./"; @@ -108,10 +110,23 @@ describe("group delimiter", () => { }); }); +describe("separator", () => { + const cases: { input: Separator["value"], expected: Separator }[] = [ + { input: ",", expected: separator(",") }, + ]; + + it.each(cases)("make separator token for '$input'", ({ input, expected }) => { + const token = separator(input); + + expect(token).toEqual(expected); + }); +}); + describe("keywords", () => { const cases: { input: Keyword["value"], expected: Keyword }[] = [ { input: "만약", expected: keyword("만약") }, { input: "아니면", expected: keyword("아니면") }, + { input: "함수", expected: keyword("함수") }, ]; it.each(cases)("make keyword token for '$input'", ({ input, expected }) => { diff --git a/src/lexer/token/index.ts b/src/lexer/token/index.ts index 9ed9277..d7a1b6b 100644 --- a/src/lexer/token/index.ts +++ b/src/lexer/token/index.ts @@ -6,6 +6,7 @@ export type TokenType = StringLiteral | GroupDelimiter | BlockDelimiter | + Separator | Keyword | Illegal | End; @@ -19,8 +20,10 @@ type LogicalOperatorValue = "!" | "!=" | "==" | ">" | "<" | ">=" | "<="; type BooleanLiteralValue = "참" | "거짓"; type GroupDelimiterValue = "(" | ")"; type BlockDelimiterValue = "{" | "}"; -type KeywordValue = BranchKeywordValue; +type SeparatorValue = ","; +type KeywordValue = BranchKeywordValue | FunctionKeywordValue; type BranchKeywordValue = "만약" | "아니면"; +type FunctionKeywordValue = "함수"; type EndValue = typeof END_VALUE; export interface Operator { @@ -58,6 +61,11 @@ export interface BlockDelimiter { value: BlockDelimiterValue; } +export interface Separator { + type: "separator", + value: SeparatorValue; +} + export interface Keyword { type: "keyword"; value: KeywordValue; @@ -108,6 +116,11 @@ export const blockDelimiter = (value: BlockDelimiter["value"]): BlockDelimiter = value, }); +export const separator = (value: Separator["value"]): Separator => ({ + type: "separator", + value, +}); + export const keyword = (value: Keyword["value"]): Keyword => ({ type: "keyword", value, diff --git a/src/parser/index.test.ts b/src/parser/index.test.ts index 127aa30..8fcb8df 100644 --- a/src/parser/index.test.ts +++ b/src/parser/index.test.ts @@ -749,6 +749,76 @@ describe("parseProgram()", () => { it.each(cases)("parse $name", testParsing); }); + describe("functions", () => { + const cases: { name: string, input: string, expected: Program }[] = [ + { + name: "function expression with parameters", + input: "함수 (사과, 바나나) { 사과 + 바나나 }", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + 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: "바나나" }, + }, + }, + ], + }, + }, + }, + ], + }, + }, + { + name: "function expression with no parameters", + input: "함수 () { 사과 + 바나나 }", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "function expression", + parameter: [], + body: { + type: "block", + statements: [ + { + type: "expression statement", + expression: { + type: "infix expression", + infix: "+", + left: { type: "identifier", value: "사과" }, + right: { type: "identifier", value: "바나나" }, + }, + }, + ], + }, + }, + }, + ], + }, + }, + ]; + + 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 1664714..511a4fc 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -10,6 +10,7 @@ import { makeExpressionStatement, makePrefixExpression, makeInfixExpression, + makeFunctionExpression, } from "./syntax-tree"; import type { Program, @@ -194,6 +195,13 @@ export default class Parser { const expression = makePrefixExpression(prefix, subExpression); return expression; } + if (token.type === "keyword" && token.value === "함수") { + const parameters = this.parseParameters(); + const body = this.parseBlock(); + + const functionExpression = makeFunctionExpression(body, parameters); + return functionExpression; + } if (token.type === "group delimiter" && token.value === "(") { const groupedExpression = this.parseExpression(bindingPower.lowest); @@ -209,6 +217,60 @@ export default class Parser { throw new Error(`bad token type ${token.type} (${token.value}) for prefix expression`); } + private parseParameters(): Identifier[] { + const parameters: Identifier[] = []; + + const maybeGroupStart = this.buffer.read(); + if (maybeGroupStart.type !== "group delimiter" || maybeGroupStart.value !== "(") { + throw new Error(`expected ( but received ${maybeGroupStart.type}`); + } + this.buffer.next(); + + // early return empty parameters if end of parameter list + const maybeIdentifierOrGroupEnd = this.buffer.read(); + this.buffer.next(); + if (maybeIdentifierOrGroupEnd.type === "group delimiter" && maybeIdentifierOrGroupEnd.value === ")") { + return []; + } + const maybeIdentifier = maybeIdentifierOrGroupEnd; + + // read first parameter + if (maybeIdentifier.type !== "identifier") { + throw new Error(`expected identifier but received ${maybeIdentifier}`); + } + const identifier = maybeIdentifier; + parameters.push(identifier); + + // read the rest parameters + while (true) { + const maybeCommaOrGroupEnd = this.buffer.read(); + this.buffer.next(); + + // break if end of parameter list + if (maybeCommaOrGroupEnd.type === "group delimiter" && maybeCommaOrGroupEnd.value === ")") { + break; + } + const maybeComma = maybeCommaOrGroupEnd; + + // read comma + if (maybeComma.type !== "separator") { + throw new Error(`expected comma but received ${maybeComma}`); + } + + // read next identifier + const maybeIdentifier = this.buffer.read(); + this.buffer.next(); + if (maybeIdentifier.type !== "identifier") { + throw new Error(`expected identifier but received ${maybeIdentifier}`); + } + const identifier = maybeIdentifier; + + parameters.push(identifier); + } + + return parameters; + } + private parseInfixExpression(left: Expression): Expression | null { let token = this.buffer.read(); if (token.type !== "operator") { diff --git a/src/parser/syntax-tree/expression/index.ts b/src/parser/syntax-tree/expression/index.ts index 7bd5d01..e73c6a3 100644 --- a/src/parser/syntax-tree/expression/index.ts +++ b/src/parser/syntax-tree/expression/index.ts @@ -1,3 +1,5 @@ +import { Block } from "../group"; + export type Expression = Identifier | NumberNode | @@ -5,6 +7,7 @@ export type Expression = StringNode | PrefixExpression | InfixExpression | + FunctionExpression | Assignment; export interface Identifier { @@ -40,6 +43,12 @@ export interface InfixExpression { right: Expression; } +export interface FunctionExpression { + type: "function expression"; + parameter: Identifier[], + body: Block; +} + export interface Assignment { type: "assignment"; left: Identifier; @@ -79,6 +88,12 @@ export const makeInfixExpression = (infix: InfixExpression["infix"], left: Infix right, }); +export const makeFunctionExpression = (body: FunctionExpression["body"], parameter: FunctionExpression["parameter"] = []): FunctionExpression => ({ + type: "function expression", + parameter, + body, +}); + export const makeAssignment = (left: Assignment["left"], right: Assignment["right"]): Assignment => ({ type: "assignment", left,