Skip to content

Commit

Permalink
support function expressions (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
wcho21 authored Dec 21, 2023
1 parent 47ffe0a commit b567083
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 2 deletions.
23 changes: 23 additions & 0 deletions src/evaluator/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
});
5 changes: 5 additions & 0 deletions src/evaluator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
27 changes: 27 additions & 0 deletions src/lexer/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
groupDelimiter,
blockDelimiter,
keyword,
separator,
illegal,
end,
} from "./token";
Expand All @@ -21,6 +22,7 @@ import type {
GroupDelimiter,
BlockDelimiter,
Keyword,
Separator,
Illegal,
End,
} from "./token";
Expand Down Expand Up @@ -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("$") },
Expand Down Expand Up @@ -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 }) => {
Expand Down
8 changes: 7 additions & 1 deletion src/lexer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -95,7 +101,7 @@ export default class Lexer {
return Token.booleanLiteral(read);
}

if (read === "만약" || read === "아니면") {
if (read === "만약" || read === "아니면" || read === "함수") {
return Token.keyword(read);
}

Expand Down
15 changes: 15 additions & 0 deletions src/lexer/token/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
booleanLiteral,
stringLiteral,
groupDelimiter,
separator,
keyword,
} from "./";
import type {
Expand All @@ -14,6 +15,7 @@ import type {
BooleanLiteral,
StringLiteral,
GroupDelimiter,
Separator,
Keyword,
} from "./";

Expand Down Expand Up @@ -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 }) => {
Expand Down
15 changes: 14 additions & 1 deletion src/lexer/token/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type TokenType =
StringLiteral |
GroupDelimiter |
BlockDelimiter |
Separator |
Keyword |
Illegal |
End;
Expand All @@ -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 {
Expand Down Expand Up @@ -58,6 +61,11 @@ export interface BlockDelimiter {
value: BlockDelimiterValue;
}

export interface Separator {
type: "separator",
value: SeparatorValue;
}

export interface Keyword {
type: "keyword";
value: KeywordValue;
Expand Down Expand Up @@ -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,
Expand Down
70 changes: 70 additions & 0 deletions src/parser/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }[] = [
{
Expand Down
62 changes: 62 additions & 0 deletions src/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
makeExpressionStatement,
makePrefixExpression,
makeInfixExpression,
makeFunctionExpression,
} from "./syntax-tree";
import type {
Program,
Expand Down Expand Up @@ -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);

Expand All @@ -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") {
Expand Down
Loading

0 comments on commit b567083

Please sign in to comment.