From d0358385d32d71064c6dc3e11124ad1720c7e2ff Mon Sep 17 00:00:00 2001 From: Won-hee Cho <89635107+wcho21@users.noreply.github.com> Date: Tue, 23 Jan 2024 06:40:27 +0900 Subject: [PATCH 1/7] lex characters with position context (#47) --- .../char-buffer/char-reader/index.test.ts | 113 +++++ src/lexer/char-buffer/char-reader/index.ts | 126 +++++ .../char-reader/source-char/index.ts | 6 + src/lexer/char-buffer/index.test.ts | 56 ++- src/lexer/char-buffer/index.ts | 23 +- src/lexer/char-buffer/reader/index.test.ts | 40 -- src/lexer/char-buffer/reader/index.ts | 33 -- src/lexer/index.test.ts | 471 ++++++++++++++++++ src/lexer/index.ts | 365 +++++++++++++- src/lexer/source-token/base/index.ts | 8 + src/lexer/source-token/delimiter/index.ts | 45 ++ src/lexer/source-token/identifier/index.ts | 33 ++ src/lexer/source-token/index.ts | 29 ++ src/lexer/source-token/literal/index.ts | 43 ++ src/lexer/source-token/operator/index.ts | 20 + src/lexer/source-token/special/index.ts | 44 ++ src/lexer/token/index.ts | 10 + src/lexer/util/index.ts | 4 +- src/util/position/index.ts | 4 + 19 files changed, 1367 insertions(+), 106 deletions(-) create mode 100644 src/lexer/char-buffer/char-reader/index.test.ts create mode 100644 src/lexer/char-buffer/char-reader/index.ts create mode 100644 src/lexer/char-buffer/char-reader/source-char/index.ts delete mode 100644 src/lexer/char-buffer/reader/index.test.ts delete mode 100644 src/lexer/char-buffer/reader/index.ts create mode 100644 src/lexer/source-token/base/index.ts create mode 100644 src/lexer/source-token/delimiter/index.ts create mode 100644 src/lexer/source-token/identifier/index.ts create mode 100644 src/lexer/source-token/index.ts create mode 100644 src/lexer/source-token/literal/index.ts create mode 100644 src/lexer/source-token/operator/index.ts create mode 100644 src/lexer/source-token/special/index.ts create mode 100644 src/util/position/index.ts diff --git a/src/lexer/char-buffer/char-reader/index.test.ts b/src/lexer/char-buffer/char-reader/index.test.ts new file mode 100644 index 0000000..da97225 --- /dev/null +++ b/src/lexer/char-buffer/char-reader/index.test.ts @@ -0,0 +1,113 @@ +import Reader from "./"; + +describe("readSource()", () => { + it("read a character", () => { + const reader = new Reader("a", "\0"); + const expected = { value: "a", position: { row: 0, col: 0 }}; + + const char = reader.readChar(); + + expect(char).toEqual(expected); + }); + + it("read the same character twice if not advanced", () => { + const reader = new Reader("a", "\0"); + const expected = { value: "a", position: { row: 0, col: 0 }}; + + const char1 = reader.readChar(); + const char2 = reader.readChar(); + + expect(char1).toEqual(expected); + expect(char2).toEqual(expected); + }); + + it("read the fallback character if end of input", () => { + const reader = new Reader("", "\0"); + const expected = { value: "\0", position: { row: 0, col: 0 }}; + + const char1 = reader.readChar(); + const char2 = reader.readChar(); + + expect(char1).toEqual(expected); + expect(char2).toEqual(expected); + }); +}); + +describe("advance()", () => { + it("advance to next character", () => { + const reader = new Reader("ab", "\0"); + const expected = { value: "b", position: { row: 0, col: 1 }}; + + reader.advance(); + const char = reader.readChar(); + + expect(char).toEqual(expected); + }); + + it("advance to next line if new line (LF)", () => { + const reader = new Reader("a\nb", "\0"); + const expected1 = { value: "\n", position: { row: 0, col: 1 }}; + const expected2 = { value: "b", position: { row: 1, col: 0 }}; + + reader.advance(); + const char1 = reader.readChar(); + expect(char1).toEqual(expected1); + + reader.advance(); + const char2 = reader.readChar(); + expect(char2).toEqual(expected2); + }); + + it("advance to next line if new line (CR LF)", () => { + const reader = new Reader("a\r\nb", "\0"); + const expected1 = { value: "\r\n", position: { row: 0, col: 1 }}; + const expected2 = { value: "b", position: { row: 1, col: 0 }}; + + reader.advance(); + const char1 = reader.readChar(); + expect(char1).toEqual(expected1); + + reader.advance(); + const char2 = reader.readChar(); + expect(char2).toEqual(expected2); + }); +}); + +describe("read()", () => { + it("read a character", () => { + const reader = new Reader("a", "\0"); + + expect(reader.read()).toBe("a"); + }); + + it("read the same character twice", () => { + const reader = new Reader("a", "\0"); + + expect(reader.read()).toBe("a"); + expect(reader.read()).toBe("a"); + }); + + it("read fallback character if end of input", () => { + const reader = new Reader("", "\0"); + + expect(reader.read()).toBe("\0"); + expect(reader.read()).toBe("\0"); + }); +}); + +describe("next()", () => { + it("increment index and read next character", () => { + const reader = new Reader("ab", "\0"); + + reader.next(); + expect(reader.read()).toBe("b"); + }); + + it("not increment index if end of input", () => { + const reader = new Reader("a", "\0"); + + reader.next(); + reader.next(); + expect(reader.read()).toBe("\0"); + }); +}); diff --git a/src/lexer/char-buffer/char-reader/index.ts b/src/lexer/char-buffer/char-reader/index.ts new file mode 100644 index 0000000..f7a3f6c --- /dev/null +++ b/src/lexer/char-buffer/char-reader/index.ts @@ -0,0 +1,126 @@ +import type SourceChar from "./source-char"; + +export default class CharReader { + private readonly chars: string; + private readonly fallbackChar: string; + private index: number; // next index to read + private row: number; // current row + private col: number; // current col + + /** + * @params chars - Characters to initialize + * @params fallbackChar - A character to return on underflow + */ + constructor(chars: string, fallbackChar: string) { + this.chars = chars; + this.fallbackChar = fallbackChar; + + this.index = 0; // first character to read is at index 0 + this.row = 0; + this.col = 0; + } + + private peekNewLine(): string | null { + // return if end of input + if (this.index === this.chars.length) { + return null; + } + + // return if no new line + const char = this.chars[this.index]; + if (char !== "\r" && char !== "\n") { + return null; + } + + // return if the last character + if (this.index+1 === this.chars.length) { + return char; + } + + // return if single-character new line + const nextChar = this.chars[this.index+1]; + if (nextChar !== "\r" && nextChar !== "\n") { + return char; + } + + // return two-character new line + return char + nextChar; + } + + readChar(): SourceChar { + // return fallback character if end of input + if (this.index === this.chars.length) { + const fallbackChar: SourceChar = { + value: this.fallbackChar, + position: { + row: this.row, + col: this.col, + }, + }; + return fallbackChar; + } + + // return new line character(s) if any + const newLine = this.peekNewLine(); + if (newLine !== null) { + const newLineChar = { + value: newLine, + position: { + row: this.row, + col: this.col, + }, + }; + return newLineChar; + } + + // return single character + const char = this.chars[this.index]; + const sourceChar: SourceChar = { + value: char, + position: { + row: this.row, + col: this.col, + }, + }; + return sourceChar; + } + + advance(): void { + if (this.index === this.chars.length) { + return; + } + + // advance position past new line character(s) if any + const newLine = this.peekNewLine(); + if (newLine !== null) { + this.index += newLine.length; + ++this.row; + this.col = 0; + return; + } + + // advance + ++this.index; + ++this.col; + } + + /** @deprecated Returns current character; if end of input, return fallback character */ + read(): string { + if (this.index === this.chars.length) { + return this.fallbackChar; + } + + return this.chars[this.index]; + } + + /** @deprecated Increment index to get next character with get() */ + next(): void { + if (this.index === this.chars.length) { + return; + } + + this.index++; + } +} + +export type { SourceChar }; diff --git a/src/lexer/char-buffer/char-reader/source-char/index.ts b/src/lexer/char-buffer/char-reader/source-char/index.ts new file mode 100644 index 0000000..3577751 --- /dev/null +++ b/src/lexer/char-buffer/char-reader/source-char/index.ts @@ -0,0 +1,6 @@ +import Position from "../../../../util/position"; + +export default interface SourceChar { + value: string, + position: Position, +}; diff --git a/src/lexer/char-buffer/index.test.ts b/src/lexer/char-buffer/index.test.ts index 7a85e49..b9903c9 100644 --- a/src/lexer/char-buffer/index.test.ts +++ b/src/lexer/char-buffer/index.test.ts @@ -1,21 +1,61 @@ -import Buffer from "./"; +import CharBuffer from "./"; + +describe("popChar()", () => { + it("pop characters", () => { + const buffer = new CharBuffer("ab"); + const expected1 = { value: "a", position: { row: 0, col: 0 }}; + const expected2 = { value: "b", position: { row: 0, col: 1 }}; + + expect(buffer.popChar()).toEqual(expected1); + expect(buffer.popChar()).toEqual(expected2); + }); + + it("pop null characters if nothing to pop", () => { + const buffer = new CharBuffer(""); + const expected = { value: "\0", position: { row: 0, col: 0 }}; + + // pop the same null character more than once + expect(buffer.popChar()).toEqual(expected); + expect(buffer.popChar()).toEqual(expected); + }); +}); + +describe("peekChar()", () => { + it("peek character", () => { + const buffer = new CharBuffer("a"); + const expected = { value: "a", position: { row: 0, col: 0 }}; + + // peek the same character if not popped + expect(buffer.peekChar()).toEqual(expected); + expect(buffer.peekChar()).toEqual(expected); + }); + + it("peek null characters if nothing to pop", () => { + const buffer = new CharBuffer(""); + const expected = { value: "\0", position: { row: 0, col: 0 }}; + + // peek the same null character more than once + expect(buffer.peekChar()).toEqual(expected); + expect(buffer.peekChar()).toEqual(expected); + }); +}); describe("pop()", () => { it("pop characters", () => { - const buffer = new Buffer("ab"); + const buffer = new CharBuffer("ab"); expect(buffer.pop()).toBe("a"); expect(buffer.pop()).toBe("b"); }); it("pop null character if nothing to pop", () => { - const buffer = new Buffer(""); + const buffer = new CharBuffer(""); expect(buffer.pop()).toBe("\0"); }); it("pop null character more than once if nothing to pop", () => { - const buffer = new Buffer(""); + const buffer = new CharBuffer(""); expect(buffer.pop()).toBe("\0"); expect(buffer.pop()).toBe("\0"); @@ -24,26 +64,26 @@ describe("pop()", () => { describe("peek()", () => { it("peek character", () => { - const buffer = new Buffer("a"); + const buffer = new CharBuffer("a"); expect(buffer.peek()).toBe("a"); }); it("peek the same character twice", () => { - const buffer = new Buffer("a"); + const buffer = new CharBuffer("a"); expect(buffer.peek()).toBe("a"); expect(buffer.peek()).toBe("a"); }); it("peek null character if nothing to pop", () => { - const buffer = new Buffer(""); + const buffer = new CharBuffer(""); expect(buffer.peek()).toBe("\0"); }); it("peek null character more than once if nothing to pop", () => { - const buffer = new Buffer(""); + const buffer = new CharBuffer(""); expect(buffer.peek()).toBe("\0"); expect(buffer.peek()).toBe("\0"); diff --git a/src/lexer/char-buffer/index.ts b/src/lexer/char-buffer/index.ts index f6a32c7..11e9dcd 100644 --- a/src/lexer/char-buffer/index.ts +++ b/src/lexer/char-buffer/index.ts @@ -1,14 +1,28 @@ -import Reader from "./reader"; +import CharReader, { type SourceChar } from "./char-reader"; export default class CharBuffer { static readonly END_OF_INPUT = "\0"; - private readonly reader: Reader; + private readonly reader: CharReader; constructor(input: string) { - this.reader = new Reader(input, CharBuffer.END_OF_INPUT); + this.reader = new CharReader(input, CharBuffer.END_OF_INPUT); } + popChar(): SourceChar { + const char = this.reader.readChar(); + this.reader.advance(); + + return char; + } + + peekChar(): SourceChar { + const char = this.reader.readChar(); + + return char; + } + + /** @deprecated */ pop(): string { const char = this.reader.read(); this.reader.next(); @@ -16,9 +30,12 @@ export default class CharBuffer { return char; } + /** @deprecated */ peek(): string { const char = this.reader.read(); return char; } } + +export type { SourceChar } from "./char-reader"; diff --git a/src/lexer/char-buffer/reader/index.test.ts b/src/lexer/char-buffer/reader/index.test.ts deleted file mode 100644 index c53cc98..0000000 --- a/src/lexer/char-buffer/reader/index.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Reader from "./"; - -describe("read()", () => { - it("read a character", () => { - const reader = new Reader("a", "\0"); - - expect(reader.read()).toBe("a"); - }); - - it("read the same character twice", () => { - const reader = new Reader("a", "\0"); - - expect(reader.read()).toBe("a"); - expect(reader.read()).toBe("a"); - }); - - it("read fallback character if end of input", () => { - const reader = new Reader("", "\0"); - - expect(reader.read()).toBe("\0"); - expect(reader.read()).toBe("\0"); - }); -}); - -describe("next()", () => { - it("increment index and read next character", () => { - const reader = new Reader("ab", "\0"); - - reader.next(); - expect(reader.read()).toBe("b"); - }); - - it("not increment index if end of input", () => { - const reader = new Reader("a", "\0"); - - reader.next(); - reader.next(); - expect(reader.read()).toBe("\0"); - }); -}); diff --git a/src/lexer/char-buffer/reader/index.ts b/src/lexer/char-buffer/reader/index.ts deleted file mode 100644 index 66d6bd1..0000000 --- a/src/lexer/char-buffer/reader/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -export default class Reader { - private readonly chars: string; - private readonly fallbackChar: string; - private index: number; - - /** - * @params chars - Characters to initialize - * @params fallbackChar - A character to return on underflow - */ - constructor(chars: string, fallbackChar: string) { - this.chars = chars; - this.fallbackChar = fallbackChar; - this.index = 0; // first character to read is at index 0 - } - - /** Returns current character; if end of input, return fallback character */ - read(): string { - if (this.index === this.chars.length) { - return this.fallbackChar; - } - - return this.chars[this.index]; - } - - /** Increment index to get next character with get() */ - next(): void { - if (this.index === this.chars.length) { - return; - } - - this.index++; - } -} diff --git a/src/lexer/index.test.ts b/src/lexer/index.test.ts index 27f0597..ac451d7 100644 --- a/src/lexer/index.test.ts +++ b/src/lexer/index.test.ts @@ -27,6 +27,477 @@ import type { End, } from "./token"; +import type { + SourceToken, +} from "./source-token"; + +describe("getSourceToken()", () => { + describe("single token", () => { + const testLex = ({ input, expected }: { input: string, expected: SourceToken }) => { + const lexer = new Lexer(input); + + const token = lexer.getSourceToken(); + + expect(token).toEqual(expected); + }; + + describe("operators", () => { + const cases: { input: string, expected: SourceToken }[] = [ + { + input: "+", + expected: { + type: "operator", + value: "+", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 0 }, + }, + }, + { + input: "-", + expected: { + type: "operator", + value: "-", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 0 }, + }, + }, + { + input: "*", + expected: { + type: "operator", + value: "*", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 0 }, + }, + }, + { + input: "/", + expected: { + type: "operator", + value: "/", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 0 }, + }, + }, + { + input: "=", + expected: { + type: "operator", + value: "=", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 0 }, + }, + }, + { + input: "==", + expected: { + type: "operator", + value: "==", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 1 }, + }, + }, + { + input: "!", + expected: { + type: "operator", + value: "!", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 0 }, + }, + }, + { + input: "!=", + expected: { + type: "operator", + value: "!=", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 1 }, + }, + }, + { + input: ">", + expected: { + type: "operator", + value: ">", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 0 }, + }, + }, + { + input: ">=", + expected: { + type: "operator", + value: ">=", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 1 }, + }, + }, + { + input: "<", + expected: { + type: "operator", + value: "<", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 0 }, + }, + }, + { + input: "<=", + expected: { + type: "operator", + value: "<=", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 1 }, + }, + }, + ]; + + it.each(cases)("lex operator '$input'", testLex); + }); + + describe("delimiters", () => { + const cases: { input: string, expected: SourceToken }[] = [ + { + input: "(", + expected: { + type: "group delimiter", + value: "(", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 0 }, + }, + }, + { + input: ")", + expected: { + type: "group delimiter", + value: ")", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 0 }, + }, + }, + { + input: "{", + expected: { + type: "block delimiter", + value: "{", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 0 }, + }, + }, + { + input: "}", + expected: { + type: "block delimiter", + value: "}", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 0 }, + }, + }, + { + input: ",", + expected: { + type: "separator", + value: ",", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 0 }, + }, + }, + ]; + + it.each(cases)("lex delimiter '$input'", testLex); + }); + + describe("number literals", () => { + const cases: { input: string, expected: SourceToken }[] = [ + { + input: "0", + expected: { + type: "number literal", + value: "0", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 0 }, + }, + }, + { + input: "1", + expected: { + type: "number literal", + value: "1", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 0 }, + }, + }, + { + input: "1234", + expected: { + type: "number literal", + value: "1234", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 3 }, + }, + }, + { + input: "12.34", + expected: { + type: "number literal", + value: "12.34", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 4 }, + }, + }, + ]; + + it.each(cases)("lex number literal '$input'", testLex); + }); + + describe("boolean literals", () => { + const cases: { input: string, expected: SourceToken }[] = [ + { + input: "참", + expected: { + type: "boolean literal", + value: "참", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 0 }, + }, + }, + { + input: "거짓", + expected: { + type: "boolean literal", + value: "거짓", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 1 }, + }, + }, + ]; + + it.each(cases)("lex boolean literal '$input'", testLex); + }); + + describe("string literals", () => { + const cases: { input: string, expected: SourceToken }[] = [ + { + input: "'foo bar 123 !@# 참'", + expected: { + type: "string literal", + value: "foo bar 123 !@# 참", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 18 }, + }, + }, + { + input: "''", + expected: { + type: "string literal", + value: "", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 1 }, + }, + }, + ]; + + it.each(cases)("lex string literal '$input'", testLex); + }); + + describe("keywords", () => { + const cases: { input: string, expected: SourceToken }[] = [ + { + input: "만약", + expected: { + type: "keyword", + value: "만약", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 1 }, + }, + }, + { + input: "아니면", + expected: { + type: "keyword", + value: "아니면", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 2 }, + }, + }, + { + input: "함수", + expected: { + type: "keyword", + value: "함수", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 1 }, + }, + }, + { + input: "결과", + expected: { + type: "keyword", + value: "결과", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 1 }, + }, + }, + ]; + + it.each(cases)("lex keyword '$input'", testLex); + }); + + describe("identifiers", () => { + const cases: { input: string, expected: SourceToken }[] = [ + { + input: "Foo이름123_", + expected: { + type: "identifier", + value: "Foo이름123_", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 8 }, + }, + }, + { + input: "이름Foo123_", + expected: { + type: "identifier", + value: "이름Foo123_", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 8 }, + }, + }, + { + input: "_이름Foo123", + expected: { + type: "identifier", + value: "_이름Foo123", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 8 }, + }, + }, + ]; + + it.each(cases)("lex identifier '$input'", testLex); + }); + + describe("special", () => { + const cases: { input: string, expected: SourceToken }[] = [ + { + input: "$", + expected: { + type: "illegal", + value: "$", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 0 }, + }, + }, + { + input: "'foo", + expected: { + type: "illegal string", + value: "foo", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 4 }, + }, + }, + { + input: "", + expected: { + type: "end", + value: "$end", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 0 }, + }, + }, + ]; + + it.each(cases)("lex special '$input'", testLex); + }); + }); + + describe("multiple tokens", () => { + const cases: { input: string, expectedTokens: SourceToken[] }[] = [ + { + input: "12 + 34", + expectedTokens: [ + { + type: "number literal", + value: "12", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 1 }, + }, + { + type: "operator", + value: "+", + posBegin: { row: 0, col: 3 }, + posEnd: { row: 0, col: 3 }, + }, + { + type: "number literal", + value: "34", + posBegin: { row: 0, col: 5 }, + posEnd: { row: 0, col: 6 }, + }, + { + type: "end", + value: "$end", + posBegin: { row: 0, col: 7 }, + posEnd: { row: 0, col: 7 }, + }, + ] + }, + { + input: "만약 참 {\n 12\r\n}", + expectedTokens: [ + { + type: "keyword", + value: "만약", + posBegin: { row: 0, col: 0 }, + posEnd: { row: 0, col: 1 }, + }, + { + type: "boolean literal", + value: "참", + posBegin: { row: 0, col: 3 }, + posEnd: { row: 0, col: 3 }, + }, + { + type: "block delimiter", + value: "{", + posBegin: { row: 0, col: 5 }, + posEnd: { row: 0, col: 5 }, + }, + { + type: "number literal", + value: "12", + posBegin: { row: 1, col: 2 }, + posEnd: { row: 1, col: 3 }, + }, + { + type: "block delimiter", + value: "}", + posBegin: { row: 2, col: 0 }, + posEnd: { row: 2, col: 0 }, + }, + { + type: "end", + value: "$end", + posBegin: { row: 2, col: 1 }, + posEnd: { row: 2, col: 1 }, + }, + ] + }, + ]; + + it.each(cases)("get tokens from input '$input'", ({ input, expectedTokens }) => { + const lexer = new Lexer(input); + + for (const expected of expectedTokens) { + const token = lexer.getSourceToken(); + expect(token).toEqual(expected); + } + }); + }); +}); + describe("getToken()", () => { describe("single token", () => { const testLexing = ({ input, expected }: { input: string, expected: TokenType }) => { diff --git a/src/lexer/index.ts b/src/lexer/index.ts index c464bbc..a60f30a 100644 --- a/src/lexer/index.ts +++ b/src/lexer/index.ts @@ -1,7 +1,31 @@ -import CharBuffer from "./char-buffer"; +import CharBuffer, { type SourceChar } from "./char-buffer"; import * as Token from "./token"; import * as Util from "./util"; +import { + createOperatorToken, + createGroupDelimiterToken, + createBlockDelimiterToken, + createSeparatorToken, + createIllegalToken, + createIllegalStringLiteralToken, + createNumberLiteralToken, + createBooleanLiteralToken, + createStringLiteralToken, + createKeywordToken, + createIdentifierToken, + createEndToken, +} from "./source-token"; +import type { + SourceToken, + OperatorToken, + NumberLiteralToken, + StringLiteralToken, + IllegalStringLiteralToken, +} from "./source-token"; +import type Position from "../util/position"; +import { isDigit, isLetter, isWhitespace } from "./util"; + export default class Lexer { private readonly charBuffer: CharBuffer; @@ -9,6 +33,303 @@ export default class Lexer { this.charBuffer = new CharBuffer(input); } + getSourceToken(): SourceToken { + this.skipWhitespaceChars(); + + const char = this.charBuffer.peekChar(); + switch (char.value) { + case "+": + case "-": + case "*": + case "/": + { + const { position } = this.charBuffer.popChar(); + const value = char.value; + + const token = createOperatorToken(value, position, position); + return token; + } + + case "(": + case ")": + { + const { position } = this.charBuffer.popChar(); + const value = char.value; + + const token = createGroupDelimiterToken(value, position, position); + return token; + } + + case "{": + case "}": + { + const { position } = this.charBuffer.popChar(); + const value = char.value; + + const token = createBlockDelimiterToken(value, position, position); + return token; + } + + case ",": + { + const { position } = this.charBuffer.popChar(); + const value = char.value; + + const token = createSeparatorToken(value, position, position); + return token; + } + + case "!": + { + const { position: pos } = this.charBuffer.popChar(); + + const token = this.lexCharsStartingWithBang(pos); + return token; + } + + case "=": + { + const { position: pos } = this.charBuffer.popChar(); + + const token = this.lexCharsStartingWithEqual(pos); + return token; + } + + case ">": + { + const { position: pos } = this.charBuffer.popChar(); + + const token = this.lexCharsStartingWithGreaterThan(pos); + return token; + } + + case "<": + { + const { position: pos } = this.charBuffer.popChar(); + + const token = this.lexCharsStartingWithLessThan(pos); + return token; + } + + case "'": + { + const { position: pos } = this.charBuffer.popChar(); + + const token = this.lexCharsStartingWithSingleQuote(pos); + return token; + } + + case CharBuffer.END_OF_INPUT: + { + const { position: pos } = this.charBuffer.popChar(); + + // TODO + const token = createEndToken(pos, pos); + return token; + } + + default: + { + if (isDigit(char.value)) { + const token = this.lexNumberLiteral(); + return token; + } + + if (isLetter(char.value)) { + const token = this.lexLetters(); + return token; + } + + const { position } = this.charBuffer.popChar(); + const token = createIllegalToken(char.value, position, position); + return token; + } + } + } + + private skipWhitespaceChars(): void { + while (true) { + const char = this.charBuffer.peekChar(); + if (!isWhitespace(char.value)) { + break; + } + + this.charBuffer.popChar(); + } + } + + /** assumes the bang character popped */ + private lexCharsStartingWithBang(bangPos: Position): OperatorToken { + const peek = this.charBuffer.peekChar(); + switch (peek.value) { + case "=": + { + const { position: posEnd } = this.charBuffer.popChar(); + return createOperatorToken("!=", bangPos, posEnd); + } + + default: + return createOperatorToken("!", bangPos, bangPos); + } + } + + /** assumes the equal character popped */ + private lexCharsStartingWithEqual(equalPos: Position): OperatorToken { + const peek = this.charBuffer.peekChar(); + switch (peek.value) { + case "=": + { + const { position: posEnd } = this.charBuffer.popChar(); + return createOperatorToken("==", equalPos, posEnd); + } + + default: + return createOperatorToken("=", equalPos, equalPos); + } + } + + /** assume the greater-than character popped */ + private lexCharsStartingWithGreaterThan(greaterThanPos: Position): OperatorToken { + const peek = this.charBuffer.peekChar(); + switch (peek.value) { + case "=": + { + const { position: posEnd } = this.charBuffer.popChar(); + return createOperatorToken(">=", greaterThanPos, posEnd); + } + + default: + return createOperatorToken(">", greaterThanPos, greaterThanPos); + } + } + + /** assume the less-than character popped */ + private lexCharsStartingWithLessThan(lessThanPos: Position): OperatorToken { + const peek = this.charBuffer.peekChar(); + switch (peek.value) { + case "=": + { + const { position: posEnd } = this.charBuffer.popChar(); + return createOperatorToken("<=", lessThanPos, posEnd); + } + + default: + return createOperatorToken("<", lessThanPos, lessThanPos); + } + } + + /** assume the single quote character popped */ + private lexCharsStartingWithSingleQuote(quotePos: Position): StringLiteralToken | IllegalStringLiteralToken { + const chars: SourceChar[] = []; + + while (true) { + const char = this.charBuffer.popChar(); + + const value = chars.map(char => char.value).join(""); + const posBegin = quotePos; + const posEnd = char.position; + + if (char.value === "'") { + return createStringLiteralToken(value, posBegin, posEnd); + } + + if (char.value === CharBuffer.END_OF_INPUT) { + return createIllegalStringLiteralToken(value, posBegin, posEnd); + } + + chars.push(char); + } + } + + private lexNumberLiteral(): NumberLiteralToken { + const wholeNumberPart = this.readDigitChars(); + const decimalPart = this.readDecimalChars(); + const numberChars = wholeNumberPart.concat(decimalPart); + + const value = numberChars.map(char => char.value).join(""); + const posBegin = numberChars[0].position; + const posEnd = numberChars[numberChars.length-1].position; + + const token = createNumberLiteralToken(value, posBegin, posEnd); + return token; + } + + private lexLetters(): any { + const letterChars = this.readLetterChars(); + + const value = letterChars.map(char => char.value).join(""); + const posBegin = letterChars[0].position; + const posEnd = letterChars[letterChars.length-1].position; + + // order is important; match keywords first, then identifier + switch (value) { + case "참": + case "거짓": + { + const token = createBooleanLiteralToken(value, posBegin, posEnd); + return token; + } + + case "만약": + case "아니면": + case "함수": + case "결과": + { + const token = createKeywordToken(value, posBegin, posEnd); + return token; + } + + default: + { + const token = createIdentifierToken(value, posBegin, posEnd); + return token; + } + } + } + + private readDigitChars(): SourceChar[] { + const chars: SourceChar[] = []; + while (true) { + const peek = this.charBuffer.peekChar(); + if (!isDigit(peek.value)) { + break; + } + + chars.push(this.charBuffer.popChar()); + } + + return chars; + } + + private readDecimalChars(): SourceChar[] { + // read decimal point; if not, early return + const maybeDot = this.charBuffer.peekChar(); + if (maybeDot.value !== ".") { + return []; + } + const dot = this.charBuffer.popChar(); + + // read and return decimal part + const digits = this.readDigitChars(); + const decimalChars = [dot].concat(digits); + return decimalChars; + } + + private readLetterChars(): SourceChar[] { + const chars: SourceChar[] = []; + while (true) { + const peek = this.charBuffer.peekChar(); + if (!isLetter(peek.value) && !isDigit(peek.value)) { + break; + } + + chars.push(this.charBuffer.popChar()); + } + + return chars; + } + + /** @deprecated */ getToken(): Token.TokenType { this.skipWhitespaces(); @@ -18,33 +339,33 @@ export default class Lexer { case "-": case "*": case "/": - { + { const operator = this.charBuffer.pop() as typeof char; return Token.operator(operator); } case "(": case ")": - { + { const delimiter = this.charBuffer.pop() as typeof char; return Token.groupDelimiter(delimiter); } case "{": case "}": - { + { const delimiter = this.charBuffer.pop() as typeof char; return Token.blockDelimiter(delimiter); } case ",": - { + { const separator = this.charBuffer.pop() as typeof char; return Token.separator(separator); } case "!": - { + { this.charBuffer.pop(); const operator = this.readOperatorStartingWithBang(); @@ -52,7 +373,7 @@ export default class Lexer { } case "=": - { + { this.charBuffer.pop(); const operator: "=" | "==" = this.readOperatorStartingWithEqual(); @@ -60,7 +381,7 @@ export default class Lexer { } case ">": - { + { this.charBuffer.pop(); const operator: ">" | ">=" = this.readOperatorStartingWithGreaterThan(); @@ -68,7 +389,7 @@ export default class Lexer { } case "<": - { + { this.charBuffer.pop(); const operator: "<" | "<=" = this.readOperatorStartingWithLessThan(); @@ -76,7 +397,7 @@ export default class Lexer { } case "'": - { + { this.charBuffer.pop(); const [str, ok] = this.readStringLiteral(); @@ -103,9 +424,9 @@ export default class Lexer { if ( read === "만약" || - read === "아니면" || - read === "함수" || - read === "결과" + read === "아니면" || + read === "함수" || + read === "결과" ) { return Token.keyword(read); } @@ -118,13 +439,14 @@ export default class Lexer { } } + /** @deprecated */ private skipWhitespaces(): void { while (Util.isWhitespace(this.charBuffer.peek())) { this.charBuffer.pop(); } } - /** assume the bang character popped */ + /** @deprecated assume the bang character popped */ private readOperatorStartingWithBang(): "!" | "!=" { switch (this.charBuffer.peek()) { case "=": @@ -136,7 +458,7 @@ export default class Lexer { } } - /** assume the equal character popped */ + /** @deprecated assume the equal character popped */ private readOperatorStartingWithEqual(): "=" | "==" { switch (this.charBuffer.peek()) { case "=": @@ -148,7 +470,7 @@ export default class Lexer { } } - /** assume the greater-than character popped */ + /** @deprecated assume the greater-than character popped */ private readOperatorStartingWithGreaterThan(): ">" | ">=" { switch (this.charBuffer.peek()) { case "=": @@ -160,7 +482,7 @@ export default class Lexer { } } - /** assume the less-than character popped */ + /** @deprecated assume the less-than character popped */ private readOperatorStartingWithLessThan(): "<" | "<=" { switch (this.charBuffer.peek()) { case "=": @@ -172,7 +494,7 @@ export default class Lexer { } } - /** return [string-literal, true] if ok; otherwise [string-read-so-far, false] */ + /** @deprecated return [string-literal, true] if ok; otherwise [string-read-so-far, false] */ private readStringLiteral(): [string, boolean] { const read: string[] = []; @@ -191,6 +513,7 @@ export default class Lexer { } } + /** @deprecated */ private readNumberLiteral(): string { const wholeNumberPart = this.readDigits(); const decimalPart = this.readDecimalPart(); @@ -200,6 +523,7 @@ export default class Lexer { return number; } + /** @deprecated */ private readDigits(): string { const read: string[] = []; while (Util.isDigit(this.charBuffer.peek())) { @@ -210,7 +534,7 @@ export default class Lexer { return digits; } - /** helper function for readNumberLiteral() method */ + /** @deprecated helper function for readNumberLiteral() method */ private readDecimalPart(): string { // read decimal point; if not, early return const maybeDecimalPoint = this.charBuffer.peek(); @@ -225,11 +549,12 @@ export default class Lexer { return decimalPart; } + /** @deprecated */ private readLettersAndDigits(): string { const read = []; while ( Util.isLetter(this.charBuffer.peek()) || - Util.isDigit(this.charBuffer.peek()) + Util.isDigit(this.charBuffer.peek()) ) { read.push(this.charBuffer.pop()); } diff --git a/src/lexer/source-token/base/index.ts b/src/lexer/source-token/base/index.ts new file mode 100644 index 0000000..445461e --- /dev/null +++ b/src/lexer/source-token/base/index.ts @@ -0,0 +1,8 @@ +import type Position from "../../../util/position"; + +export interface SourceTokenBase { + type: string, + value: string, + posBegin: Position, + posEnd: Position, +}; diff --git a/src/lexer/source-token/delimiter/index.ts b/src/lexer/source-token/delimiter/index.ts new file mode 100644 index 0000000..54e23e3 --- /dev/null +++ b/src/lexer/source-token/delimiter/index.ts @@ -0,0 +1,45 @@ +import type Position from "../../../util/position"; +import type { SourceTokenBase } from "./../base"; + +export type GroupDelimiterValue = "(" | ")"; +export type BlockDelimiterValue = "{" | "}"; +export type SeparatorValue = ","; + +export interface GroupDelimiterToken extends SourceTokenBase { + type: "group delimiter"; + value: GroupDelimiterValue; +} + +export interface BlockDelimiterToken extends SourceTokenBase { + type: "block delimiter"; + value: BlockDelimiterValue; +} + +export interface SeparatorToken extends SourceTokenBase { + type: "separator", + value: SeparatorValue; +} + +type CreateGroupDelimiterToken = (value: GroupDelimiterValue, posBegin: Position, posEnd: Position) => GroupDelimiterToken; +export const createGroupDelimiterToken: CreateGroupDelimiterToken = (value, posBegin, posEnd) => ({ + type: "group delimiter", + value, + posBegin, + posEnd, +}); + +type CreateBlockDelimiterToken = (value: BlockDelimiterValue, posBegin: Position, posEnd: Position) => BlockDelimiterToken; +export const createBlockDelimiterToken: CreateBlockDelimiterToken = (value, posBegin, posEnd) => ({ + type: "block delimiter", + value, + posBegin, + posEnd, +}); + +type CreateSeparatorToken = (value: SeparatorValue, posBegin: Position, posEnd: Position) => SeparatorToken; +export const createSeparatorToken: CreateSeparatorToken = (value, posBegin, posEnd) => ({ + type: "separator", + value, + posBegin, + posEnd, +}); diff --git a/src/lexer/source-token/identifier/index.ts b/src/lexer/source-token/identifier/index.ts new file mode 100644 index 0000000..32948ae --- /dev/null +++ b/src/lexer/source-token/identifier/index.ts @@ -0,0 +1,33 @@ +import type Position from "../../../util/position"; +import type { SourceTokenBase } from "./../base"; + +type KeywordValue = BranchKeywordValue | FunctionKeywordValue | ReturnKeywordValue; +type BranchKeywordValue = "만약" | "아니면"; +type FunctionKeywordValue = "함수"; +type ReturnKeywordValue = "결과"; + +export interface IdentifierToken extends SourceTokenBase { + type: "identifier"; + value: string; +} + +export interface KeywordToken extends SourceTokenBase { + type: "keyword"; + value: KeywordValue; +} + +type CreateIdentifierToken = (value: string, posBegin: Position, posEnd: Position) => IdentifierToken; +export const createIdentifierToken: CreateIdentifierToken = (value, posBegin, posEnd) => ({ + type: "identifier", + value, + posBegin, + posEnd, +}); + +type CreateKeywordToken = (value: KeywordValue, posBegin: Position, posEnd: Position) => KeywordToken; +export const createKeywordToken: CreateKeywordToken = (value, posBegin, posEnd) => ({ + type: "keyword", + value, + posBegin, + posEnd, +}); diff --git a/src/lexer/source-token/index.ts b/src/lexer/source-token/index.ts new file mode 100644 index 0000000..e3f71a2 --- /dev/null +++ b/src/lexer/source-token/index.ts @@ -0,0 +1,29 @@ +import type { OperatorToken } from "./operator"; +import type { IdentifierToken, KeywordToken } from "./identifier"; +import type { NumberLiteralToken, BooleanLiteralToken, StringLiteralToken } from "./literal"; +import type { GroupDelimiterToken, BlockDelimiterToken, SeparatorToken } from "./delimiter"; +import type { IllegalToken, IllegalStringLiteralToken, EndToken } from "./special"; + +export type SourceToken = OperatorToken + | IdentifierToken + | KeywordToken + | NumberLiteralToken + | BooleanLiteralToken + | StringLiteralToken + | GroupDelimiterToken + | BlockDelimiterToken + | SeparatorToken + | IllegalToken + | IllegalStringLiteralToken + | EndToken + +export * from "./operator"; +export type * from "./operator"; +export * from "./identifier"; +export type * from "./identifier"; +export * from "./literal"; +export type * from "./literal"; +export * from "./delimiter"; +export type * from "./delimiter"; +export * from "./special"; +export type * from "./special"; diff --git a/src/lexer/source-token/literal/index.ts b/src/lexer/source-token/literal/index.ts new file mode 100644 index 0000000..05905d1 --- /dev/null +++ b/src/lexer/source-token/literal/index.ts @@ -0,0 +1,43 @@ +import type Position from "../../../util/position"; +import type { SourceTokenBase } from "./../base"; + +type BooleanLiteralValue = "참" | "거짓"; + +export interface NumberLiteralToken extends SourceTokenBase { + type: "number literal"; + value: string; +} + +export interface BooleanLiteralToken extends SourceTokenBase { + type: "boolean literal"; + value: BooleanLiteralValue; +} + +export interface StringLiteralToken extends SourceTokenBase { + type: "string literal"; + value: string; +} + +type CreateNumberLiteralToken = (value: string, posBegin: Position, posEnd: Position) => NumberLiteralToken; +export const createNumberLiteralToken: CreateNumberLiteralToken = (value, posBegin, posEnd) => ({ + type: "number literal", + value, + posBegin, + posEnd, +}); + +type CreateBooleanLiteralToken = (value: BooleanLiteralValue, posBegin: Position, posEnd: Position) => BooleanLiteralToken; +export const createBooleanLiteralToken: CreateBooleanLiteralToken = (value, posBegin, posEnd) => ({ + type: "boolean literal", + value, + posBegin, + posEnd, +}); + +type CreateStringLiteralToken = (value: string, posBegin: Position, posEnd: Position) => StringLiteralToken; +export const createStringLiteralToken: CreateStringLiteralToken = (value, posBegin, posEnd) => ({ + type: "string literal", + value, + posBegin, + posEnd, +}); diff --git a/src/lexer/source-token/operator/index.ts b/src/lexer/source-token/operator/index.ts new file mode 100644 index 0000000..693ed16 --- /dev/null +++ b/src/lexer/source-token/operator/index.ts @@ -0,0 +1,20 @@ +import type Position from "../../../util/position"; +import type { SourceTokenBase } from "./../base"; + +export type OperatorValue = ArithmeticOperatorValue | AssignmentOperatorValue | LogicalOperatorValue; +export type ArithmeticOperatorValue = "+" | "-" | "*" | "/"; +export type AssignmentOperatorValue = "="; +export type LogicalOperatorValue = "!" | "!=" | "==" | ">" | "<" | ">=" | "<="; + +export interface OperatorToken extends SourceTokenBase { + type: "operator", + value: OperatorValue, +} + +type CreateOperatorToken = (value: OperatorValue, posBegin: Position, posEnd: Position) => OperatorToken; +export const createOperatorToken: CreateOperatorToken = (value, posBegin, posEnd) => ({ + type: "operator", + value, + posBegin, + posEnd, +}); diff --git a/src/lexer/source-token/special/index.ts b/src/lexer/source-token/special/index.ts new file mode 100644 index 0000000..3f6f116 --- /dev/null +++ b/src/lexer/source-token/special/index.ts @@ -0,0 +1,44 @@ +import type Position from "../../../util/position"; +import type { SourceTokenBase } from "./../base"; + +export const END_VALUE = "$end"; // unreadable character '$' used to avoid other token values +type EndValue = typeof END_VALUE; + +export interface IllegalToken extends SourceTokenBase { + type: "illegal"; + value: string; +} + +export interface IllegalStringLiteralToken extends SourceTokenBase { + type: "illegal string"; + value: string; +} + +export interface EndToken extends SourceTokenBase { + type: "end"; + value: EndValue +} + +type CreateIllegalToken = (value: string, posBegin: Position, posEnd: Position) => IllegalToken; +export const createIllegalToken: CreateIllegalToken = (value, posBegin, posEnd) => ({ + type: "illegal", + value, + posBegin, + posEnd, +}); + +type CreateIllegalStringLiteralToken = (value: string, posBegin: Position, posEnd: Position) => IllegalStringLiteralToken; +export const createIllegalStringLiteralToken: CreateIllegalStringLiteralToken = (value, posBegin, posEnd) => ({ + type: "illegal string", + value, + posBegin, + posEnd, +}); + +type CreateEndToken = (posBegin: Position, posEnd: Position) => EndToken; +export const createEndToken: CreateEndToken = (posBegin, posEnd) => ({ + type: "end", + value: END_VALUE, + posBegin, + posEnd, +}); diff --git a/src/lexer/token/index.ts b/src/lexer/token/index.ts index ee587de..cb8d1de 100644 --- a/src/lexer/token/index.ts +++ b/src/lexer/token/index.ts @@ -82,51 +82,61 @@ export interface End { value: EndValue } +/** @deprecated */ export const operator = (value: Operator["value"]): Operator => ({ type: "operator", value, }); +/** @deprecated */ export const identifier = (value: Identifier["value"]): Identifier => ({ type: "identifier", value, }); +/** @deprecated */ export const numberLiteral = (value: NumberLiteral["value"]): NumberLiteral => ({ type: "number literal", value, }); +/** @deprecated */ export const booleanLiteral = (value: BooleanLiteral["value"]): BooleanLiteral => ({ type: "boolean literal", value, }); +/** @deprecated */ export const stringLiteral = (value: StringLiteral["value"]): StringLiteral => ({ type: "string literal", value, }); +/** @deprecated */ export const groupDelimiter = (value: GroupDelimiter["value"]): GroupDelimiter => ({ type: "group delimiter", value, }); +/** @deprecated */ export const blockDelimiter = (value: BlockDelimiter["value"]): BlockDelimiter => ({ type: "block delimiter", value, }); +/** @deprecated */ export const separator = (value: Separator["value"]): Separator => ({ type: "separator", value, }); +/** @deprecated */ export const keyword = (value: Keyword["value"]): Keyword => ({ type: "keyword", value, }); +/** @deprecated */ export const illegal = (value: Illegal["value"]): Illegal => ({ type: "illegal", value, diff --git a/src/lexer/util/index.ts b/src/lexer/util/index.ts index 485d6d6..3d2276f 100644 --- a/src/lexer/util/index.ts +++ b/src/lexer/util/index.ts @@ -15,9 +15,9 @@ export const isDigit = (char: string): boolean => { }; export const isWhitespace = (char: string): boolean => { - if (char.length !== 1) { + if (char.length > 2) { return false; } - return /^[ \t\r\n]$/.test(char); + return /^(\r\n|[ \t\r\n])$/.test(char); } diff --git a/src/util/position/index.ts b/src/util/position/index.ts new file mode 100644 index 0000000..b2c5565 --- /dev/null +++ b/src/util/position/index.ts @@ -0,0 +1,4 @@ +export default interface Position { + readonly row: number; + readonly col: number; +} From 1e0b789379746bc7d1b4e943696bc31fa8283342 Mon Sep 17 00:00:00 2001 From: Won-hee Cho <89635107+wcho21@users.noreply.github.com> Date: Thu, 25 Jan 2024 03:09:52 +0900 Subject: [PATCH 2/7] better typing for tokens (#49) --- src/lexer/char-buffer/char-reader/index.ts | 56 ++-- src/lexer/index.test.ts | 270 ++++++++++++------ src/lexer/index.ts | 4 +- src/lexer/source-token/base/index.ts | 37 ++- .../source-token/delimiter/index.test.ts | 96 +++++++ src/lexer/source-token/delimiter/index.ts | 47 +-- .../source-token/identifier/index.test.ts | 71 +++++ src/lexer/source-token/identifier/index.ts | 40 +-- src/lexer/source-token/literal/index.test.ts | 96 +++++++ src/lexer/source-token/literal/index.ts | 49 +--- src/lexer/source-token/operator/index.test.ts | 46 +++ src/lexer/source-token/operator/index.ts | 17 +- src/lexer/source-token/special/index.test.ts | 96 +++++++ src/lexer/source-token/special/index.ts | 47 +-- .../source-token/testing/fixtures/index.ts | 1 + src/lexer/token/index.ts | 1 + src/util/position/index.ts | 4 +- 17 files changed, 691 insertions(+), 287 deletions(-) create mode 100644 src/lexer/source-token/delimiter/index.test.ts create mode 100644 src/lexer/source-token/identifier/index.test.ts create mode 100644 src/lexer/source-token/literal/index.test.ts create mode 100644 src/lexer/source-token/operator/index.test.ts create mode 100644 src/lexer/source-token/special/index.test.ts create mode 100644 src/lexer/source-token/testing/fixtures/index.ts diff --git a/src/lexer/char-buffer/char-reader/index.ts b/src/lexer/char-buffer/char-reader/index.ts index f7a3f6c..dc4de63 100644 --- a/src/lexer/char-buffer/char-reader/index.ts +++ b/src/lexer/char-buffer/char-reader/index.ts @@ -20,33 +20,7 @@ export default class CharReader { this.col = 0; } - private peekNewLine(): string | null { - // return if end of input - if (this.index === this.chars.length) { - return null; - } - - // return if no new line - const char = this.chars[this.index]; - if (char !== "\r" && char !== "\n") { - return null; - } - - // return if the last character - if (this.index+1 === this.chars.length) { - return char; - } - - // return if single-character new line - const nextChar = this.chars[this.index+1]; - if (nextChar !== "\r" && nextChar !== "\n") { - return char; - } - - // return two-character new line - return char + nextChar; - } - + /** return character at current position */ readChar(): SourceChar { // return fallback character if end of input if (this.index === this.chars.length) { @@ -85,6 +59,7 @@ export default class CharReader { return sourceChar; } + /** advance to next character */ advance(): void { if (this.index === this.chars.length) { return; @@ -104,6 +79,33 @@ export default class CharReader { ++this.col; } + private peekNewLine(): string | null { + // return if end of input + if (this.index === this.chars.length) { + return null; + } + + // return if no new line + const char = this.chars[this.index]; + if (char !== "\r" && char !== "\n") { + return null; + } + + // return if the last character + if (this.index+1 === this.chars.length) { + return char; + } + + // return if single-character new line + const nextChar = this.chars[this.index+1]; + if (nextChar !== "\r" && nextChar !== "\n") { + return char; + } + + // return two-character new line + return char + nextChar; + } + /** @deprecated Returns current character; if end of input, return fallback character */ read(): string { if (this.index === this.chars.length) { diff --git a/src/lexer/index.test.ts b/src/lexer/index.test.ts index ac451d7..41814cc 100644 --- a/src/lexer/index.test.ts +++ b/src/lexer/index.test.ts @@ -48,8 +48,10 @@ describe("getSourceToken()", () => { expected: { type: "operator", value: "+", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 0 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, }, }, { @@ -57,8 +59,10 @@ describe("getSourceToken()", () => { expected: { type: "operator", value: "-", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 0 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, }, }, { @@ -66,8 +70,10 @@ describe("getSourceToken()", () => { expected: { type: "operator", value: "*", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 0 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, }, }, { @@ -75,8 +81,10 @@ describe("getSourceToken()", () => { expected: { type: "operator", value: "/", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 0 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, }, }, { @@ -84,8 +92,10 @@ describe("getSourceToken()", () => { expected: { type: "operator", value: "=", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 0 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, }, }, { @@ -93,8 +103,10 @@ describe("getSourceToken()", () => { expected: { type: "operator", value: "==", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 1 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, }, }, { @@ -102,8 +114,10 @@ describe("getSourceToken()", () => { expected: { type: "operator", value: "!", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 0 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, }, }, { @@ -111,8 +125,10 @@ describe("getSourceToken()", () => { expected: { type: "operator", value: "!=", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 1 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, }, }, { @@ -120,8 +136,10 @@ describe("getSourceToken()", () => { expected: { type: "operator", value: ">", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 0 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, }, }, { @@ -129,8 +147,10 @@ describe("getSourceToken()", () => { expected: { type: "operator", value: ">=", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 1 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, }, }, { @@ -138,8 +158,10 @@ describe("getSourceToken()", () => { expected: { type: "operator", value: "<", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 0 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, }, }, { @@ -147,8 +169,10 @@ describe("getSourceToken()", () => { expected: { type: "operator", value: "<=", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 1 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, }, }, ]; @@ -163,8 +187,10 @@ describe("getSourceToken()", () => { expected: { type: "group delimiter", value: "(", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 0 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, }, }, { @@ -172,8 +198,10 @@ describe("getSourceToken()", () => { expected: { type: "group delimiter", value: ")", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 0 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, }, }, { @@ -181,8 +209,10 @@ describe("getSourceToken()", () => { expected: { type: "block delimiter", value: "{", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 0 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, }, }, { @@ -190,8 +220,10 @@ describe("getSourceToken()", () => { expected: { type: "block delimiter", value: "}", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 0 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, }, }, { @@ -199,8 +231,10 @@ describe("getSourceToken()", () => { expected: { type: "separator", value: ",", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 0 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, }, }, ]; @@ -215,8 +249,10 @@ describe("getSourceToken()", () => { expected: { type: "number literal", value: "0", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 0 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, }, }, { @@ -224,8 +260,10 @@ describe("getSourceToken()", () => { expected: { type: "number literal", value: "1", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 0 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, }, }, { @@ -233,8 +271,10 @@ describe("getSourceToken()", () => { expected: { type: "number literal", value: "1234", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 3 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 3 }, + }, }, }, { @@ -242,8 +282,10 @@ describe("getSourceToken()", () => { expected: { type: "number literal", value: "12.34", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 4 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 4 }, + }, }, }, ]; @@ -258,8 +300,10 @@ describe("getSourceToken()", () => { expected: { type: "boolean literal", value: "참", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 0 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, }, }, { @@ -267,8 +311,10 @@ describe("getSourceToken()", () => { expected: { type: "boolean literal", value: "거짓", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 1 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, }, }, ]; @@ -283,8 +329,10 @@ describe("getSourceToken()", () => { expected: { type: "string literal", value: "foo bar 123 !@# 참", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 18 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 18 }, + }, }, }, { @@ -292,8 +340,10 @@ describe("getSourceToken()", () => { expected: { type: "string literal", value: "", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 1 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, }, }, ]; @@ -308,8 +358,10 @@ describe("getSourceToken()", () => { expected: { type: "keyword", value: "만약", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 1 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, }, }, { @@ -317,8 +369,10 @@ describe("getSourceToken()", () => { expected: { type: "keyword", value: "아니면", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 2 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 2 }, + }, }, }, { @@ -326,8 +380,10 @@ describe("getSourceToken()", () => { expected: { type: "keyword", value: "함수", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 1 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, }, }, { @@ -335,8 +391,10 @@ describe("getSourceToken()", () => { expected: { type: "keyword", value: "결과", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 1 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, }, }, ]; @@ -351,8 +409,10 @@ describe("getSourceToken()", () => { expected: { type: "identifier", value: "Foo이름123_", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 8 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, + }, }, }, { @@ -360,8 +420,10 @@ describe("getSourceToken()", () => { expected: { type: "identifier", value: "이름Foo123_", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 8 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, + }, }, }, { @@ -369,8 +431,10 @@ describe("getSourceToken()", () => { expected: { type: "identifier", value: "_이름Foo123", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 8 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, + }, }, }, ]; @@ -385,8 +449,10 @@ describe("getSourceToken()", () => { expected: { type: "illegal", value: "$", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 0 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, }, }, { @@ -394,8 +460,10 @@ describe("getSourceToken()", () => { expected: { type: "illegal string", value: "foo", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 4 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 4 }, + }, }, }, { @@ -403,8 +471,10 @@ describe("getSourceToken()", () => { expected: { type: "end", value: "$end", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 0 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, }, }, ]; @@ -421,26 +491,34 @@ describe("getSourceToken()", () => { { type: "number literal", value: "12", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 1 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, }, { type: "operator", value: "+", - posBegin: { row: 0, col: 3 }, - posEnd: { row: 0, col: 3 }, + range: { + begin: { row: 0, col: 3 }, + end: { row: 0, col: 3 }, + }, }, { type: "number literal", value: "34", - posBegin: { row: 0, col: 5 }, - posEnd: { row: 0, col: 6 }, + range: { + begin: { row: 0, col: 5 }, + end: { row: 0, col: 6 }, + }, }, { type: "end", value: "$end", - posBegin: { row: 0, col: 7 }, - posEnd: { row: 0, col: 7 }, + range: { + begin: { row: 0, col: 7 }, + end: { row: 0, col: 7 }, + }, }, ] }, @@ -450,38 +528,50 @@ describe("getSourceToken()", () => { { type: "keyword", value: "만약", - posBegin: { row: 0, col: 0 }, - posEnd: { row: 0, col: 1 }, + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, }, { type: "boolean literal", value: "참", - posBegin: { row: 0, col: 3 }, - posEnd: { row: 0, col: 3 }, + range: { + begin: { row: 0, col: 3 }, + end: { row: 0, col: 3 }, + }, }, { type: "block delimiter", value: "{", - posBegin: { row: 0, col: 5 }, - posEnd: { row: 0, col: 5 }, + range: { + begin: { row: 0, col: 5 }, + end: { row: 0, col: 5 }, + }, }, { type: "number literal", value: "12", - posBegin: { row: 1, col: 2 }, - posEnd: { row: 1, col: 3 }, + range: { + begin: { row: 1, col: 2 }, + end: { row: 1, col: 3 }, + }, }, { type: "block delimiter", value: "}", - posBegin: { row: 2, col: 0 }, - posEnd: { row: 2, col: 0 }, + range: { + begin: { row: 2, col: 0 }, + end: { row: 2, col: 0 }, + }, }, { type: "end", value: "$end", - posBegin: { row: 2, col: 1 }, - posEnd: { row: 2, col: 1 }, + range: { + begin: { row: 2, col: 1 }, + end: { row: 2, col: 1 }, + }, }, ] }, diff --git a/src/lexer/index.ts b/src/lexer/index.ts index a60f30a..c315684 100644 --- a/src/lexer/index.ts +++ b/src/lexer/index.ts @@ -123,8 +123,7 @@ export default class Lexer { { const { position: pos } = this.charBuffer.popChar(); - // TODO - const token = createEndToken(pos, pos); + const token = createEndToken("$end", pos, pos); return token; } @@ -564,3 +563,4 @@ export default class Lexer { } export type { TokenType } from "./token"; +export type { SourceToken } from "./source-token"; diff --git a/src/lexer/source-token/base/index.ts b/src/lexer/source-token/base/index.ts index 445461e..92512f9 100644 --- a/src/lexer/source-token/base/index.ts +++ b/src/lexer/source-token/base/index.ts @@ -1,8 +1,35 @@ import type Position from "../../../util/position"; -export interface SourceTokenBase { - type: string, - value: string, - posBegin: Position, - posEnd: Position, +export interface Range { + readonly begin: Position, + readonly end: Position, }; + +export interface SourceTokenBase { + readonly type: T, + readonly value: V, + readonly range: Range, +}; + +/** returns overloaded token creator function */ +export function createTokenCreator(type: T["type"]) { + // explicitly specify the return type since the overloaded function cannot infer it + type Token = { type: T["type"], value: T["value"], range: Range }; + + function createToken(value: T["value"], range: Range): Token; + function createToken(value: T["value"], pos1: Position, pos2: Position): Token; + function createToken(value: T["value"], arg1: Position | Range, pos2?: Position): Token { + if (pos2 !== undefined) { + const range = { begin: arg1 as Position, end: pos2 as Position }; + return { type, value, range }; + } + + return { type, value, range: arg1 as { begin: Position, end: Position } }; + }; + + return createToken; +}; + +declare function createToken(value: T["value"], range: Range): T; +declare function createToken(value: T["value"], pos1: Position, pos2: Position): T; +export type CreateToken = typeof createToken; diff --git a/src/lexer/source-token/delimiter/index.test.ts b/src/lexer/source-token/delimiter/index.test.ts new file mode 100644 index 0000000..0aeff2c --- /dev/null +++ b/src/lexer/source-token/delimiter/index.test.ts @@ -0,0 +1,96 @@ +import { + createGroupDelimiterToken, + createBlockDelimiterToken, + createSeparatorToken, +} from "./"; +import { fakePos } from "../testing/fixtures"; + +describe("create token with begin and end position", () => { + const cases = [ + { + name: "group delimiter", + token: createGroupDelimiterToken("(", fakePos, fakePos), + expected: { + type: "group delimiter", + value: "(", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "block delimiter", + token: createBlockDelimiterToken("{", fakePos, fakePos), + expected: { + type: "block delimiter", + value: "{", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "separator", + token: createSeparatorToken(",", fakePos, fakePos), + expected: { + type: "separator", + value: ",", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); + +describe("create token with range", () => { + const cases = [ + { + name: "group delimiter", + token: createGroupDelimiterToken("(", { begin: fakePos, end: fakePos }), + expected: { + type: "group delimiter", + value: "(", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "block delimiter", + token: createBlockDelimiterToken("{", { begin: fakePos, end: fakePos }), + expected: { + type: "block delimiter", + value: "{", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "separator", + token: createSeparatorToken(",", { begin: fakePos, end: fakePos }), + expected: { + type: "separator", + value: ",", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); diff --git a/src/lexer/source-token/delimiter/index.ts b/src/lexer/source-token/delimiter/index.ts index 54e23e3..79109b3 100644 --- a/src/lexer/source-token/delimiter/index.ts +++ b/src/lexer/source-token/delimiter/index.ts @@ -1,45 +1,14 @@ -import type Position from "../../../util/position"; -import type { SourceTokenBase } from "./../base"; +import type { SourceTokenBase, CreateToken } from "./../base"; +import { createTokenCreator } from "../base"; export type GroupDelimiterValue = "(" | ")"; export type BlockDelimiterValue = "{" | "}"; export type SeparatorValue = ","; -export interface GroupDelimiterToken extends SourceTokenBase { - type: "group delimiter"; - value: GroupDelimiterValue; -} +export type GroupDelimiterToken = SourceTokenBase<"group delimiter", GroupDelimiterValue>; +export type BlockDelimiterToken = SourceTokenBase<"block delimiter", BlockDelimiterValue>; +export type SeparatorToken = SourceTokenBase<"separator", SeparatorValue>; -export interface BlockDelimiterToken extends SourceTokenBase { - type: "block delimiter"; - value: BlockDelimiterValue; -} - -export interface SeparatorToken extends SourceTokenBase { - type: "separator", - value: SeparatorValue; -} - -type CreateGroupDelimiterToken = (value: GroupDelimiterValue, posBegin: Position, posEnd: Position) => GroupDelimiterToken; -export const createGroupDelimiterToken: CreateGroupDelimiterToken = (value, posBegin, posEnd) => ({ - type: "group delimiter", - value, - posBegin, - posEnd, -}); - -type CreateBlockDelimiterToken = (value: BlockDelimiterValue, posBegin: Position, posEnd: Position) => BlockDelimiterToken; -export const createBlockDelimiterToken: CreateBlockDelimiterToken = (value, posBegin, posEnd) => ({ - type: "block delimiter", - value, - posBegin, - posEnd, -}); - -type CreateSeparatorToken = (value: SeparatorValue, posBegin: Position, posEnd: Position) => SeparatorToken; -export const createSeparatorToken: CreateSeparatorToken = (value, posBegin, posEnd) => ({ - type: "separator", - value, - posBegin, - posEnd, -}); +export const createGroupDelimiterToken: CreateToken = createTokenCreator("group delimiter"); +export const createBlockDelimiterToken: CreateToken = createTokenCreator("block delimiter"); +export const createSeparatorToken: CreateToken = createTokenCreator("separator"); diff --git a/src/lexer/source-token/identifier/index.test.ts b/src/lexer/source-token/identifier/index.test.ts new file mode 100644 index 0000000..e86d0a3 --- /dev/null +++ b/src/lexer/source-token/identifier/index.test.ts @@ -0,0 +1,71 @@ +import { + createIdentifierToken, + createKeywordToken, +} from "./"; +import { fakePos } from "../testing/fixtures"; + +describe("create token with begin and end position", () => { + const cases = [ + { + name: "identifier", + token: createIdentifierToken("foo", fakePos, fakePos), + expected: { + type: "identifier", + value: "foo", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "keyword", + token: createKeywordToken("만약", fakePos, fakePos), + expected: { + type: "keyword", + value: "만약", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); + +describe("create token with range", () => { + const cases = [ + { + name: "identifier", + token: createIdentifierToken("foo", { begin: fakePos, end: fakePos }), + expected: { + type: "identifier", + value: "foo", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "keyword", + token: createKeywordToken("만약", { begin: fakePos, end: fakePos }), + expected: { + type: "keyword", + value: "만약", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); diff --git a/src/lexer/source-token/identifier/index.ts b/src/lexer/source-token/identifier/index.ts index 32948ae..2fb4943 100644 --- a/src/lexer/source-token/identifier/index.ts +++ b/src/lexer/source-token/identifier/index.ts @@ -1,33 +1,13 @@ -import type Position from "../../../util/position"; -import type { SourceTokenBase } from "./../base"; +import type { SourceTokenBase, CreateToken } from "./../base"; +import { createTokenCreator } from "../base"; -type KeywordValue = BranchKeywordValue | FunctionKeywordValue | ReturnKeywordValue; -type BranchKeywordValue = "만약" | "아니면"; -type FunctionKeywordValue = "함수"; -type ReturnKeywordValue = "결과"; +export type KeywordValue = BranchKeywordValue | FunctionKeywordValue | ReturnKeywordValue; +export type BranchKeywordValue = "만약" | "아니면"; +export type FunctionKeywordValue = "함수"; +export type ReturnKeywordValue = "결과"; -export interface IdentifierToken extends SourceTokenBase { - type: "identifier"; - value: string; -} +export type IdentifierToken = SourceTokenBase<"identifier", string>; +export type KeywordToken = SourceTokenBase<"keyword", KeywordValue>; -export interface KeywordToken extends SourceTokenBase { - type: "keyword"; - value: KeywordValue; -} - -type CreateIdentifierToken = (value: string, posBegin: Position, posEnd: Position) => IdentifierToken; -export const createIdentifierToken: CreateIdentifierToken = (value, posBegin, posEnd) => ({ - type: "identifier", - value, - posBegin, - posEnd, -}); - -type CreateKeywordToken = (value: KeywordValue, posBegin: Position, posEnd: Position) => KeywordToken; -export const createKeywordToken: CreateKeywordToken = (value, posBegin, posEnd) => ({ - type: "keyword", - value, - posBegin, - posEnd, -}); +export const createIdentifierToken: CreateToken = createTokenCreator("identifier"); +export const createKeywordToken: CreateToken = createTokenCreator("keyword"); diff --git a/src/lexer/source-token/literal/index.test.ts b/src/lexer/source-token/literal/index.test.ts new file mode 100644 index 0000000..43509ec --- /dev/null +++ b/src/lexer/source-token/literal/index.test.ts @@ -0,0 +1,96 @@ +import { + createNumberLiteralToken, + createBooleanLiteralToken, + createStringLiteralToken, +} from "./"; +import { fakePos } from "../testing/fixtures"; + +describe("create token with begin and end position", () => { + const cases = [ + { + name: "number literal", + token: createNumberLiteralToken("0", fakePos, fakePos), + expected: { + type: "number literal", + value: "0", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "boolean literal", + token: createBooleanLiteralToken("참", fakePos, fakePos), + expected: { + type: "boolean literal", + value: "참", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "string literal", + token: createStringLiteralToken("foo", fakePos, fakePos), + expected: { + type: "string literal", + value: "foo", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); + +describe("create token with range", () => { + const cases = [ + { + name: "number literal", + token: createNumberLiteralToken("0", { begin: fakePos, end: fakePos }), + expected: { + type: "number literal", + value: "0", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "boolean literal", + token: createBooleanLiteralToken("참", { begin: fakePos, end: fakePos }), + expected: { + type: "boolean literal", + value: "참", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "string literal", + token: createStringLiteralToken("foo", { begin: fakePos, end: fakePos }), + expected: { + type: "string literal", + value: "foo", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); diff --git a/src/lexer/source-token/literal/index.ts b/src/lexer/source-token/literal/index.ts index 05905d1..4377e42 100644 --- a/src/lexer/source-token/literal/index.ts +++ b/src/lexer/source-token/literal/index.ts @@ -1,43 +1,12 @@ -import type Position from "../../../util/position"; -import type { SourceTokenBase } from "./../base"; +import type { SourceTokenBase, CreateToken } from "./../base"; +import { createTokenCreator } from "../base"; -type BooleanLiteralValue = "참" | "거짓"; +export type BooleanLiteralValue = "참" | "거짓"; -export interface NumberLiteralToken extends SourceTokenBase { - type: "number literal"; - value: string; -} +export type NumberLiteralToken = SourceTokenBase<"number literal", string>; +export type BooleanLiteralToken = SourceTokenBase<"boolean literal", BooleanLiteralValue>; +export type StringLiteralToken = SourceTokenBase<"string literal", string>; -export interface BooleanLiteralToken extends SourceTokenBase { - type: "boolean literal"; - value: BooleanLiteralValue; -} - -export interface StringLiteralToken extends SourceTokenBase { - type: "string literal"; - value: string; -} - -type CreateNumberLiteralToken = (value: string, posBegin: Position, posEnd: Position) => NumberLiteralToken; -export const createNumberLiteralToken: CreateNumberLiteralToken = (value, posBegin, posEnd) => ({ - type: "number literal", - value, - posBegin, - posEnd, -}); - -type CreateBooleanLiteralToken = (value: BooleanLiteralValue, posBegin: Position, posEnd: Position) => BooleanLiteralToken; -export const createBooleanLiteralToken: CreateBooleanLiteralToken = (value, posBegin, posEnd) => ({ - type: "boolean literal", - value, - posBegin, - posEnd, -}); - -type CreateStringLiteralToken = (value: string, posBegin: Position, posEnd: Position) => StringLiteralToken; -export const createStringLiteralToken: CreateStringLiteralToken = (value, posBegin, posEnd) => ({ - type: "string literal", - value, - posBegin, - posEnd, -}); +export const createNumberLiteralToken: CreateToken = createTokenCreator("number literal"); +export const createBooleanLiteralToken: CreateToken = createTokenCreator("boolean literal"); +export const createStringLiteralToken: CreateToken = createTokenCreator("string literal"); diff --git a/src/lexer/source-token/operator/index.test.ts b/src/lexer/source-token/operator/index.test.ts new file mode 100644 index 0000000..33a68d9 --- /dev/null +++ b/src/lexer/source-token/operator/index.test.ts @@ -0,0 +1,46 @@ +import { + createOperatorToken, +} from "./"; +import { fakePos } from "../testing/fixtures"; + +describe("create token with begin and end position", () => { + const cases = [ + { + name: "operator", + token: createOperatorToken("+", fakePos, fakePos), + expected: { + type: "operator", + value: "+", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); + +describe("create token with range", () => { + const cases = [ + { + name: "operator", + token: createOperatorToken("+", { begin: fakePos, end: fakePos }), + expected: { + type: "operator", + value: "+", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); diff --git a/src/lexer/source-token/operator/index.ts b/src/lexer/source-token/operator/index.ts index 693ed16..97d02ff 100644 --- a/src/lexer/source-token/operator/index.ts +++ b/src/lexer/source-token/operator/index.ts @@ -1,20 +1,11 @@ -import type Position from "../../../util/position"; -import type { SourceTokenBase } from "./../base"; +import type { SourceTokenBase, CreateToken } from "./../base"; +import { createTokenCreator } from "../base"; export type OperatorValue = ArithmeticOperatorValue | AssignmentOperatorValue | LogicalOperatorValue; export type ArithmeticOperatorValue = "+" | "-" | "*" | "/"; export type AssignmentOperatorValue = "="; export type LogicalOperatorValue = "!" | "!=" | "==" | ">" | "<" | ">=" | "<="; -export interface OperatorToken extends SourceTokenBase { - type: "operator", - value: OperatorValue, -} +export type OperatorToken = SourceTokenBase<"operator", OperatorValue>; -type CreateOperatorToken = (value: OperatorValue, posBegin: Position, posEnd: Position) => OperatorToken; -export const createOperatorToken: CreateOperatorToken = (value, posBegin, posEnd) => ({ - type: "operator", - value, - posBegin, - posEnd, -}); +export const createOperatorToken: CreateToken = createTokenCreator("operator"); diff --git a/src/lexer/source-token/special/index.test.ts b/src/lexer/source-token/special/index.test.ts new file mode 100644 index 0000000..6aa7a1b --- /dev/null +++ b/src/lexer/source-token/special/index.test.ts @@ -0,0 +1,96 @@ +import { + createIllegalToken, + createIllegalStringLiteralToken, + createEndToken, +} from "./"; +import { fakePos } from "../testing/fixtures"; + +describe("create token with begin and end position", () => { + const cases = [ + { + name: "illegal", + token: createIllegalToken("$", fakePos, fakePos), + expected: { + type: "illegal", + value: "$", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "illegal string", + token: createIllegalStringLiteralToken("foo", fakePos, fakePos), + expected: { + type: "illegal string", + value: "foo", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "end", + token: createEndToken("$end", fakePos, fakePos), + expected: { + type: "end", + value: "$end", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); + +describe("create token with range", () => { + const cases = [ + { + name: "illegal", + token: createIllegalToken("$end", { begin: fakePos, end: fakePos }), + expected: { + type: "illegal", + value: "$end", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "illegal string", + token: createIllegalStringLiteralToken("foo", { begin: fakePos, end: fakePos }), + expected: { + type: "illegal string", + value: "foo", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + { + name: "end", + token: createEndToken("$end", { begin: fakePos, end: fakePos }), + expected: { + type: "end", + value: "$end", + range: { + begin: fakePos, + end: fakePos, + }, + }, + }, + ]; + + it.each(cases)("create $name token", ({ token, expected }) => { + expect(token).toEqual(expected); + }); +}); diff --git a/src/lexer/source-token/special/index.ts b/src/lexer/source-token/special/index.ts index 3f6f116..6e50e57 100644 --- a/src/lexer/source-token/special/index.ts +++ b/src/lexer/source-token/special/index.ts @@ -1,44 +1,13 @@ -import type Position from "../../../util/position"; -import type { SourceTokenBase } from "./../base"; +import type { SourceTokenBase, CreateToken } from "./../base"; +import { createTokenCreator } from "../base"; export const END_VALUE = "$end"; // unreadable character '$' used to avoid other token values type EndValue = typeof END_VALUE; -export interface IllegalToken extends SourceTokenBase { - type: "illegal"; - value: string; -} +export type IllegalToken = SourceTokenBase<"illegal", string>; +export type IllegalStringLiteralToken = SourceTokenBase<"illegal string", string>; +export type EndToken = SourceTokenBase<"end", EndValue>; -export interface IllegalStringLiteralToken extends SourceTokenBase { - type: "illegal string"; - value: string; -} - -export interface EndToken extends SourceTokenBase { - type: "end"; - value: EndValue -} - -type CreateIllegalToken = (value: string, posBegin: Position, posEnd: Position) => IllegalToken; -export const createIllegalToken: CreateIllegalToken = (value, posBegin, posEnd) => ({ - type: "illegal", - value, - posBegin, - posEnd, -}); - -type CreateIllegalStringLiteralToken = (value: string, posBegin: Position, posEnd: Position) => IllegalStringLiteralToken; -export const createIllegalStringLiteralToken: CreateIllegalStringLiteralToken = (value, posBegin, posEnd) => ({ - type: "illegal string", - value, - posBegin, - posEnd, -}); - -type CreateEndToken = (posBegin: Position, posEnd: Position) => EndToken; -export const createEndToken: CreateEndToken = (posBegin, posEnd) => ({ - type: "end", - value: END_VALUE, - posBegin, - posEnd, -}); +export const createIllegalToken: CreateToken = createTokenCreator("illegal"); +export const createIllegalStringLiteralToken: CreateToken = createTokenCreator("illegal string"); +export const createEndToken: CreateToken = createTokenCreator("end"); diff --git a/src/lexer/source-token/testing/fixtures/index.ts b/src/lexer/source-token/testing/fixtures/index.ts new file mode 100644 index 0000000..441a3d0 --- /dev/null +++ b/src/lexer/source-token/testing/fixtures/index.ts @@ -0,0 +1 @@ +export const fakePos = { row: 0, col: 0 }; diff --git a/src/lexer/token/index.ts b/src/lexer/token/index.ts index cb8d1de..cced492 100644 --- a/src/lexer/token/index.ts +++ b/src/lexer/token/index.ts @@ -1,3 +1,4 @@ +/** @deprecated */ export type TokenType = Operator | Identifier | diff --git a/src/util/position/index.ts b/src/util/position/index.ts index b2c5565..c37a918 100644 --- a/src/util/position/index.ts +++ b/src/util/position/index.ts @@ -1,4 +1,4 @@ export default interface Position { - readonly row: number; - readonly col: number; + readonly row: number, + readonly col: number, } From 9d8f8c326d0f381136f84965b850eecacdde4991 Mon Sep 17 00:00:00 2001 From: Won-hee Cho <89635107+wcho21@users.noreply.github.com> Date: Thu, 25 Jan 2024 03:38:45 +0900 Subject: [PATCH 3/7] add syntax node types with position context (#50) --- src/parser/source-token-reader/index.test.ts | 76 +++++++++++ src/parser/source-token-reader/index.ts | 24 ++++ src/parser/syntax-node/base/index.ts | 25 ++++ .../syntax-node/expression/index.test.ts | 118 ++++++++++++++++++ src/parser/syntax-node/expression/index.ts | 35 ++++++ src/parser/syntax-node/group/index.test.ts | 33 +++++ src/parser/syntax-node/group/index.ts | 12 ++ src/parser/syntax-node/index.ts | 12 ++ .../syntax-node/statement/index.test.ts | 65 ++++++++++ src/parser/syntax-node/statement/index.ts | 15 +++ .../syntax-node/testing/fixtures/index.ts | 1 + src/parser/token-reader/index.ts | 4 + 12 files changed, 420 insertions(+) create mode 100644 src/parser/source-token-reader/index.test.ts create mode 100644 src/parser/source-token-reader/index.ts create mode 100644 src/parser/syntax-node/base/index.ts create mode 100644 src/parser/syntax-node/expression/index.test.ts create mode 100644 src/parser/syntax-node/expression/index.ts create mode 100644 src/parser/syntax-node/group/index.test.ts create mode 100644 src/parser/syntax-node/group/index.ts create mode 100644 src/parser/syntax-node/index.ts create mode 100644 src/parser/syntax-node/statement/index.test.ts create mode 100644 src/parser/syntax-node/statement/index.ts create mode 100644 src/parser/syntax-node/testing/fixtures/index.ts diff --git a/src/parser/source-token-reader/index.test.ts b/src/parser/source-token-reader/index.test.ts new file mode 100644 index 0000000..7e222fd --- /dev/null +++ b/src/parser/source-token-reader/index.test.ts @@ -0,0 +1,76 @@ +import Lexer from "../../lexer"; +import SourceTokenReader from "./"; + +const createReader = (input: string) => { + const lexer = new Lexer(input); + + return new SourceTokenReader(lexer); +}; + +describe("read()", () => { + it("read a token", () => { + const input = "42"; + const reader = createReader(input); + const expected = { + type: "number literal", + value: "42", + range: { + begin: { col: 0, row: 0 }, + end: { col: 1, row: 0 }, + }, + }; + + expect(reader.read()).toEqual(expected); + }); + + it("read the end token if nothing to read", () => { + const input = ""; + const reader = createReader(input); + const expected = { + type: "end", + value: "$end", + range: { + begin: { col: 0, row: 0 }, + end: { col: 0, row: 0 }, + }, + }; + + expect(reader.read()).toEqual(expected); + }); +}); + +describe("advance()", () => { + it("advance to next token", () => { + const input = "42 99"; + const reader = createReader(input); + const expected = { + type: "number literal", + value: "99", + range: { + begin: { col: 3, row: 0 }, + end: { col: 4, row: 0 }, + }, + }; + + reader.advance(); + expect(reader.read()).toEqual(expected); + }); +}); + +describe("isEnd()", () => { + it("return true if end", () => { + const input = ""; + const reader = createReader(input); + const expected = true; + + expect(reader.isEnd()).toEqual(expected); + }); + + it("return false if not end", () => { + const input = "42"; + const reader = createReader(input); + const expected = false; + + expect(reader.isEnd()).toEqual(expected); + }); +}); diff --git a/src/parser/source-token-reader/index.ts b/src/parser/source-token-reader/index.ts new file mode 100644 index 0000000..341117b --- /dev/null +++ b/src/parser/source-token-reader/index.ts @@ -0,0 +1,24 @@ +import Lexer from "../../lexer"; +import type { SourceToken } from "../../lexer"; + +export default class SourceTokenReader { + private readonly lexer: Lexer; + private token: SourceToken; + + constructor(lexer: Lexer) { + this.lexer = lexer; + this.token = lexer.getSourceToken(); + } + + read(): SourceToken { + return this.token; + } + + advance(): void { + this.token = this.lexer.getSourceToken(); + } + + isEnd(): boolean { + return this.token.type === "end"; + } +} diff --git a/src/parser/syntax-node/base/index.ts b/src/parser/syntax-node/base/index.ts new file mode 100644 index 0000000..68e77c5 --- /dev/null +++ b/src/parser/syntax-node/base/index.ts @@ -0,0 +1,25 @@ +import type Position from "../../../util/position"; + +export interface SyntaxNodeBase { + type: T, + range: { + begin: Position, + end: Position, + }, + fields: F, +}; + +export function createNodeCreator(type: N["type"]) { + function createNode(fields: N["fields"], rangeBegin: Position, rangeEnd: Position) { + const range = { + begin: rangeBegin, + end: rangeEnd, + }; + + return { type, range, fields }; + }; + + return createNode; +}; + +export type CreateNode = (fields: N["fields"], rangeBegin: Position, rangeEnd: Position) => N; diff --git a/src/parser/syntax-node/expression/index.test.ts b/src/parser/syntax-node/expression/index.test.ts new file mode 100644 index 0000000..3f40b1c --- /dev/null +++ b/src/parser/syntax-node/expression/index.test.ts @@ -0,0 +1,118 @@ +import { + createIdentifierNode, + createNumberNode, + createStringNode, + createPrefixNode, + createInfixNode, + createFunctionNode, + createCallNode, + createAssignmentNode, +} from "./"; +import type { + IdentifierNode, + ExpressionNode, +} from "./"; +import type { + BlockNode, +} from "../group"; +import { fakePos } from "../testing/fixtures"; + +const cases = [ + { + name: "identifier", + node: createIdentifierNode({ value: "foo" }, fakePos, fakePos), + expected: { + type: "identifier", + fields: { + value: "foo", + }, + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "number", + node: createNumberNode({ value: 42 }, fakePos, fakePos), + expected: { + type: "number", + fields: { + value: 42, + }, + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "string", + node: createStringNode({ value: "foo" }, fakePos, fakePos), + expected: { + type: "string", + fields: { + value: "foo", + }, + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "prefix", + node: createPrefixNode({ prefix: "+", right: {} as ExpressionNode }, fakePos, fakePos), + expected: { + type: "prefix", + fields: { + prefix: "+", + right: {}, + }, + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "infix", + node: createInfixNode({ infix: "+", left: {} as ExpressionNode, right: {} as ExpressionNode }, fakePos, fakePos), + expected: { + type: "infix", + fields: { + infix: "+", + left: {}, + right: {}, + }, + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "function", + node: createFunctionNode({ parameters: [] as IdentifierNode[], body: {} as BlockNode }, fakePos, fakePos), + expected: { + type: "function", + fields: { + parameters: [], + body: {}, + }, + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "call", + node: createCallNode({ func: {} as IdentifierNode, args: [] as ExpressionNode[] }, fakePos, fakePos), + expected: { + type: "call", + fields: { + func: {}, + args: [], + }, + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "assignment", + node: createAssignmentNode({ left: {} as IdentifierNode, right: {} as ExpressionNode }, fakePos, fakePos), + expected: { + type: "assignment", + fields: { + left: {}, + right: {}, + }, + range: { begin: fakePos, end: fakePos }, + }, + }, +]; +it.each(cases)("create $name node", ({ node, expected }) => { + expect(node).toEqual(expected); +}); diff --git a/src/parser/syntax-node/expression/index.ts b/src/parser/syntax-node/expression/index.ts new file mode 100644 index 0000000..6250ee7 --- /dev/null +++ b/src/parser/syntax-node/expression/index.ts @@ -0,0 +1,35 @@ +import type { SyntaxNodeBase, CreateNode } from "../base"; +import { createNodeCreator } from "../base"; +import type { BlockNode } from "../group"; + +export type Prefix = "+" | "-" | "!"; +export type Infix = "+" | "-" | "*" | "/" | "=" | "==" | "!=" | ">" | "<" | ">=" | "<="; + +export type ExpressionNode = IdentifierNode + | NumberNode + | BooleanNode + | StringNode + | PrefixNode + | InfixNode + | FunctionNode + | CallNode + | AssignmentNode; + +export interface IdentifierNode extends SyntaxNodeBase<"identifier", { value: string }> {}; +export interface NumberNode extends SyntaxNodeBase<"number", { value: number }> {}; +export interface BooleanNode extends SyntaxNodeBase<"boolean", { value: boolean }> {}; +export interface StringNode extends SyntaxNodeBase<"string", { value: string }> {}; +export interface PrefixNode extends SyntaxNodeBase<"prefix", { prefix: Prefix, right: ExpressionNode }> {}; +export interface InfixNode extends SyntaxNodeBase<"infix", { infix: Infix, left: ExpressionNode, right: ExpressionNode }> {}; +export interface FunctionNode extends SyntaxNodeBase<"function", { parameters: IdentifierNode[], body: BlockNode }> {}; +export interface CallNode extends SyntaxNodeBase<"call", { func: IdentifierNode | FunctionNode, args: ExpressionNode[] }> {}; +export interface AssignmentNode extends SyntaxNodeBase<"assignment", { left: IdentifierNode, right: ExpressionNode }> {}; + +export const createIdentifierNode: CreateNode = createNodeCreator("identifier"); +export const createNumberNode: CreateNode = createNodeCreator("number"); +export const createStringNode: CreateNode = createNodeCreator("string"); +export const createPrefixNode: CreateNode = createNodeCreator("prefix"); +export const createInfixNode: CreateNode = createNodeCreator("infix"); +export const createFunctionNode: CreateNode = createNodeCreator("function"); +export const createCallNode: CreateNode = createNodeCreator("call"); +export const createAssignmentNode: CreateNode = createNodeCreator("assignment"); diff --git a/src/parser/syntax-node/group/index.test.ts b/src/parser/syntax-node/group/index.test.ts new file mode 100644 index 0000000..0187f3c --- /dev/null +++ b/src/parser/syntax-node/group/index.test.ts @@ -0,0 +1,33 @@ +import { + createProgramNode, + createBlockNode, +} from "./"; +import { fakePos } from "../testing/fixtures"; + +const cases = [ + { + name: "program", + node: createProgramNode({ statements: [] }, fakePos, fakePos), + expected: { + type: "program", + fields: { + statements: [], + }, + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "block", + node: createBlockNode({ statements: [] }, fakePos, fakePos), + expected: { + type: "block", + fields: { + statements: [], + }, + range: { begin: fakePos, end: fakePos }, + }, + }, +]; +it.each(cases)("create $name node", ({ node, expected }) => { + expect(node).toEqual(expected); +}); diff --git a/src/parser/syntax-node/group/index.ts b/src/parser/syntax-node/group/index.ts new file mode 100644 index 0000000..77a24ef --- /dev/null +++ b/src/parser/syntax-node/group/index.ts @@ -0,0 +1,12 @@ +import type { SyntaxNodeBase, CreateNode } from "../base"; +import { createNodeCreator } from "../base"; + +export type GroupNode = ProgramNode | BlockNode; + +/** a root node for a syntax tree of a program */ +export interface ProgramNode extends SyntaxNodeBase<"program", { statements: any[] }> {}; +/** a group of statements */ +export interface BlockNode extends SyntaxNodeBase<"block", { statements: any[] }> {}; + +export const createProgramNode: CreateNode = createNodeCreator("program"); +export const createBlockNode: CreateNode = createNodeCreator("block"); diff --git a/src/parser/syntax-node/index.ts b/src/parser/syntax-node/index.ts new file mode 100644 index 0000000..cdf0e22 --- /dev/null +++ b/src/parser/syntax-node/index.ts @@ -0,0 +1,12 @@ +import type { GroupNode } from "./group"; +import type { StatementNode } from "./statement"; +import type { ExpressionNode } from "./expression"; + +export type SyntaxNode = GroupNode | StatementNode | ExpressionNode; + +export * from "./group"; +export type * from "./group"; +export * from "./statement"; +export type * from "./statement"; +export * from "./expression"; +export type * from "./expression"; diff --git a/src/parser/syntax-node/statement/index.test.ts b/src/parser/syntax-node/statement/index.test.ts new file mode 100644 index 0000000..9e873d4 --- /dev/null +++ b/src/parser/syntax-node/statement/index.test.ts @@ -0,0 +1,65 @@ +import { + createBranchNode, + createReturnNode, + createExpressionStatementNode, +} from "./"; +import type { + ExpressionNode, +} from "../expression"; +import type { + BlockNode, +} from "../group"; +import { fakePos } from "../testing/fixtures"; + +const cases = [ + { + name: "branch", + node: createBranchNode({ predicate: {} as ExpressionNode, consequence: {} as BlockNode }, fakePos, fakePos), + expected: { + type: "branch", + fields: { + predicate: {}, + consequence: {}, + }, + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "branch with alternative", + node: createBranchNode({ predicate: {} as ExpressionNode, consequence: {} as BlockNode, alternative: {} as BlockNode }, fakePos, fakePos), + expected: { + type: "branch", + fields: { + predicate: {}, + consequence: {}, + alternative: {}, + }, + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "return", + node: createReturnNode({ expression: {} as ExpressionNode }, fakePos, fakePos), + expected: { + type: "return", + fields: { + expression: {}, + }, + range: { begin: fakePos, end: fakePos }, + }, + }, + { + name: "expression statement", + node: createExpressionStatementNode({ expression: {} as ExpressionNode }, fakePos, fakePos), + expected: { + type: "expression statement", + fields: { + expression: {}, + }, + range: { begin: fakePos, end: fakePos }, + }, + }, +]; +it.each(cases)("create $name node", ({ node, expected }) => { + expect(node).toEqual(expected); +}); diff --git a/src/parser/syntax-node/statement/index.ts b/src/parser/syntax-node/statement/index.ts new file mode 100644 index 0000000..88b0fd5 --- /dev/null +++ b/src/parser/syntax-node/statement/index.ts @@ -0,0 +1,15 @@ +import type { SyntaxNodeBase, CreateNode } from "../base"; +import { createNodeCreator } from "../base"; +import type { BlockNode } from "../group"; +import type { ExpressionNode } from "../expression"; + +export type StatementNode = BranchNode | ReturnNode | ExpressionStatementNode; + +export interface BranchNode extends SyntaxNodeBase<"branch", { predicate: ExpressionNode, consequence: BlockNode, alternative?: BlockNode }> {}; +export interface ReturnNode extends SyntaxNodeBase<"return", { expression: ExpressionNode }> {}; +/** A wrapper type to treat a single expression as a statement. */ +export interface ExpressionStatementNode extends SyntaxNodeBase<"expression statement", { expression: ExpressionNode }> {}; + +export const createBranchNode: CreateNode = createNodeCreator("branch"); +export const createReturnNode: CreateNode = createNodeCreator("return"); +export const createExpressionStatementNode: CreateNode = createNodeCreator("expression statement"); diff --git a/src/parser/syntax-node/testing/fixtures/index.ts b/src/parser/syntax-node/testing/fixtures/index.ts new file mode 100644 index 0000000..e6928c3 --- /dev/null +++ b/src/parser/syntax-node/testing/fixtures/index.ts @@ -0,0 +1 @@ +export const fakePos = { col: 0, row: 0 }; diff --git a/src/parser/token-reader/index.ts b/src/parser/token-reader/index.ts index f1025a4..015b0c0 100644 --- a/src/parser/token-reader/index.ts +++ b/src/parser/token-reader/index.ts @@ -1,6 +1,7 @@ import Lexer from "../../lexer"; import type { TokenType } from "../../lexer"; +/** @deprecated */ export default class TokenReader { private readonly lexer: Lexer; private token: TokenType; @@ -10,14 +11,17 @@ export default class TokenReader { this.token = lexer.getToken(); } + /** @deprecated */ isEnd(): boolean { return this.token.type === "end"; } + /** @deprecated */ read(): TokenType { return this.token; } + /** @deprecated */ next(): void { this.token = this.lexer.getToken(); } From 8952a68e5260471d7e413d2f226e813a6996b05f Mon Sep 17 00:00:00 2001 From: Won-hee Cho <89635107+wcho21@users.noreply.github.com> Date: Wed, 31 Jan 2024 13:48:09 +0900 Subject: [PATCH 4/7] Feat (#51) --- package.json | 1 + pnpm-lock.yaml | 65 +- src/parser/binding-power.ts | 45 + src/parser/index.ts | 1 + src/parser/syntax-node/base/index.ts | 38 +- src/parser/syntax-node/expression/index.ts | 1 + src/parser/syntax-node/group/index.ts | 5 +- src/parser/v2.test.ts | 1374 ++++++++++++++++++++ src/parser/v2.ts | 397 ++++++ src/util/position/index.ts | 19 + tsconfig.json | 1 + 11 files changed, 1901 insertions(+), 46 deletions(-) create mode 100644 src/parser/binding-power.ts create mode 100644 src/parser/v2.test.ts create mode 100644 src/parser/v2.ts diff --git a/package.json b/package.json index 0372888..f7064cb 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ ], "devDependencies": { "@types/jest": "^29.5.10", + "@types/node": "^20.11.6", "jest": "^29.7.0", "ts-jest": "^29.1.1", "typescript": "^5.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0087520..285005f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,12 @@ devDependencies: '@types/jest': specifier: ^29.5.10 version: 29.5.10 + '@types/node': + specifier: ^20.11.6 + version: 20.11.6 jest: specifier: ^29.7.0 - version: 29.7.0 + version: 29.7.0(@types/node@20.11.6) ts-jest: specifier: ^29.1.1 version: 29.1.1(@babel/core@7.23.5)(jest@29.7.0)(typescript@5.3.2) @@ -389,7 +392,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -410,14 +413,14 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.10.3) + jest-config: 29.7.0(@types/node@20.11.6) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -445,7 +448,7 @@ packages: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 jest-mock: 29.7.0 dev: true @@ -472,7 +475,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.10.3 + '@types/node': 20.11.6 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -505,7 +508,7 @@ packages: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.20 - '@types/node': 20.10.3 + '@types/node': 20.11.6 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -593,7 +596,7 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.10.3 + '@types/node': 20.11.6 '@types/yargs': 17.0.32 chalk: 4.1.2 dev: true @@ -701,7 +704,7 @@ packages: /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: - '@types/node': 20.10.3 + '@types/node': 20.11.6 dev: true /@types/istanbul-lib-coverage@2.0.6: @@ -731,8 +734,8 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true - /@types/node@20.10.3: - resolution: {integrity: sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==} + /@types/node@20.11.6: + resolution: {integrity: sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==} dependencies: undici-types: 5.26.5 dev: true @@ -1220,7 +1223,7 @@ packages: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} dev: true - /create-jest@29.7.0: + /create-jest@29.7.0(@types/node@20.11.6): resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -1229,7 +1232,7 @@ packages: chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.10.3) + jest-config: 29.7.0(@types/node@20.11.6) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -1680,7 +1683,7 @@ packages: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -1701,7 +1704,7 @@ packages: - supports-color dev: true - /jest-cli@29.7.0: + /jest-cli@29.7.0(@types/node@20.11.6): resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -1715,10 +1718,10 @@ packages: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0 + create-jest: 29.7.0(@types/node@20.11.6) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.10.3) + jest-config: 29.7.0(@types/node@20.11.6) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -1729,7 +1732,7 @@ packages: - ts-node dev: true - /jest-config@29.7.0(@types/node@20.10.3): + /jest-config@29.7.0(@types/node@20.11.6): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -1744,7 +1747,7 @@ packages: '@babel/core': 7.23.5 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 babel-jest: 29.7.0(@babel/core@7.23.5) chalk: 4.1.2 ci-info: 3.9.0 @@ -1804,7 +1807,7 @@ packages: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 jest-mock: 29.7.0 jest-util: 29.7.0 dev: true @@ -1820,7 +1823,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.10.3 + '@types/node': 20.11.6 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -1871,7 +1874,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 jest-util: 29.7.0 dev: true @@ -1926,7 +1929,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -1957,7 +1960,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 chalk: 4.1.2 cjs-module-lexer: 1.2.3 collect-v8-coverage: 1.0.2 @@ -2009,7 +2012,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -2034,7 +2037,7 @@ packages: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.3 + '@types/node': 20.11.6 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -2046,7 +2049,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.10.3 + '@types/node': 20.11.6 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true @@ -2055,13 +2058,13 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.10.3 + '@types/node': 20.11.6 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true - /jest@29.7.0: + /jest@29.7.0(@types/node@20.11.6): resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -2074,7 +2077,7 @@ packages: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0 + jest-cli: 29.7.0(@types/node@20.11.6) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -2657,7 +2660,7 @@ packages: '@babel/core': 7.23.5 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0 + jest: 29.7.0(@types/node@20.11.6) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 diff --git a/src/parser/binding-power.ts b/src/parser/binding-power.ts new file mode 100644 index 0000000..3f4eb64 --- /dev/null +++ b/src/parser/binding-power.ts @@ -0,0 +1,45 @@ +export type BindingPower = number; + +export type BindingPowerEntry = { left: BindingPower, right: BindingPower }; +export type BindingPowers = { [key: string]: BindingPowerEntry }; + +export const bindingPowers: BindingPowers = { + lowest: { left: 0, right: 1 }, + assignment: { left: 31, right: 30 }, + comparison: { left: 41, right: 40 }, + summative: { left: 50, right: 51 }, + productive: { left: 60, right: 61 }, + prefix: { left: 70, right: 71 }, + call: { left: 80, right: 81 }, +}; + +export const getInfixBindingPower = (infix: string): BindingPowerEntry => { + switch (infix) { + case "=": + return bindingPowers.assignment; + + case "==": + case "!=": + case ">": + case "<": + case ">=": + case "<=": + return bindingPowers.comparison; + + case "+": + case "-": + return bindingPowers.summative; + + case "*": + case "/": + return bindingPowers.productive; + + // for function call, it behaves an infix operator which lies between + // function expression and parameter list, e.g, print("hello") + case "(": + return bindingPowers.call; + + default: + return bindingPowers.lowest; + } +}; diff --git a/src/parser/index.ts b/src/parser/index.ts index 78ea614..97fba0f 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -67,6 +67,7 @@ const getBindingPower = (infix: string): BindingPower => { } }; +/** @deprecated */ export default class Parser { private buffer: TokenReader; diff --git a/src/parser/syntax-node/base/index.ts b/src/parser/syntax-node/base/index.ts index 68e77c5..dc1fa6e 100644 --- a/src/parser/syntax-node/base/index.ts +++ b/src/parser/syntax-node/base/index.ts @@ -1,25 +1,37 @@ import type Position from "../../../util/position"; +export interface Range { + readonly begin: Position, + readonly end: Position, +}; + export interface SyntaxNodeBase { - type: T, - range: { - begin: Position, - end: Position, - }, - fields: F, + readonly type: T, + readonly range: Range, + readonly fields: F, }; export function createNodeCreator(type: N["type"]) { - function createNode(fields: N["fields"], rangeBegin: Position, rangeEnd: Position) { - const range = { - begin: rangeBegin, - end: rangeEnd, - }; + type Node = { type: N["type"], range: N["range"], fields: N["fields"] }; + + function createNode(fields: N["fields"], range: Range): Node; + function createNode(fields: N["fields"], rangeBegin: Position, rangeEnd: Position): Node; + function createNode(fields: N["fields"], arg1: Range | Position, rangeEnd?: Position): Node { + if (rangeEnd !== undefined) { + const range = { + begin: arg1 as Position, + end: rangeEnd, + }; + + return { type, range, fields }; + } - return { type, range, fields }; + return { type, range: arg1 as Range, fields }; }; return createNode; }; -export type CreateNode = (fields: N["fields"], rangeBegin: Position, rangeEnd: Position) => N; +declare function createNode(fields: N["fields"], range: Range): N; +declare function createNode(fields: N["fields"], rangeBegin: Position, rangeEnd: Position): N; +export type CreateNode = typeof createNode; diff --git a/src/parser/syntax-node/expression/index.ts b/src/parser/syntax-node/expression/index.ts index 6250ee7..8f7fc9a 100644 --- a/src/parser/syntax-node/expression/index.ts +++ b/src/parser/syntax-node/expression/index.ts @@ -27,6 +27,7 @@ export interface AssignmentNode extends SyntaxNodeBase<"assignment", { left: Ide export const createIdentifierNode: CreateNode = createNodeCreator("identifier"); export const createNumberNode: CreateNode = createNodeCreator("number"); +export const createBooleanNode: CreateNode = createNodeCreator("boolean"); export const createStringNode: CreateNode = createNodeCreator("string"); export const createPrefixNode: CreateNode = createNodeCreator("prefix"); export const createInfixNode: CreateNode = createNodeCreator("infix"); diff --git a/src/parser/syntax-node/group/index.ts b/src/parser/syntax-node/group/index.ts index 77a24ef..5a2d20b 100644 --- a/src/parser/syntax-node/group/index.ts +++ b/src/parser/syntax-node/group/index.ts @@ -1,12 +1,13 @@ import type { SyntaxNodeBase, CreateNode } from "../base"; import { createNodeCreator } from "../base"; +import type { StatementNode } from "../statement"; export type GroupNode = ProgramNode | BlockNode; /** a root node for a syntax tree of a program */ -export interface ProgramNode extends SyntaxNodeBase<"program", { statements: any[] }> {}; +export interface ProgramNode extends SyntaxNodeBase<"program", { statements: StatementNode[] }> {}; /** a group of statements */ -export interface BlockNode extends SyntaxNodeBase<"block", { statements: any[] }> {}; +export interface BlockNode extends SyntaxNodeBase<"block", { statements: StatementNode[] }> {}; export const createProgramNode: CreateNode = createNodeCreator("program"); export const createBlockNode: CreateNode = createNodeCreator("block"); diff --git a/src/parser/v2.test.ts b/src/parser/v2.test.ts new file mode 100644 index 0000000..3bb902f --- /dev/null +++ b/src/parser/v2.test.ts @@ -0,0 +1,1374 @@ +import Lexer from "../lexer"; +import Parser from "./v2"; +import { + ParserError, + BadPrefixError, + BadExpressionError, +} from "./v2"; +import type { + ProgramNode, + AssignmentNode, + IdentifierNode, + ExpressionStatementNode, +} from "./syntax-node"; + +type SuccessTestCase = { name: string, input: string, expected: E }; +type FailureTestCase = { name: string, input: string, expected: E }; + +describe("parseSource()", () => { + const createParser = (input: string) => { + const lexer = new Lexer(input); + const parser = new Parser(lexer); + + return parser; + }; + + const testSuccess = ({ input, expected }: { input: string, expected: ProgramNode }) => { + const parser = createParser(input); + + const node = parser.parseSource(); + + expect(node).toMatchObject(expected); + }; + + const testFailure = ({ input, expected }: { input: string, expected: typeof ParserError }) => { + const parser = createParser(input); + + expect(() => parser.parseSource()).toThrow(expected); + }; + + describe("creating nodes", () => { + describe("literal expressions", () => { + const cases: SuccessTestCase[] = [ + { + name: "a number literal", + input: "42", + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "number", + fields: { + value: 42, + }, + } + }, + }, + ], + }, + }, + }, + { + name: "a boolean literal", + input: "참", + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "boolean", + fields: { + value: true, + }, + } + }, + }, + ], + }, + }, + }, + { + name: "a string literal", + input: "'foo bar'", + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "string", + fields: { + value: "foo bar", + }, + } + }, + }, + ], + }, + }, + }, + { + name: "a identifer literal", + input: "foo", + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "identifier", + fields: { + value: "foo", + }, + } + }, + }, + ], + }, + }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + + describe("arithmetic expressions", () => { + describe("single number", () => { + const cases: SuccessTestCase[] = [ + { + name: "positive number", + input: "+42", + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "prefix", + fields: { + prefix: "+", + right: { + type: "number", + fields: { + value: 42, + }, + }, + }, + } + }, + }, + ], + }, + }, + }, + { + name: "negative number", + input: "-42", + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "prefix", + fields: { + prefix: "-", + right: { + type: "number", + fields: { + value: 42, + }, + }, + }, + } + }, + }, + ], + }, + }, + }, + { + name: "doubly negative number", + input: "--42", + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "prefix", + fields: { + prefix: "-", + right: { + type: "prefix", + fields: { + prefix: "-", + right: { + type: "number", + fields: { + value: 42, + }, + }, + }, + }, + }, + } + }, + }, + ], + }, + }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + + describe("left associativity", () => { + const leftAssocCases = [ + { infix: "+", name: "left associative addition" }, + { infix: "-", name: "left associative subtraction" }, + { infix: "*", name: "left associative multiplication" }, + { infix: "/", name: "left associative division" }, + ]; + const leftAssocTestCases: SuccessTestCase[] = leftAssocCases.map(({ infix, name }) => ({ + name, + input: `11 ${infix} 22 ${infix} 33`, + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "infix", + fields: { + infix, + left: { + type: "infix", + fields: { + infix, + left: { + type: "number", + fields: { + value: 11, + }, + }, + right: { + type: "number", + fields: { + value: 22, + }, + }, + }, + }, + right: { + type: "number", + fields: { + value: 33, + }, + }, + }, + } + }, + }, + ], + }, + }, + })); + + it.each(leftAssocTestCases)("$name", testSuccess); + }); + + describe("associativity among different operations", () => { + const cases: SuccessTestCase[] = [ + { + name: "four operations", + input: "11+22*33/44-55", + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "infix", + fields: { + infix: "-", + left: { + type: "infix", + fields: { + infix: "+", + left: { + type: "number", + fields: { value: 11 }, + }, + right: { + type: "infix", + fields: { + infix: "/", + left: { + type: "infix", + fields: { + infix: "*", + left: { + type: "number", + fields: { + value: 22 + }, + }, + right: { + type: "number", + fields: { + value: 33 + }, + }, + }, + }, + right: { + type: "number", + fields: { + value: 44 + }, + }, + }, + }, + }, + }, + right: { + type: "number", + fields: { + value: 55, + }, + }, + }, + } + }, + }, + ], + }, + }, + }, + { + name: "with grouped", + input: "11+(22+33)", + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "infix", + fields: { + infix: "+", + left: { + type: "number", + fields: { + value: 11, + }, + }, + right: { + type: "infix", + fields: { + infix: "+", + left: { + type: "number", + fields: { + value: 22, + }, + }, + right: { + type: "number", + fields: { + value: 33, + }, + }, + }, + }, + }, + } + }, + }, + ], + }, + }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + + }); + + describe("logical expressions", () => { + describe("unary operation", () => { + const cases: SuccessTestCase[] = [ + { + name: "negation expression", + input: "!foo", + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "prefix", + fields: { + prefix: "!", + right: { + type: "identifier", + fields: { + value: "foo", + }, + }, + }, + } + }, + }, + ], + }, + }, + }, + { + name: "double negation expression", + input: "!!foo", + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "prefix", + fields: { + prefix: "!", + right: { + type: "prefix", + fields: { + prefix: "!", + right: { + type: "identifier", + fields: { + value: "foo", + }, + }, + }, + }, + }, + }, + }, + }, + ], + }, + }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + + describe("binary operation", () => { + const infixCases = [ + { name: "equal-to expression", infix: "==" }, + { name: "not-equal-to expression", infix: "!=" }, + { name: "greater-than expression", infix: ">" }, + { name: "less-than expression", infix: "<" }, + { name: "greater-than-or-equal-to expression", infix: ">=" }, + { name: "less-than-or-equal-to expression", infix: "<=" }, + ]; + const infixTestCases: SuccessTestCase[] = infixCases.map(({ name, infix }) => ({ + name, + input: `foo ${infix} bar`, + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "infix", + fields: { + infix, + left: { + type: "identifier", + fields: { + value: "foo", + }, + }, + right: { + type: "identifier", + fields: { + value: "bar", + }, + }, + }, + }, + }, + }, + ], + }, + }, + })); + + it.each(infixTestCases)("$name", testSuccess); + }); + + describe("right associativity", () => { + const infixCases = [ + { name: "right associative equal-to expression", infix: "==" }, + { name: "right associative not-equal-to expression", infix: "!=" }, + ]; + const infixTestCases: SuccessTestCase[] = infixCases.map(({ name, infix }) => ({ + name, + input: `foo ${infix} bar ${infix} baz`, + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "infix", + fields: { + infix, + left: { + type: "identifier", + fields: { + value: "foo", + }, + }, + right: { + type: "infix", + fields: { + infix, + left: { + type: "identifier", + fields: { + value: "bar", + }, + }, + right: { + type: "identifier", + fields: { + value: "baz", + }, + }, + }, + }, + }, + }, + }, + }, + ], + }, + }, + })); + + it.each(infixTestCases)("$name", testSuccess); + }); + + describe("grouped expression", () => { + const cases: SuccessTestCase[] = [ + { + name: "equal-to and not-equal-to", + input: "(foo == bar) != baz", + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "infix", + fields: { + infix: "!=", + left: { + type: "infix", + fields: { + infix: "==", + left: { + type: "identifier", + fields: { + value: "foo", + }, + }, + right: { + type: "identifier", + fields: { + value: "bar", + }, + }, + }, + }, + right: { + type: "identifier", + fields: { + value: "baz" + }, + }, + }, + }, + }, + }, + ], + }, + }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + }); + + describe("assignment", () => { + const cases: SuccessTestCase[] = [ + { + name: "a single assignment statement", + input: "x = 42", + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "assignment", + fields: { + left: { + type: "identifier", + fields: { + value: "x", + }, + }, + right: { + type: "number", + fields: { + value: 42, + }, + }, + }, + }, + }, + }, + ], + }, + }, + }, + { + name: "right associative assignment", + input: "x = y = 42", + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "assignment", + fields: { + left: { + type: "identifier", + fields: { + value: "x", + }, + }, + right: { + type: "assignment", + fields: { + left: { + type: "identifier", + fields: { + value: "y", + }, + }, + right: { + type: "number", + fields: { + value: 42, + }, + }, + }, + }, + }, + } + }, + }, + ], + }, + }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + + describe("call expressions", () => { + const cases: SuccessTestCase[] = [ + { + name: "call function with identifier", + input: "foo(bar, 42)", + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "call", + fields: { + func: { + type: "identifier", + fields: { + value: "foo", + }, + }, + args: [ + { + type: "identifier", + fields: { + value: "bar", + }, + }, + { + type: "number", + fields: { + value: 42, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }, + { + name: "call function with function literal", + input: "함수(foo){ foo }(42)", + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "call", + fields: { + func: { + type: "function", + fields: { + parameters: {}, // omit + body: {}, // omit + }, + }, + args: [ + { + type: "number", + fields: { + value: 42, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + + describe("function expressions", () => { + const cases: SuccessTestCase[] = [ + { + name: "function expression with parameters", + input: "함수 (foo, bar) { foo + bar }", + expected: { + type: "program", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "function", + fields: { + parameters: [ + { + type: "identifier", + fields: { + value: "foo", + }, + }, + { + type: "identifier", + fields: { + value: "bar", + }, + }, + ], + body: { + type: "block", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "infix", + fields: {}, // omit + }, + }, + }, + ], + }, + }, + }, + }, + }, + }, + ], + }, + }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + describe("return statement", () => { + const cases: SuccessTestCase[] = [ + { + name: "return number literal", + input: "결과 42", + expected: { + type: "program", + fields: { + statements: [ + { + type: "return", + fields: { + expression: { + type: "number", + fields: { + value: 42, + }, + }, + }, + }, + ], + }, + }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + + describe("branch statement", () => { + const cases: SuccessTestCase[] = [ + { + name: "predicate and consequence", + input: "만약 foo { bar }", + expected: { + type: "program", + fields: { + statements: [ + { + type: "branch", + fields: { + predicate: { + type: "identifier", + fields: { + value: "foo", + }, + }, + consequence: { + type: "block", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "identifier", + fields: { + value: "bar", + }, + }, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }, + { + name: "predicate and consequence with alternative", + input: "만약 foo { bar } 아니면 { baz }", + expected: { + type: "program", + fields: { + statements: [ + { + type: "branch", + fields: { + predicate: { + type: "identifier", + fields: { + value: "foo", + }, + }, + consequence: { + type: "block", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "identifier", + fields: { + value: "bar", + }, + }, + }, + }, + ], + }, + }, + alternative: { + type: "block", + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "identifier", + fields: { + value: "baz", + }, + }, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + }); + + describe("marking positions", () => { + describe("single statements", () => { + describe("literal expressions", () => { + const literalCases = [ + { + name: "number literal", + type: "number", + input: "12345", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 4 }, + }, + }, + { + name: "string literal", + type: "string", + input: "'foo bar'", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, + }, + }, + { + name: "boolean literal", + type: "boolean", + input: "거짓", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, + }, + { + name: "identifier literal", + type: "identifier", + input: "foo", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 2 }, + }, + }, + ]; + const cases: SuccessTestCase[] = literalCases.map(({ name, input, range, type }) => ({ + name, + input, + expected: { + type: "program", + range, + fields: { + statements: [ + { + type: "expression statement", + range, + fields: { + expression: { + type, + range, + } + }, + }, + ], + }, + }, + })); + + it.each(cases)("$name", testSuccess); + }); + + describe("single expressions", () => { + const cases: SuccessTestCase[] = [ + { + name: "assignment", + input: "x = 42", + expected: { + type: "program", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 5 }, + }, + fields: { + statements: [ + { + type: "expression statement", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 5 }, + }, + fields: { + expression: { + type: "assignment", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 5 }, + }, + fields: { + left: { + type: "identifier", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + right: { + type: "number", + range: { + begin: { row: 0, col: 4 }, + end: { row: 0, col: 5 }, + }, + }, + }, + }, + }, + }, + ], + }, + }, + }, + { + name: "arithmetic expression", + input: "11 + 22", + expected: { + type: "program", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 6 }, + }, + fields: { + statements: [ + { + type: "expression statement", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 6 }, + }, + fields: { + expression: { + type: "infix", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 6 }, + }, + fields: { + left: { + type: "number", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, + }, + right: { + type: "number", + range: { + begin: { row: 0, col: 5 }, + end: { row: 0, col: 6 }, + }, + }, + }, + }, + }, + }, + ], + }, + }, + }, + { + name: "grouped expression", + input: "(11 + 22)", + expected: { + type: "program", + range: { + }, + fields: { + statements: [ + { + type: "expression statement", + range: { + }, + fields: { + expression: { + type: "infix", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, + }, + fields: { + left: { + type: "number", + range: { + begin: { row: 0, col: 1 }, + end: { row: 0, col: 2 }, + }, + }, + right: { + type: "number", + range: { + begin: { row: 0, col: 6 }, + end: { row: 0, col: 7 }, + }, + }, + }, + }, + }, + }, + ], + }, + }, + }, + { + name: "function expression", + input: "함수(foo) {\n foo\n}", + expected: { + type: "program", + range: { + begin: { + row: 0, + col: 0, + }, + end: { + row: 2, + col: 0, + }, + }, + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "function", + range: { + begin: { + row: 0, + col: 0, + }, + end: { + row: 2, + col: 0, + }, + }, + fields: { + body: { + type: "block", + range: { + begin: { + row: 0, + col: 8, + }, + end: { + row: 2, + col: 0, + }, + }, + }, + }, + }, + }, + }, + ], + }, + }, + }, + { + name: "call expression", + input: "foo(bar, baz)", + expected: { + type: "program", + range: { + }, + fields: { + statements: [ + { + type: "expression statement", + fields: { + expression: { + type: "call", + range: { + begin: { + row: 0, + col: 0, + }, + end: { + row: 0, + col: 12, + }, + }, + }, + }, + }, + ], + }, + }, + }, + ]; + + it.each(cases.slice(4))("$name", testSuccess); + }); + + describe("single statements", () => { + const cases: SuccessTestCase[] = [ + { + name: "branch statement", + input: "만약 foo {\n 11\n} 아니면 {\n 22\n}", + expected: { + type: "program", + range: { + }, + fields: { + statements: [ + { + type: "branch", + range: { + }, + fields: { + predicate: { + range: { + begin: { + row: 0, + col: 3, + }, + end: { + row: 0, + col: 5, + }, + }, + }, + consequence: { + type: "block", + range: { + begin: { + row: 0, + col: 7, + }, + end: { + row: 2, + col: 0, + }, + }, + }, + alternative: { + type: "block", + range: { + begin: { + row: 2, + col: 6, + }, + end: { + row: 4, + col: 0, + }, + }, + }, + }, + }, + ], + }, + }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + }); + }); + + describe("error handling", () => { + const cases: FailureTestCase[] = [ + { + name: "not parsable expression start", + input: "*3", + expected: BadExpressionError, + } + ]; + + it.each(cases)("$name", testFailure); + }); +}); diff --git a/src/parser/v2.ts b/src/parser/v2.ts new file mode 100644 index 0000000..4df21f6 --- /dev/null +++ b/src/parser/v2.ts @@ -0,0 +1,397 @@ +import type * as Node from "./syntax-node"; +import * as node from "./syntax-node"; +import { getInfixBindingPower, bindingPowers, type BindingPowerEntry } from "./binding-power"; + +import { copyRange, type Range } from "../util/position"; + +export class ParserError extends Error { + public received: string; + public expected: string; + public range: { begin: { col: number, row: number }, end: { col: number, row: number }}; + + constructor(received: string, expected: string, range: Range) { + super(); + this.received = received; + this.expected = expected; + this.range = range; + } +}; + +export class BadNumberLiteralError extends ParserError {}; +export class BadBooleanLiteralError extends ParserError {}; +export class BadPrefixError extends ParserError {}; +export class BadInfixError extends ParserError {}; +export class BadExpressionError extends ParserError {}; +export class BadGroupDelimiterError extends ParserError {}; +export class BadBlockDelimiterError extends ParserError {}; +export class BadAssignmentError extends ParserError {}; +export class BadFunctionKeywordError extends ParserError {}; +export class BadIdentifierError extends ParserError {}; +export class BadSeparatorError extends ParserError {}; + +import Lexer from "../lexer"; +import SourceTokenReader from "./source-token-reader"; + +type PrefixOperator = "+" | "-" | "!"; +type InfixOperator = "+" | "-" | "*" | "/" | "!=" | "==" | ">" | "<" | ">=" | "<="; + +export default class Parser { + private static readonly PREFIX_OPERATORS = ["+", "-", "!"] as const; + private static readonly INFIX_OPERATORS = ["+", "-", "*", "/", "!=", "==", ">", "<", ">=", "<="] as const; + + private reader: SourceTokenReader; + + constructor(lexer: Lexer) { + this.reader = new SourceTokenReader(lexer); + } + + parseSource(): Node.ProgramNode { + const statements: Node.StatementNode[] = []; + + while (!this.reader.isEnd()) { + statements.push(this.parseStatement()); + } + + const firstPos = { row: 0, col: 0 }; + const posBegin = statements.length > 0 ? statements[0].range.begin : firstPos; + const posEnd = statements.length > 0 ? statements[statements.length-1].range.end : firstPos; + + const program = node.createProgramNode({ statements }, posBegin, posEnd); + + return program; + } + + private parseBlock(): Node.BlockNode { + const firstToken = this.reader.read(); + this.advanceOrThrow("block delimiter", "{", BadBlockDelimiterError); + + const statements: Node.StatementNode[] = []; + while (true) { + const token = this.reader.read(); + if (token.type === "block delimiter" && token.value === "}") { + this.reader.advance(); + + const range = copyRange(firstToken.range.begin, token.range.end); + return node.createBlockNode({ statements }, range); + } + + const statement = this.parseStatement(); + statements.push(statement); + } + } + + private parseStatement(): Node.StatementNode { + const token = this.reader.read(); + const { type, value } = token; + + if (type === "keyword" && value === "만약") { + return this.parseBranchStatement(); + } + + if (type === "keyword" && value === "결과") { + return this.parseReturnStatement(); + } + + return this.parseExpressionStatement(); + } + + private parseBranchStatement(): Node.BranchNode { + const { range } = this.reader.read(); + this.reader.advance(); + + const predicate = this.parseExpression(bindingPowers.lowest); + const consequence = this.parseBlock(); + + const maybeElseToken = this.reader.read(); + if (maybeElseToken.type !== "keyword" || maybeElseToken.value !== "아니면") { + return node.createBranchNode({ predicate, consequence }, range); + } + this.reader.advance(); + + const alternative = this.parseBlock(); + return node.createBranchNode({ predicate, consequence, alternative }, range); + } + + private parseReturnStatement(): Node.ReturnNode { + const { range } = this.reader.read(); + this.reader.advance(); + + const expression = this.parseExpression(bindingPowers.lowest); + return node.createReturnNode({ expression }, range); + } + + /** return expression statement node, which is just a statement wrapper for an expression */ + private parseExpressionStatement(): Node.ExpressionStatementNode { + const expression = this.parseExpression(bindingPowers.lowest); + + const range = expression.range; + return node.createExpressionStatementNode({ expression }, range); + } + + private parseExpression(threshold: BindingPowerEntry): Node.ExpressionNode { + let topNode = this.parseExpressionStart(); + + while (true) { + const nextBindingPower = getInfixBindingPower(this.reader.read().value); + if (nextBindingPower.left <= threshold.right) { + break; + } + + const infixExpression = this.parseExpressionMiddle(topNode); + if (infixExpression === null) { + break; + } + + topNode = infixExpression; + } + + return topNode; + } + + private parseExpressionStart(): Node.ExpressionNode { + const { type, value, range } = this.reader.read(); + + if (type === "number literal") { + return this.parseNumberLiteral(); + } + if (type === "boolean literal") { + return this.parseBooleanLiteral(); + } + if (type === "string literal") { + return this.parseStringLiteral(); + } + if (type === "identifier") { + return this.parseIdentifier(); + } + if (type === "operator" && this.isPrefixOperator(value)) { + return this.parsePrefix(); + } + if (type === "keyword" && value === "함수") { + return this.parseFunction(); + } + if (type === "group delimiter" && value === "(") { + return this.parseGroupedExpression(); + } + + throw new BadExpressionError(type, "expression", range); + } + + /** return node if parsable; null otherwise **/ + private parseExpressionMiddle(left: Node.ExpressionNode): Node.ExpressionNode | null { + const { type, value } = this.reader.read(); + + if (type === "group delimiter" && value === "(") { + if (left.type !== "function" && left.type !== "identifier") { + return null; + } + + return this.parseCall(left); + } + + if (type === "operator" && this.isInfixOperator(value)) { + return this.parseInfix(left); + } + + if (type === "operator" && value === "=" && left.type === "identifier") { + return this.parseAssignment(left); + } + + return null; + } + + private parseCall(left: Node.FunctionNode | Node.IdentifierNode): Node.CallNode { + this.advanceOrThrow("group delimiter", "(", BadGroupDelimiterError); + + const secondToken = this.reader.read(); + if (secondToken.type === "group delimiter" && secondToken.value === ")") { + this.reader.advance(); // eat delimiter + + const range = copyRange(left.range.begin, secondToken.range.end); + return node.createCallNode({ func: left, args: [] }, range); + } + + const args = [this.parseExpression(bindingPowers.lowest)]; + while (true) { + const token = this.reader.read(); + if (token.type !== "separator") { + break; + } + this.reader.advance(); // eat comma + + args.push(this.parseExpression(bindingPowers.lowest)); + } + + const lastToken = this.reader.read(); + this.advanceOrThrow("group delimiter", ")", BadGroupDelimiterError); + + const range = copyRange(left.range.begin, lastToken.range.end); + + return node.createCallNode({ func: left, args }, range); + } + + private parseAssignment(left: Node.IdentifierNode): Node.ExpressionNode { + const { value, range } = this.reader.read(); + this.reader.advance(); + + if (value !== "=") { + throw new BadAssignmentError(value, "=", range); + } + const infix = value; + + const infixBindingPower = getInfixBindingPower(infix); + const right = this.parseExpression(infixBindingPower); + const assignmentRange = { begin: left.range.begin, end: right.range.end }; + + return node.createAssignmentNode({ left, right }, assignmentRange); + } + + private parseNumberLiteral(): Node.NumberNode { + const { value, range } = this.reader.read(); + this.reader.advance(); + + const parsed = Number(value); + if (Number.isNaN(parsed)) { + throw new BadNumberLiteralError(value, "non NaN", range); + } + + return node.createNumberNode({ value: parsed }, range); + } + + private parseBooleanLiteral(): Node.BooleanNode { + const { value, range } = this.reader.read(); + this.reader.advance(); + + let parsed: boolean; + if (value === "참") { + parsed = true; + } else if (value === "거짓") { + parsed = false; + } else { + throw new BadBooleanLiteralError(value, "참, 거짓", range); + } + + return node.createBooleanNode({ value: parsed }, range); + } + + private parseStringLiteral(): Node.StringNode { + const { value, range } = this.reader.read(); + this.reader.advance(); + + return node.createStringNode({ value }, range); + } + + private parseIdentifier(): Node.IdentifierNode { + const { type, value, range } = this.reader.read(); + this.reader.advance(); + + if (type !== "identifier") { + throw new BadIdentifierError(type, "identifier", range); + } + + return node.createIdentifierNode({ value }, range); + } + + private parsePrefix(): Node.PrefixNode { + const { value, range } = this.reader.read(); + this.reader.advance(); + + if (!this.isPrefixOperator(value)) { + throw new BadPrefixError(value, "prefix operator", range); + } + + const prefix = value; + const right = this.parseExpression(bindingPowers.prefix); + + return node.createPrefixNode({ prefix, right }, range); + } + + private parseInfix(left: Node.ExpressionNode): Node.InfixNode { + const { value, range } = this.reader.read(); + this.reader.advance(); + + if (!this.isInfixOperator(value)) { + throw new BadInfixError(value, "infix operator", range); + } + const infix = value; + + const infixBindingPower = getInfixBindingPower(infix); + const right = this.parseExpression(infixBindingPower); + const infixRange = copyRange(left.range.begin, right.range.end); + + return node.createInfixNode({ infix, left, right }, infixRange); + } + + private parseFunction(): Node.FunctionNode { + const firstToken = this.reader.read(); + + this.advanceOrThrow("keyword", "함수", BadFunctionKeywordError); + + const parameters = this.parseParameters(); + const body = this.parseBlock(); + + const range = copyRange(firstToken.range.begin, body.range.end); + return node.createFunctionNode({ parameters, body }, range); + } + + private parseParameters(): Node.IdentifierNode[] { + this.advanceOrThrow("group delimiter", "(", BadGroupDelimiterError); + + const groupEndOrIdentifier = this.reader.read(); + + // early return if empty parameter list + if (groupEndOrIdentifier.type === "group delimiter" && groupEndOrIdentifier.value === ")") { + this.reader.advance(); + return []; + } + + const parameters = [this.parseIdentifier()]; + + while (true) { + const commaOrGroupEnd = this.reader.read(); + this.reader.advance(); + + if (commaOrGroupEnd.type === "group delimiter" && commaOrGroupEnd.value === ")") { + return parameters; + } + + if (commaOrGroupEnd.type !== "separator") { + throw new BadSeparatorError(commaOrGroupEnd.type, ",", commaOrGroupEnd.range); + } + + parameters.push(this.parseIdentifier()); + } + } + + private parseGroupedExpression(): Node.ExpressionNode { + this.advanceOrThrow("group delimiter", "(", BadGroupDelimiterError); + + const expression = this.parseExpression(bindingPowers.lowest); + + this.advanceOrThrow("group delimiter", ")", BadGroupDelimiterError); + + // range change due to group delimiters + const offset = { begin: { row: 0, col: -1 }, end: { row: 0, col: 1 } }; + const range = copyRange(expression.range.begin, expression.range.end, offset); + + return { ...expression, range }; + } + + private advanceOrThrow(type: string, value: string, ErrorClass: typeof ParserError): void { + const token = this.reader.read(); + this.reader.advance(); + + if (token.type !== type || token.value !== value) { + throw new ErrorClass(token.value, value, token.range); + } + } + + private isPrefixOperator(operator: string): operator is PrefixOperator { + return Parser.PREFIX_OPERATORS.some(prefix => prefix === operator); + } + + private isInfixOperator(operator: string): operator is InfixOperator { + return Parser.INFIX_OPERATORS.some(infix => infix === operator); + } +}; + +export type * from "./syntax-node"; diff --git a/src/util/position/index.ts b/src/util/position/index.ts index c37a918..c2d6e4d 100644 --- a/src/util/position/index.ts +++ b/src/util/position/index.ts @@ -2,3 +2,22 @@ export default interface Position { readonly row: number, readonly col: number, } + +export interface Range { + readonly begin: Position, + readonly end: Position, +} + +export const copyPosition = (pos: Position, offset?: Position) => { + const row = pos.row + (offset?.row ?? 0); + const col = pos.col + (offset?.col ?? 0); + + return { row, col }; +} + +export const copyRange = (begin: Position, end: Position, offset?: Range) => { + const copiedBegin = copyPosition(begin, offset?.begin); + const copiedEnd = copyPosition(end, offset?.end); + + return { begin: copiedBegin, end: copiedEnd }; +} diff --git a/tsconfig.json b/tsconfig.json index 932814b..02de301 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "target": "es2015", "module": "commonjs", + "lib": ["es2022"], "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, From 3283c789401ce3092dfc56315530a7a91b810d2e Mon Sep 17 00:00:00 2001 From: Won-hee Cho <89635107+wcho21@users.noreply.github.com> Date: Wed, 31 Jan 2024 17:04:23 +0900 Subject: [PATCH 5/7] change type and interface (#52) --- .../char-reader/source-char/index.ts | 2 +- src/lexer/index.ts | 2 +- src/lexer/source-token/base/index.ts | 7 +- src/parser/syntax-node/base/index.ts | 39 +- .../syntax-node/expression/index.test.ts | 45 +- src/parser/syntax-node/expression/index.ts | 60 +- src/parser/syntax-node/group/index.test.ts | 9 +- src/parser/syntax-node/group/index.ts | 12 +- .../syntax-node/statement/index.test.ts | 23 +- src/parser/syntax-node/statement/index.ts | 20 +- src/parser/v2.test.ts | 1370 +++++++---------- src/parser/v2.ts | 2 +- src/util/position/index.ts | 2 +- 13 files changed, 632 insertions(+), 961 deletions(-) diff --git a/src/lexer/char-buffer/char-reader/source-char/index.ts b/src/lexer/char-buffer/char-reader/source-char/index.ts index 3577751..2a372cf 100644 --- a/src/lexer/char-buffer/char-reader/source-char/index.ts +++ b/src/lexer/char-buffer/char-reader/source-char/index.ts @@ -1,4 +1,4 @@ -import Position from "../../../../util/position"; +import type { Position } from "../../../../util/position"; export default interface SourceChar { value: string, diff --git a/src/lexer/index.ts b/src/lexer/index.ts index c315684..af6d69e 100644 --- a/src/lexer/index.ts +++ b/src/lexer/index.ts @@ -23,7 +23,7 @@ import type { StringLiteralToken, IllegalStringLiteralToken, } from "./source-token"; -import type Position from "../util/position"; +import type { Position } from "../util/position"; import { isDigit, isLetter, isWhitespace } from "./util"; export default class Lexer { diff --git a/src/lexer/source-token/base/index.ts b/src/lexer/source-token/base/index.ts index 92512f9..79f4abe 100644 --- a/src/lexer/source-token/base/index.ts +++ b/src/lexer/source-token/base/index.ts @@ -1,9 +1,4 @@ -import type Position from "../../../util/position"; - -export interface Range { - readonly begin: Position, - readonly end: Position, -}; +import type { Position, Range } from "../../../util/position"; export interface SourceTokenBase { readonly type: T, diff --git a/src/parser/syntax-node/base/index.ts b/src/parser/syntax-node/base/index.ts index dc1fa6e..5b6ec81 100644 --- a/src/parser/syntax-node/base/index.ts +++ b/src/parser/syntax-node/base/index.ts @@ -1,37 +1,30 @@ -import type Position from "../../../util/position"; +import type { Position, Range } from "../../../util/position"; +import { copyRange } from "../../../util/position"; -export interface Range { - readonly begin: Position, - readonly end: Position, -}; - -export interface SyntaxNodeBase { +export interface SyntaxNodeBase { readonly type: T, readonly range: Range, - readonly fields: F, }; -export function createNodeCreator(type: N["type"]) { - type Node = { type: N["type"], range: N["range"], fields: N["fields"] }; +type AdditionalFields> = Omit>; - function createNode(fields: N["fields"], range: Range): Node; - function createNode(fields: N["fields"], rangeBegin: Position, rangeEnd: Position): Node; - function createNode(fields: N["fields"], arg1: Range | Position, rangeEnd?: Position): Node { - if (rangeEnd !== undefined) { - const range = { - begin: arg1 as Position, - end: rangeEnd, - }; +export function createNodeCreator>(type: T) { + type Node = { type: T, range: Range } & AdditionalFields; - return { type, range, fields }; + function createNode(fields: AdditionalFields, range: Range): Node; + function createNode(fields: AdditionalFields, rangeBegin: Position, rangeEnd: Position): Node; + function createNode(fields: AdditionalFields, arg1: Range | Position, rangeEnd?: Position): Node { + if (rangeEnd !== undefined) { + return { type, range: copyRange(arg1 as Position, rangeEnd), ...fields }; } - return { type, range: arg1 as Range, fields }; + const range = arg1 as Range; + return { type, range: copyRange(range.begin, range.end), ...fields }; }; return createNode; }; -declare function createNode(fields: N["fields"], range: Range): N; -declare function createNode(fields: N["fields"], rangeBegin: Position, rangeEnd: Position): N; -export type CreateNode = typeof createNode; +declare function createNode>(fields: AdditionalFields, range: Range): N; +declare function createNode>(fields: AdditionalFields, rangeBegin: Position, rangeEnd: Position): N; +export type CreateNode> = typeof createNode; diff --git a/src/parser/syntax-node/expression/index.test.ts b/src/parser/syntax-node/expression/index.test.ts index 3f40b1c..03a520c 100644 --- a/src/parser/syntax-node/expression/index.test.ts +++ b/src/parser/syntax-node/expression/index.test.ts @@ -23,9 +23,7 @@ const cases = [ node: createIdentifierNode({ value: "foo" }, fakePos, fakePos), expected: { type: "identifier", - fields: { - value: "foo", - }, + value: "foo", range: { begin: fakePos, end: fakePos }, }, }, @@ -34,9 +32,7 @@ const cases = [ node: createNumberNode({ value: 42 }, fakePos, fakePos), expected: { type: "number", - fields: { - value: 42, - }, + value: 42, range: { begin: fakePos, end: fakePos }, }, }, @@ -45,9 +41,7 @@ const cases = [ node: createStringNode({ value: "foo" }, fakePos, fakePos), expected: { type: "string", - fields: { - value: "foo", - }, + value: "foo", range: { begin: fakePos, end: fakePos }, }, }, @@ -56,10 +50,8 @@ const cases = [ node: createPrefixNode({ prefix: "+", right: {} as ExpressionNode }, fakePos, fakePos), expected: { type: "prefix", - fields: { - prefix: "+", - right: {}, - }, + prefix: "+", + right: {}, range: { begin: fakePos, end: fakePos }, }, }, @@ -68,11 +60,9 @@ const cases = [ node: createInfixNode({ infix: "+", left: {} as ExpressionNode, right: {} as ExpressionNode }, fakePos, fakePos), expected: { type: "infix", - fields: { - infix: "+", - left: {}, - right: {}, - }, + infix: "+", + left: {}, + right: {}, range: { begin: fakePos, end: fakePos }, }, }, @@ -81,10 +71,8 @@ const cases = [ node: createFunctionNode({ parameters: [] as IdentifierNode[], body: {} as BlockNode }, fakePos, fakePos), expected: { type: "function", - fields: { - parameters: [], - body: {}, - }, + parameters: [], + body: {}, range: { begin: fakePos, end: fakePos }, }, }, @@ -93,10 +81,8 @@ const cases = [ node: createCallNode({ func: {} as IdentifierNode, args: [] as ExpressionNode[] }, fakePos, fakePos), expected: { type: "call", - fields: { - func: {}, - args: [], - }, + func: {}, + args: [], range: { begin: fakePos, end: fakePos }, }, }, @@ -105,14 +91,13 @@ const cases = [ node: createAssignmentNode({ left: {} as IdentifierNode, right: {} as ExpressionNode }, fakePos, fakePos), expected: { type: "assignment", - fields: { - left: {}, - right: {}, - }, + left: {}, + right: {}, range: { begin: fakePos, end: fakePos }, }, }, ]; + it.each(cases)("create $name node", ({ node, expected }) => { expect(node).toEqual(expected); }); diff --git a/src/parser/syntax-node/expression/index.ts b/src/parser/syntax-node/expression/index.ts index 8f7fc9a..9bfb3d9 100644 --- a/src/parser/syntax-node/expression/index.ts +++ b/src/parser/syntax-node/expression/index.ts @@ -15,22 +15,46 @@ export type ExpressionNode = IdentifierNode | CallNode | AssignmentNode; -export interface IdentifierNode extends SyntaxNodeBase<"identifier", { value: string }> {}; -export interface NumberNode extends SyntaxNodeBase<"number", { value: number }> {}; -export interface BooleanNode extends SyntaxNodeBase<"boolean", { value: boolean }> {}; -export interface StringNode extends SyntaxNodeBase<"string", { value: string }> {}; -export interface PrefixNode extends SyntaxNodeBase<"prefix", { prefix: Prefix, right: ExpressionNode }> {}; -export interface InfixNode extends SyntaxNodeBase<"infix", { infix: Infix, left: ExpressionNode, right: ExpressionNode }> {}; -export interface FunctionNode extends SyntaxNodeBase<"function", { parameters: IdentifierNode[], body: BlockNode }> {}; -export interface CallNode extends SyntaxNodeBase<"call", { func: IdentifierNode | FunctionNode, args: ExpressionNode[] }> {}; -export interface AssignmentNode extends SyntaxNodeBase<"assignment", { left: IdentifierNode, right: ExpressionNode }> {}; +export interface IdentifierNode extends SyntaxNodeBase<"identifier"> { + value: string, +}; +export interface NumberNode extends SyntaxNodeBase<"number"> { + value: number, +}; +export interface BooleanNode extends SyntaxNodeBase<"boolean"> { + value: boolean, +}; +export interface StringNode extends SyntaxNodeBase<"string"> { + value: string, +}; +export interface PrefixNode extends SyntaxNodeBase<"prefix"> { + prefix: Prefix, + right: ExpressionNode, +}; +export interface InfixNode extends SyntaxNodeBase<"infix"> { + infix: Infix, + left: ExpressionNode, + right: ExpressionNode, +}; +export interface FunctionNode extends SyntaxNodeBase<"function"> { + parameters: IdentifierNode[], + body: BlockNode, +}; +export interface CallNode extends SyntaxNodeBase<"call"> { + func: IdentifierNode | FunctionNode, + args: ExpressionNode[], +}; +export interface AssignmentNode extends SyntaxNodeBase<"assignment"> { + left: ExpressionNode, + right: ExpressionNode, +}; -export const createIdentifierNode: CreateNode = createNodeCreator("identifier"); -export const createNumberNode: CreateNode = createNodeCreator("number"); -export const createBooleanNode: CreateNode = createNodeCreator("boolean"); -export const createStringNode: CreateNode = createNodeCreator("string"); -export const createPrefixNode: CreateNode = createNodeCreator("prefix"); -export const createInfixNode: CreateNode = createNodeCreator("infix"); -export const createFunctionNode: CreateNode = createNodeCreator("function"); -export const createCallNode: CreateNode = createNodeCreator("call"); -export const createAssignmentNode: CreateNode = createNodeCreator("assignment"); +export const createIdentifierNode: CreateNode<"identifier", IdentifierNode> = createNodeCreator<"identifier", IdentifierNode>("identifier"); +export const createNumberNode: CreateNode<"number", NumberNode> = createNodeCreator<"number", NumberNode>("number"); +export const createBooleanNode: CreateNode<"boolean", BooleanNode> = createNodeCreator<"boolean", BooleanNode>("boolean"); +export const createStringNode: CreateNode<"string", StringNode> = createNodeCreator<"string", StringNode>("string"); +export const createPrefixNode: CreateNode<"prefix", PrefixNode> = createNodeCreator<"prefix", PrefixNode>("prefix"); +export const createInfixNode: CreateNode<"infix", InfixNode> = createNodeCreator<"infix", InfixNode>("infix"); +export const createFunctionNode: CreateNode<"function", FunctionNode> = createNodeCreator<"function", FunctionNode>("function"); +export const createCallNode: CreateNode<"call", CallNode> = createNodeCreator<"call", CallNode>("call"); +export const createAssignmentNode: CreateNode<"assignment", AssignmentNode> = createNodeCreator<"assignment", AssignmentNode>("assignment"); diff --git a/src/parser/syntax-node/group/index.test.ts b/src/parser/syntax-node/group/index.test.ts index 0187f3c..8231d98 100644 --- a/src/parser/syntax-node/group/index.test.ts +++ b/src/parser/syntax-node/group/index.test.ts @@ -10,9 +10,7 @@ const cases = [ node: createProgramNode({ statements: [] }, fakePos, fakePos), expected: { type: "program", - fields: { - statements: [], - }, + statements: [], range: { begin: fakePos, end: fakePos }, }, }, @@ -21,13 +19,12 @@ const cases = [ node: createBlockNode({ statements: [] }, fakePos, fakePos), expected: { type: "block", - fields: { - statements: [], - }, + statements: [], range: { begin: fakePos, end: fakePos }, }, }, ]; + it.each(cases)("create $name node", ({ node, expected }) => { expect(node).toEqual(expected); }); diff --git a/src/parser/syntax-node/group/index.ts b/src/parser/syntax-node/group/index.ts index 5a2d20b..099f83c 100644 --- a/src/parser/syntax-node/group/index.ts +++ b/src/parser/syntax-node/group/index.ts @@ -5,9 +5,13 @@ import type { StatementNode } from "../statement"; export type GroupNode = ProgramNode | BlockNode; /** a root node for a syntax tree of a program */ -export interface ProgramNode extends SyntaxNodeBase<"program", { statements: StatementNode[] }> {}; +export interface ProgramNode extends SyntaxNodeBase<"program"> { + statements: StatementNode[], +}; /** a group of statements */ -export interface BlockNode extends SyntaxNodeBase<"block", { statements: StatementNode[] }> {}; +export interface BlockNode extends SyntaxNodeBase<"block"> { + statements: StatementNode[], +}; -export const createProgramNode: CreateNode = createNodeCreator("program"); -export const createBlockNode: CreateNode = createNodeCreator("block"); +export const createProgramNode: CreateNode<"program", ProgramNode> = createNodeCreator<"program", ProgramNode>("program"); +export const createBlockNode: CreateNode<"block", BlockNode> = createNodeCreator<"block", BlockNode>("block"); diff --git a/src/parser/syntax-node/statement/index.test.ts b/src/parser/syntax-node/statement/index.test.ts index 9e873d4..2c824a5 100644 --- a/src/parser/syntax-node/statement/index.test.ts +++ b/src/parser/syntax-node/statement/index.test.ts @@ -17,10 +17,8 @@ const cases = [ node: createBranchNode({ predicate: {} as ExpressionNode, consequence: {} as BlockNode }, fakePos, fakePos), expected: { type: "branch", - fields: { - predicate: {}, - consequence: {}, - }, + predicate: {}, + consequence: {}, range: { begin: fakePos, end: fakePos }, }, }, @@ -29,11 +27,9 @@ const cases = [ node: createBranchNode({ predicate: {} as ExpressionNode, consequence: {} as BlockNode, alternative: {} as BlockNode }, fakePos, fakePos), expected: { type: "branch", - fields: { - predicate: {}, - consequence: {}, - alternative: {}, - }, + predicate: {}, + consequence: {}, + alternative: {}, range: { begin: fakePos, end: fakePos }, }, }, @@ -42,9 +38,7 @@ const cases = [ node: createReturnNode({ expression: {} as ExpressionNode }, fakePos, fakePos), expected: { type: "return", - fields: { - expression: {}, - }, + expression: {}, range: { begin: fakePos, end: fakePos }, }, }, @@ -53,13 +47,12 @@ const cases = [ node: createExpressionStatementNode({ expression: {} as ExpressionNode }, fakePos, fakePos), expected: { type: "expression statement", - fields: { - expression: {}, - }, + expression: {}, range: { begin: fakePos, end: fakePos }, }, }, ]; + it.each(cases)("create $name node", ({ node, expected }) => { expect(node).toEqual(expected); }); diff --git a/src/parser/syntax-node/statement/index.ts b/src/parser/syntax-node/statement/index.ts index 88b0fd5..72b19d8 100644 --- a/src/parser/syntax-node/statement/index.ts +++ b/src/parser/syntax-node/statement/index.ts @@ -5,11 +5,19 @@ import type { ExpressionNode } from "../expression"; export type StatementNode = BranchNode | ReturnNode | ExpressionStatementNode; -export interface BranchNode extends SyntaxNodeBase<"branch", { predicate: ExpressionNode, consequence: BlockNode, alternative?: BlockNode }> {}; -export interface ReturnNode extends SyntaxNodeBase<"return", { expression: ExpressionNode }> {}; +export interface BranchNode extends SyntaxNodeBase<"branch"> { + predicate: ExpressionNode, + consequence: BlockNode, + alternative?: BlockNode, +}; +export interface ReturnNode extends SyntaxNodeBase<"return"> { + expression: ExpressionNode +}; /** A wrapper type to treat a single expression as a statement. */ -export interface ExpressionStatementNode extends SyntaxNodeBase<"expression statement", { expression: ExpressionNode }> {}; +export interface ExpressionStatementNode extends SyntaxNodeBase<"expression statement"> { + expression: ExpressionNode +}; -export const createBranchNode: CreateNode = createNodeCreator("branch"); -export const createReturnNode: CreateNode = createNodeCreator("return"); -export const createExpressionStatementNode: CreateNode = createNodeCreator("expression statement"); +export const createBranchNode: CreateNode<"branch", BranchNode> = createNodeCreator<"branch", BranchNode>("branch"); +export const createReturnNode: CreateNode<"return", ReturnNode> = createNodeCreator<"return", ReturnNode>("return"); +export const createExpressionStatementNode: CreateNode<"expression statement", ExpressionStatementNode> = createNodeCreator<"expression statement", ExpressionStatementNode>("expression statement"); diff --git a/src/parser/v2.test.ts b/src/parser/v2.test.ts index 3bb902f..514f1e9 100644 --- a/src/parser/v2.test.ts +++ b/src/parser/v2.test.ts @@ -2,14 +2,10 @@ import Lexer from "../lexer"; import Parser from "./v2"; import { ParserError, - BadPrefixError, BadExpressionError, } from "./v2"; import type { ProgramNode, - AssignmentNode, - IdentifierNode, - ExpressionStatementNode, } from "./syntax-node"; type SuccessTestCase = { name: string, input: string, expected: E }; @@ -45,21 +41,15 @@ describe("parseSource()", () => { input: "42", expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "number", - fields: { - value: 42, - }, - } - }, - }, - ], - }, + statements: [ + { + type: "expression statement", + expression: { + type: "number", + value: 42, + } + }, + ], }, }, { @@ -67,21 +57,15 @@ describe("parseSource()", () => { input: "참", expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "boolean", - fields: { - value: true, - }, - } - }, - }, - ], - }, + statements: [ + { + type: "expression statement", + expression: { + type: "boolean", + value: true, + } + }, + ], }, }, { @@ -89,21 +73,15 @@ describe("parseSource()", () => { input: "'foo bar'", expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "string", - fields: { - value: "foo bar", - }, - } - }, + statements: [ + { + type: "expression statement", + expression: { + type: "string", + value: "foo bar", }, - ], - }, + }, + ], }, }, { @@ -111,21 +89,15 @@ describe("parseSource()", () => { input: "foo", expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "identifier", - fields: { - value: "foo", - }, - } - }, - }, - ], - }, + statements: [ + { + type: "expression statement", + expression: { + type: "identifier", + value: "foo", + } + }, + ], }, }, ]; @@ -141,27 +113,19 @@ describe("parseSource()", () => { input: "+42", expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "prefix", - fields: { - prefix: "+", - right: { - type: "number", - fields: { - value: 42, - }, - }, - }, - } + statements: [ + { + type: "expression statement", + expression: { + type: "prefix", + prefix: "+", + right: { + type: "number", + value: 42, }, - }, - ], - }, + } + }, + ], }, }, { @@ -169,27 +133,19 @@ describe("parseSource()", () => { input: "-42", expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "prefix", - fields: { - prefix: "-", - right: { - type: "number", - fields: { - value: 42, - }, - }, - }, - } + statements: [ + { + type: "expression statement", + expression: { + type: "prefix", + prefix: "-", + right: { + type: "number", + value: 42, }, - }, - ], - }, + } + }, + ], }, }, { @@ -197,33 +153,23 @@ describe("parseSource()", () => { input: "--42", expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "prefix", - fields: { - prefix: "-", - right: { - type: "prefix", - fields: { - prefix: "-", - right: { - type: "number", - fields: { - value: 42, - }, - }, - }, - }, - }, - } + statements: [ + { + type: "expression statement", + expression: { + type: "prefix", + prefix: "-", + right: { + type: "prefix", + prefix: "-", + right: { + type: "number", + value: 42, + }, }, }, - ], - }, + }, + ], }, }, ]; @@ -243,45 +189,31 @@ describe("parseSource()", () => { input: `11 ${infix} 22 ${infix} 33`, expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "infix", - fields: { - infix, - left: { - type: "infix", - fields: { - infix, - left: { - type: "number", - fields: { - value: 11, - }, - }, - right: { - type: "number", - fields: { - value: 22, - }, - }, - }, - }, - right: { - type: "number", - fields: { - value: 33, - }, - }, - }, - } + statements: [ + { + type: "expression statement", + expression: { + type: "infix", + infix, + left: { + type: "infix", + infix, + left: { + type: "number", + value: 11, + }, + right: { + type: "number", + value: 22, + }, + }, + right: { + type: "number", + value: 33, }, }, - ], - }, + }, + ], }, })); @@ -295,67 +227,47 @@ describe("parseSource()", () => { input: "11+22*33/44-55", expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { + statements: [ + { + type: "expression statement", + expression: { + type: "infix", + infix: "-", + left: { + type: "infix", + infix: "+", + left: { + type: "number", + value: 11, + }, + right: { type: "infix", - fields: { - infix: "-", + infix: "/", + left: { + type: "infix", + infix: "*", left: { - type: "infix", - fields: { - infix: "+", - left: { - type: "number", - fields: { value: 11 }, - }, - right: { - type: "infix", - fields: { - infix: "/", - left: { - type: "infix", - fields: { - infix: "*", - left: { - type: "number", - fields: { - value: 22 - }, - }, - right: { - type: "number", - fields: { - value: 33 - }, - }, - }, - }, - right: { - type: "number", - fields: { - value: 44 - }, - }, - }, - }, - }, + type: "number", + value: 22 }, right: { type: "number", - fields: { - value: 55, - }, + value: 33 }, }, - } + right: { + type: "number", + value: 44, + }, + }, + }, + right: { + type: "number", + value: 55, }, }, - ], - }, + }, + ], }, }, { @@ -363,45 +275,31 @@ describe("parseSource()", () => { input: "11+(22+33)", expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "infix", - fields: { - infix: "+", - left: { - type: "number", - fields: { - value: 11, - }, - }, - right: { - type: "infix", - fields: { - infix: "+", - left: { - type: "number", - fields: { - value: 22, - }, - }, - right: { - type: "number", - fields: { - value: 33, - }, - }, - }, - }, - }, - } + statements: [ + { + type: "expression statement", + expression: { + type: "infix", + infix: "+", + left: { + type: "number", + value: 11, + }, + right: { + type: "infix", + infix: "+", + left: { + type: "number", + value: 22, + }, + right: { + type: "number", + value: 33, + }, }, }, - ], - }, + }, + ], }, }, ]; @@ -419,27 +317,19 @@ describe("parseSource()", () => { input: "!foo", expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "prefix", - fields: { - prefix: "!", - right: { - type: "identifier", - fields: { - value: "foo", - }, - }, - }, - } + statements: [ + { + type: "expression statement", + expression: { + type: "prefix", + prefix: "!", + right: { + type: "identifier", + value: "foo", }, }, - ], - }, + }, + ], }, }, { @@ -447,33 +337,23 @@ describe("parseSource()", () => { input: "!!foo", expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "prefix", - fields: { - prefix: "!", - right: { - type: "prefix", - fields: { - prefix: "!", - right: { - type: "identifier", - fields: { - value: "foo", - }, - }, - }, - }, - }, + statements: [ + { + type: "expression statement", + expression: { + type: "prefix", + prefix: "!", + right: { + type: "prefix", + prefix: "!", + right: { + type: "identifier", + value: "foo", }, }, }, - ], - }, + }, + ], }, }, ]; @@ -495,33 +375,23 @@ describe("parseSource()", () => { input: `foo ${infix} bar`, expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "infix", - fields: { - infix, - left: { - type: "identifier", - fields: { - value: "foo", - }, - }, - right: { - type: "identifier", - fields: { - value: "bar", - }, - }, - }, - }, + statements: [ + { + type: "expression statement", + expression: { + type: "infix", + infix, + left: { + type: "identifier", + value: "foo", + }, + right: { + type: "identifier", + value: "bar", }, }, - ], - }, + }, + ], }, })); @@ -538,45 +408,31 @@ describe("parseSource()", () => { input: `foo ${infix} bar ${infix} baz`, expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "infix", - fields: { - infix, - left: { - type: "identifier", - fields: { - value: "foo", - }, - }, - right: { - type: "infix", - fields: { - infix, - left: { - type: "identifier", - fields: { - value: "bar", - }, - }, - right: { - type: "identifier", - fields: { - value: "baz", - }, - }, - }, - }, - }, + statements: [ + { + type: "expression statement", + expression: { + type: "infix", + infix, + left: { + type: "identifier", + value: "foo", + }, + right: { + type: "infix", + infix, + left: { + type: "identifier", + value: "bar", + }, + right: { + type: "identifier", + value: "baz", }, }, }, - ], - }, + }, + ], }, })); @@ -590,45 +446,31 @@ describe("parseSource()", () => { input: "(foo == bar) != baz", expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "infix", - fields: { - infix: "!=", - left: { - type: "infix", - fields: { - infix: "==", - left: { - type: "identifier", - fields: { - value: "foo", - }, - }, - right: { - type: "identifier", - fields: { - value: "bar", - }, - }, - }, - }, - right: { - type: "identifier", - fields: { - value: "baz" - }, - }, - }, + statements: [ + { + type: "expression statement", + expression: { + type: "infix", + infix: "!=", + left: { + type: "infix", + infix: "==", + left: { + type: "identifier", + value: "foo", + }, + right: { + type: "identifier", + value: "bar", }, }, + right: { + type: "identifier", + value: "baz", + }, }, - ], - }, + }, + ], }, }, ]; @@ -644,32 +486,22 @@ describe("parseSource()", () => { input: "x = 42", expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "assignment", - fields: { - left: { - type: "identifier", - fields: { - value: "x", - }, - }, - right: { - type: "number", - fields: { - value: 42, - }, - }, - }, - }, + statements: [ + { + type: "expression statement", + expression: { + type: "assignment", + left: { + type: "identifier", + value: "x", + }, + right: { + type: "number", + value: 42, }, }, - ], - }, + }, + ], }, }, { @@ -677,43 +509,29 @@ describe("parseSource()", () => { input: "x = y = 42", expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "assignment", - fields: { - left: { - type: "identifier", - fields: { - value: "x", - }, - }, - right: { - type: "assignment", - fields: { - left: { - type: "identifier", - fields: { - value: "y", - }, - }, - right: { - type: "number", - fields: { - value: 42, - }, - }, - }, - }, - }, - } + statements: [ + { + type: "expression statement", + expression: { + type: "assignment", + left: { + type: "identifier", + value: "x", + }, + right: { + type: "assignment", + left: { + type: "identifier", + value: "y", + }, + right: { + type: "number", + value: 42, + }, }, }, - ], - }, + }, + ], }, }, ]; @@ -728,40 +546,28 @@ describe("parseSource()", () => { input: "foo(bar, 42)", expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "call", - fields: { - func: { - type: "identifier", - fields: { - value: "foo", - }, - }, - args: [ - { - type: "identifier", - fields: { - value: "bar", - }, - }, - { - type: "number", - fields: { - value: 42, - }, - }, - ], - }, - }, + statements: [ + { + type: "expression statement", + expression: { + type: "call", + func: { + type: "identifier", + value: "foo", }, + args: [ + { + type: "identifier", + value: "bar", + }, + { + type: "number", + value: 42, + }, + ], }, - ], - }, + }, + ], }, }, { @@ -769,35 +575,25 @@ describe("parseSource()", () => { input: "함수(foo){ foo }(42)", expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "call", - fields: { - func: { - type: "function", - fields: { - parameters: {}, // omit - body: {}, // omit - }, - }, - args: [ - { - type: "number", - fields: { - value: 42, - }, - }, - ], - }, - }, + statements: [ + { + type: "expression statement", + expression: { + type: "call", + func: { + type: "function", + parameters: {}, // omit + body: {}, // omit }, + args: [ + { + type: "number", + value: 42, + }, + ], }, - ], - }, + }, + ], }, }, ]; @@ -812,56 +608,42 @@ describe("parseSource()", () => { input: "함수 (foo, bar) { foo + bar }", expected: { type: "program", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "function", - fields: { - parameters: [ - { - type: "identifier", - fields: { - value: "foo", - }, - }, - { - type: "identifier", - fields: { - value: "bar", - }, - }, - ], - body: { - type: "block", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "infix", - fields: {}, // omit - }, - }, - }, - ], - }, + statements: [ + { + type: "expression statement", + expression: { + type: "function", + parameters: [ + { + type: "identifier", + value: "foo", + }, + { + type: "identifier", + value: "bar", + }, + ], + body: { + type: "block", + statements: [ + { + type: "expression statement", + expression: { + type: "infix", }, }, - }, + ], }, }, - ], - }, + }, + ], }, }, ]; it.each(cases)("$name", testSuccess); }); + describe("return statement", () => { const cases: SuccessTestCase[] = [ { @@ -869,21 +651,15 @@ describe("parseSource()", () => { input: "결과 42", expected: { type: "program", - fields: { - statements: [ - { - type: "return", - fields: { - expression: { - type: "number", - fields: { - value: 42, - }, - }, - }, + statements: [ + { + type: "return", + expression: { + type: "number", + value: 42, }, - ], - }, + }, + ], }, }, ]; @@ -898,39 +674,27 @@ describe("parseSource()", () => { input: "만약 foo { bar }", expected: { type: "program", - fields: { - statements: [ - { - type: "branch", - fields: { - predicate: { - type: "identifier", - fields: { - value: "foo", - }, - }, - consequence: { - type: "block", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "identifier", - fields: { - value: "bar", - }, - }, - }, - }, - ], + statements: [ + { + type: "branch", + predicate: { + type: "identifier", + value: "foo", + }, + consequence: { + type: "block", + statements: [ + { + type: "expression statement", + expression: { + type: "identifier", + value: "bar", }, }, - }, + ], }, - ], - }, + }, + ], }, }, { @@ -938,57 +702,39 @@ describe("parseSource()", () => { input: "만약 foo { bar } 아니면 { baz }", expected: { type: "program", - fields: { - statements: [ - { - type: "branch", - fields: { - predicate: { - type: "identifier", - fields: { - value: "foo", - }, - }, - consequence: { - type: "block", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "identifier", - fields: { - value: "bar", - }, - }, - }, - }, - ], + statements: [ + { + type: "branch", + predicate: { + type: "identifier", + value: "foo", + }, + consequence: { + type: "block", + statements: [ + { + type: "expression statement", + expression: { + type: "identifier", + value: "bar", }, }, - alternative: { - type: "block", - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "identifier", - fields: { - value: "baz", - }, - }, - }, - }, - ], + ], + }, + alternative: { + type: "block", + statements: [ + { + type: "expression statement", + expression: { + type: "identifier", + value: "baz", }, }, - }, + ], }, - ], - }, + }, + ], }, }, ]; @@ -1044,20 +790,16 @@ describe("parseSource()", () => { expected: { type: "program", range, - fields: { - statements: [ - { - type: "expression statement", + statements: [ + { + type: "expression statement", + range, + expression: { + type, range, - fields: { - expression: { - type, - range, - } - }, }, - ], - }, + }, + ], }, })); @@ -1075,42 +817,36 @@ describe("parseSource()", () => { begin: { row: 0, col: 0 }, end: { row: 0, col: 5 }, }, - fields: { - statements: [ - { - type: "expression statement", + statements: [ + { + type: "expression statement", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 5 }, + }, + expression: { + type: "assignment", range: { begin: { row: 0, col: 0 }, end: { row: 0, col: 5 }, }, - fields: { - expression: { - type: "assignment", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 5 }, - }, - fields: { - left: { - type: "identifier", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 0 }, - }, - }, - right: { - type: "number", - range: { - begin: { row: 0, col: 4 }, - end: { row: 0, col: 5 }, - }, - }, - }, + left: { + type: "identifier", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + right: { + type: "number", + range: { + begin: { row: 0, col: 4 }, + end: { row: 0, col: 5 }, }, }, }, - ], - }, + }, + ], }, }, { @@ -1122,42 +858,36 @@ describe("parseSource()", () => { begin: { row: 0, col: 0 }, end: { row: 0, col: 6 }, }, - fields: { - statements: [ - { - type: "expression statement", + statements: [ + { + type: "expression statement", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 6 }, + }, + expression: { + type: "infix", range: { begin: { row: 0, col: 0 }, end: { row: 0, col: 6 }, }, - fields: { - expression: { - type: "infix", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 6 }, - }, - fields: { - left: { - type: "number", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 1 }, - }, - }, - right: { - type: "number", - range: { - begin: { row: 0, col: 5 }, - end: { row: 0, col: 6 }, - }, - }, - }, + left: { + type: "number", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, + }, + right: { + type: "number", + range: { + begin: { row: 0, col: 5 }, + end: { row: 0, col: 6 }, }, }, }, - ], - }, + }, + ], }, }, { @@ -1166,41 +896,39 @@ describe("parseSource()", () => { expected: { type: "program", range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, }, - fields: { - statements: [ - { - type: "expression statement", + statements: [ + { + type: "expression statement", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, + }, + expression: { + type: "infix", range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, }, - fields: { - expression: { - type: "infix", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 8 }, - }, - fields: { - left: { - type: "number", - range: { - begin: { row: 0, col: 1 }, - end: { row: 0, col: 2 }, - }, - }, - right: { - type: "number", - range: { - begin: { row: 0, col: 6 }, - end: { row: 0, col: 7 }, - }, - }, - }, + left: { + type: "number", + range: { + begin: { row: 0, col: 1 }, + end: { row: 0, col: 2 }, + }, + }, + right: { + type: "number", + range: { + begin: { row: 0, col: 6 }, + end: { row: 0, col: 7 }, }, }, }, - ], - }, + }, + ], }, }, { @@ -1209,52 +937,28 @@ describe("parseSource()", () => { expected: { type: "program", range: { - begin: { - row: 0, - col: 0, - }, - end: { - row: 2, - col: 0, - }, + begin: { row: 0, col: 0, }, + end: { row: 2, col: 0, }, }, - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "function", - range: { - begin: { - row: 0, - col: 0, - }, - end: { - row: 2, - col: 0, - }, - }, - fields: { - body: { - type: "block", - range: { - begin: { - row: 0, - col: 8, - }, - end: { - row: 2, - col: 0, - }, - }, - }, - }, + statements: [ + { + type: "expression statement", + expression: { + type: "function", + range: { + begin: { row: 0, col: 0, }, + end: { row: 2, col: 0, }, + }, + body: { + type: "block", + range: { + begin: { row: 0, col: 8, }, + end: { row: 2, col: 0, }, }, }, }, - ], - }, + }, + ], }, }, { @@ -1264,33 +968,23 @@ describe("parseSource()", () => { type: "program", range: { }, - fields: { - statements: [ - { - type: "expression statement", - fields: { - expression: { - type: "call", - range: { - begin: { - row: 0, - col: 0, - }, - end: { - row: 0, - col: 12, - }, - }, - }, + statements: [ + { + type: "expression statement", + expression: { + type: "call", + range: { + begin: { row: 0, col: 0, }, + end: { row: 0, col: 12, }, }, }, - ], - }, + }, + ], }, }, ]; - it.each(cases.slice(4))("$name", testSuccess); + it.each(cases)("$name", testSuccess); }); describe("single statements", () => { @@ -1302,55 +996,33 @@ describe("parseSource()", () => { type: "program", range: { }, - fields: { - statements: [ - { - type: "branch", + statements: [ + { + type: "branch", + range: { + }, + predicate: { range: { + begin: { row: 0, col: 3, }, + end: { row: 0, col: 5, }, }, - fields: { - predicate: { - range: { - begin: { - row: 0, - col: 3, - }, - end: { - row: 0, - col: 5, - }, - }, - }, - consequence: { - type: "block", - range: { - begin: { - row: 0, - col: 7, - }, - end: { - row: 2, - col: 0, - }, - }, - }, - alternative: { - type: "block", - range: { - begin: { - row: 2, - col: 6, - }, - end: { - row: 4, - col: 0, - }, - }, - }, + }, + consequence: { + type: "block", + range: { + begin: { row: 0, col: 7, }, + end: { row: 2, col: 0, }, }, }, - ], - }, + alternative: { + type: "block", + range: { + begin: { row: 2, col: 6, }, + end: { row: 4, col: 0, }, + }, + }, + }, + ], }, }, ]; diff --git a/src/parser/v2.ts b/src/parser/v2.ts index 4df21f6..a639696 100644 --- a/src/parser/v2.ts +++ b/src/parser/v2.ts @@ -7,7 +7,7 @@ import { copyRange, type Range } from "../util/position"; export class ParserError extends Error { public received: string; public expected: string; - public range: { begin: { col: number, row: number }, end: { col: number, row: number }}; + public range: Range; constructor(received: string, expected: string, range: Range) { super(); diff --git a/src/util/position/index.ts b/src/util/position/index.ts index c2d6e4d..18d8dcb 100644 --- a/src/util/position/index.ts +++ b/src/util/position/index.ts @@ -1,4 +1,4 @@ -export default interface Position { +export interface Position { readonly row: number, readonly col: number, } From a1ee8f3c9dab1ad9089afcb35504f00c6b4af9f0 Mon Sep 17 00:00:00 2001 From: Won-hee Cho <89635107+wcho21@users.noreply.github.com> Date: Wed, 31 Jan 2024 19:34:37 +0900 Subject: [PATCH 6/7] add evaluator with position context (#53) --- src/evaluator/environment/v2.test.ts | 52 ++++ src/evaluator/environment/v2.ts | 34 +++ src/evaluator/index.ts | 1 + src/evaluator/v2.test.ts | 398 +++++++++++++++++++++++++++ src/evaluator/v2.ts | 352 +++++++++++++++++++++++ src/evaluator/value/index.ts | 61 ++++ src/index.test.ts | 8 +- src/index.ts | 6 +- src/parser/v2.ts | 7 +- 9 files changed, 910 insertions(+), 9 deletions(-) create mode 100644 src/evaluator/environment/v2.test.ts create mode 100644 src/evaluator/environment/v2.ts create mode 100644 src/evaluator/v2.test.ts create mode 100644 src/evaluator/v2.ts create mode 100644 src/evaluator/value/index.ts diff --git a/src/evaluator/environment/v2.test.ts b/src/evaluator/environment/v2.test.ts new file mode 100644 index 0000000..7ba9f52 --- /dev/null +++ b/src/evaluator/environment/v2.test.ts @@ -0,0 +1,52 @@ +import type { Value } from "../value"; +import Environment from "./v2"; + +describe("set()", () => { + it("set name and value", () => { + const env = new Environment(); + const varName = "foo"; + const varValue = {} as Value; + + 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 Value; + + env.set(varName, varValue); + + expect(env.get(varName)).toBe(varValue); + }); + + it("get null if not found", () => { + const env = new Environment(); + const varNameNotSet = "foo"; + + expect(env.get(varNameNotSet)).toBe(null); + }); +}); + +describe("linked environment", () => { + it("set super environment and get via sub environment", () => { + const varNameInSuper = "foo"; + const varValueInSuper = {} as Value; + + const superEnv = new Environment(); + superEnv.set(varNameInSuper, varValueInSuper); + const subEnv = new Environment(superEnv); + + 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 varNameSetNowhere = "foo"; + expect(subEnv.get(varNameSetNowhere)).toBe(null); + }); +}); diff --git a/src/evaluator/environment/v2.ts b/src/evaluator/environment/v2.ts new file mode 100644 index 0000000..5fd9a4d --- /dev/null +++ b/src/evaluator/environment/v2.ts @@ -0,0 +1,34 @@ +import type { Value } from "../value"; + +export interface EnvironmentType { + get: (name: string) => Value | null; + set: (name: string, value: Value) => void; +} + +export default class Environment implements EnvironmentType { + private readonly superEnvironment: Environment | null; + private readonly table: Map; + + constructor(superEnvironment: Environment | null = null) { + this.superEnvironment = superEnvironment; + this.table = new Map; + } + + get(name: string): Value | 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: Value): void { + this.table.set(name, value); + } +} diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index 71fed9d..9ab12e1 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -29,6 +29,7 @@ import type { } from "./evaluated"; import Environment from "./environment"; +/** @deprecated */ export default class Evaluator { evaluate(node: Program, env: Environment): Evaluated { return this.evaluateProgram(node, env); diff --git a/src/evaluator/v2.test.ts b/src/evaluator/v2.test.ts new file mode 100644 index 0000000..5c82d76 --- /dev/null +++ b/src/evaluator/v2.test.ts @@ -0,0 +1,398 @@ +import Lexer from "../lexer"; +import Parser from "../parser/v2"; +import Evaluator, * as Eval from "./v2"; +import Environment from "./environment/v2"; + +const evaluateInput = (input: string) => { + const lexer = new Lexer(input); + const parser = new Parser(lexer); + const parsed = parser.parseSource(); + + const evaluator = new Evaluator(); + const env = new Environment(); + const evaluated = evaluator.evaluate(parsed, env); + return evaluated; +}; + +const testEvaluatingPrimitive = ({ input, expected }: { input: string, expected: any }): void => { + const evaluated = evaluateInput(input) as any; + + expect(evaluated.value).toBe(expected); +}; + +const testEvaluatingEmpty = ({ input }: { input: string }): void => { + const evaluated = evaluateInput(input) as any; + + expect(evaluated.value).toBe(null); +}; + +const testEvaluatingFunction = ({ input, expectedParamsLength }: { input: string, expectedParamsLength: number }): void => { + const evaluated = evaluateInput(input) as any; + + expect(evaluated).toHaveProperty("parameters"); + expect(evaluated.parameters.length).toBe(expectedParamsLength); + expect(evaluated).toHaveProperty("body"); + expect(evaluated).toHaveProperty("environment"); +}; + +describe("evaluate()", () => { + 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 }, + { input: "100/25", expected: 4 }, + { 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 }, + ]; + + it.each(cases)("evaluate $input", testEvaluatingPrimitive); + }); + + describe("logical not expressions", () => { + const cases = [ + { input: "!참", expected: false }, + { input: "!거짓", expected: true }, + { input: "!!참", expected: true }, + { input: "!!거짓", expected: false }, + ]; + + 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 }, + { input: "거짓 == 거짓", expected: true }, + { input: "참 != 참", expected: false }, + { 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 }, + { input: "2 >= 1", expected: true }, + { input: "1 >= 1", expected: true }, + { input: "1 >= 2", expected: false }, + { input: "2 < 1", expected: false }, + { input: "1 < 1", expected: false }, + { input: "1 < 2", expected: true }, + { input: "2 <= 1", expected: false }, + { input: "1 <= 1", expected: true }, + { input: "1 <= 2", expected: true }, + ]; + + it.each(cases)("evaluate $input", testEvaluatingPrimitive); + }); + + 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", testEvaluatingPrimitive); + }); + + 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 yielding something", () => { + const cases = [ + { + name: "simple if statement with boolean literal predicate", + input: "만약 참 { 3 }", + expected: 3 + }, + { + name: "simple if statement with boolean expression predicate", + input: "만약 1 != 2 { 4 }", + expected: 4 + }, + { + name: "simple if statement with variable comparison predicate", + input: "사과 = 3 바나나 = 4 만약 사과 < 바나나 { 5 }", + expected: 5 + }, + { + name: "simple if-else statement with true boolean literal predicate", + input: "만약 참 { 6 } 아니면 { 7 }", + expected: 6 + }, + { + name: "simple if-else statement with false boolean literal predicate", + input: "만약 거짓 { 6 } 아니면 { 7 }", + expected: 7 + }, + { + name: "simple if-else statement with boolean expression predicate", + input: "만약 1 == 2 { 34 } 아니면 { 56 }", + expected: 56 + }, + ]; + + 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 }", + }, + ]; + + it.each(cases)("evaluate $input", testEvaluatingEmpty); + }); + + describe("nested branch statements yielding something", () => { + const cases = [ + { + 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 $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 + }, + ]; + + it.each(cases)("evaluate $name", testEvaluatingPrimitive); + }); + + describe("function expressions", () => { + const cases = [ + { + name: "simple function expression", + input: "함수 () { 1 }", + expectedParamsLength: 0, + }, + { + name: "simple function expression", + input: "함수 (사과, 바나나, 포도) { 1 }", + expectedParamsLength: 3, + }, + ]; + + it.each(cases)("evaluate $name", testEvaluatingFunction); + }); + + describe("call expressions", () => { + const cases = [ + { + name: "function call with function literal", + input: "함수(바나나) { 결과 바나나 + 1 }(42)", + expected: 43, + }, + { + name: "function call with identifier", + input: "더하기 = 함수(숫자1, 숫자2) { 결과 숫자1 + 숫자2 } 더하기(3, 4)", + expected: 7, + }, + ]; + + it.each(cases)("evaluate $name", testEvaluatingPrimitive); + }); + + describe("complex statements with function and calls", () => { + const cases = [ + { + name: "make and call function containing branch statement", + input: "과일 = 함수(색깔) { 만약 (색깔 == '빨강') { 결과 '사과' } 아니면 { '포도' } } 과일('빨강')", + expected: "사과", + }, + { + name: "make and call closure", + input: "더하기 = 함수(숫자1) { 결과 함수(숫자2) { 결과 숫자1+숫자2 } } 하나더하기 = 더하기(1) 하나더하기(4)", + expected: 5, + }, + ]; + + it.each(cases)("evaluate $name", testEvaluatingPrimitive); + }); + + describe("errors", () => { + const cases = [ + { + name: "top level return error", + input: "결과 11", + expected: Eval.TopLevelReturnError, + range: { begin: { row: 0, col: 0 }, end: { row: 0, col: 4 } }, + }, + { + name: "bad predicate error", + input: "만약 11 {\n 22\n}", + expected: Eval.BadPredicateError, + range: { begin: { row: 0, col: 3 }, end: { row: 0, col: 4 } }, + received: "11", + }, + { + name: "bad identifier error", + input: "사과", + expected: Eval.BadIdentifierError, + range: { begin: { row: 0, col: 0 }, end: { row: 0, col: 1 } }, + received: "사과", + }, + ]; + + it.each(cases)("$name", ({ input, expected, range, received }) => { + expect(() => evaluateInput(input)).toThrow(expected); + try { + evaluateInput(input); + } catch (err) { + const e = err as typeof expected; + + expect(e).toMatchObject({ range }); + if (received !== undefined) { + expect(e).toMatchObject({ received }); + } + } + }); + }); +}); diff --git a/src/evaluator/v2.ts b/src/evaluator/v2.ts new file mode 100644 index 0000000..204791d --- /dev/null +++ b/src/evaluator/v2.ts @@ -0,0 +1,352 @@ +import type * as Node from "../parser/v2"; +import type * as Value from "./value"; +import * as value from "./value"; +import Environment from "./environment/v2"; +import type { Range } from "../util/position"; + +export class EvalError extends Error { + public range: Range; + public received?: string; + + constructor(range: Range, received?: string) { + super(); + this.range = range; + this.received = received; + } +} + +export class TopLevelReturnError extends EvalError {}; +export class BadPredicateError extends EvalError {}; +export class BadAssignmentLeftError extends EvalError {}; +export class BadPrefixExpressionError extends EvalError {}; +export class BadInfixExpressionError extends EvalError {}; +export class BadIdentifierError extends EvalError {}; + +type ComparisonOperator = "==" | "!=" | ">" | "<" | ">=" | "<="; + +export default class Evaluator { + evaluate(node: Node.ProgramNode, env: Environment): Value.Value { + return this.evaluateProgram(node, env); + } + + private evaluateProgram(node: Node.ProgramNode, env: Environment): Value.Value { + const { statements } = node; + + let lastEvaluated: Value.Value | null = null; + for (let i = 0; i < statements.length; ++i) { + const evaluated = this.evaluateStatement(statements[i], env); + + if (evaluated.type === "return") { + throw new TopLevelReturnError(node.range); + } + + lastEvaluated = evaluated; + } + + return lastEvaluated ?? this.createEmptyValue(node.range); + } + + private evaluateStatement(node: Node.StatementNode, env: Environment): Value.Value | Value.ReturnValue { + if (node.type === "branch") { + return this.evaluateBranchStatement(node, env); + } + if (node.type === "expression statement") { + return this.evaluateExpressionStatement(node, env); + } + if (node.type === "return") { + const val = this.evaluateExpression(node.expression, env); + return value.createReturnValue(val); + } + + const nothing: never = node; + return nothing; + } + + private evaluateBranchStatement(node: Node.BranchNode, env: Environment): Value.Value | Value.ReturnValue { + const pred = this.evaluateExpression(node.predicate, env); + if (pred.type !== "boolean") { + throw new BadPredicateError(pred.range, pred.representation); + } + + if (pred.value) { + return this.evaluateBlock(node.consequence, env); + } + + if (node.alternative === undefined) { + return this.createEmptyValue(node.range); + } + + return this.evaluateBlock(node.alternative, env); + } + + private evaluateBlock(node: Node.BlockNode, env: Environment): Value.Value | Value.ReturnValue { + let lastEvaluated: Value.Value | null = null; + + for (let i = 0; i < node.statements.length; ++i) { + const evaluated = this.evaluateStatement(node.statements[i], env); + + if (evaluated.type === "return") { + return evaluated; + } + + lastEvaluated = evaluated; + } + + return lastEvaluated ?? this.createEmptyValue(node.range); + } + + private evaluateExpressionStatement(node: Node.ExpressionStatementNode, env: Environment): Value.Value { + return this.evaluateExpression(node.expression, env); + } + + private evaluateExpression(node: Node.ExpressionNode, env: Environment): Value.Value { + if (node.type === "number") { + return this.createNumberValue(node.value, node.range); + } + if (node.type === "boolean") { + return this.createBooleanValue(node.value, node.range); + } + if (node.type === "string") { + return this.createStringValue(node.value, node.range); + } + if (node.type === "prefix") { + return this.evaluatePrefixExpression(node, env); + } + if (node.type === "infix") { + return this.evaluateInfixExpression(node, env); + } + if (node.type === "assignment") { + return this.evaluateAssignment(node, env); + } + if (node.type === "identifier") { + return this.evaluateIdentifier(node, env); + } + if (node.type === "function") { + return this.evaluateFunctionExpression(node, env); + } + if (node.type === "call") { + return this.evaluateCall(node, env); + } + + const _never: never = node; + return _never; + } + + private evaluatePrefixExpression(node: Node.PrefixNode, env: Environment): Value.Value { + const right = this.evaluateExpression(node.right, env); + + if ((node.prefix === "+" || node.prefix === "-") && right.type == "number") { + return this.evaluatePrefixNumberExpression(node.prefix, right); + } + if (node.prefix === "!" && right.type === "boolean") { + return this.evaluatePrefixBooleanExpression(node.prefix, right); + } + + throw new BadPrefixExpressionError(node.range); + } + + private evaluateInfixExpression(node: Node.InfixNode, env: Environment): Value.Value { + const left = this.evaluateExpression(node.left, env); + const right = this.evaluateExpression(node.right, env); + + if (left.type === "number" && right.type === "number" && this.isArithmeticInfixOperator(node.infix)) { + const value = this.getArithmeticInfixOperationValue(left.value, right.value, node.infix); + return this.createNumberValue(value, node.range); + } + + if (left.type === "number" && right.type === "number" && this.isComparisonInfixOperator(node.infix)) { + const value = this.getNumericComparisonInfixOperationValue(left.value, right.value, node.infix); + return this.createBooleanValue(value, node.range); + } + + if (left.type === "boolean" && right.type === "boolean" && this.isComparisonInfixOperator(node.infix)) { + const value = this.getBooleanComparisonInfixOperationValue(left.value, right.value, node.infix); + return this.createBooleanValue(value, node.range); + } + + if (left.type === "string" && right.type === "string" && this.isComparisonInfixOperator(node.infix)) { + const value = this.getStringComparisonInfixOperationValue(left.value, right.value, node.infix); + return this.createBooleanValue(value, node.range); + } + + throw new BadInfixExpressionError(node.range); + } + + private evaluateIdentifier(node: Node.IdentifierNode, env: Environment): Value.Value { + const varName = node.value; + const value = env.get(varName); + + if (value === null) { + throw new BadIdentifierError(node.range, varName); + } + + return value; + } + + private evaluateAssignment(node: Node.AssignmentNode, env: Environment): Value.Value { + if (node.left.type !== "identifier") { + throw new BadAssignmentLeftError(node.range); + } + + const varName = node.left.value; + const varValue = this.evaluateExpression(node.right, env); + + env.set(varName, varValue); + + return varValue; // evaluated value of assignment is the evaluated value of variable + } + + private evaluateFunctionExpression(node: Node.FunctionNode, env: Environment): Value.Value { + return this.createFunctionValue(node.parameters, node.body, env, node.range); + } + + private evaluateCall(node: Node.CallNode, env: Environment): Value.Value { + const func = this.evaluateExpression(node.func, env); + if (func.type !== "function") { + throw new Error(`expected function but received ${func.type}`); + } + + const callArguments = this.evaluateCallArguments(node.args, env); + + const value = this.evaluateFunctionCall(func, callArguments); + return value; + } + + private evaluateCallArguments(args: Node.ExpressionNode[], env: Environment): Value.Value[] { + return args.map(arg => this.evaluateExpression(arg, env)); + } + + private evaluateFunctionCall(func: Value.FunctionValue, callArguments: Value.Value[]): Value.Value { + const env = this.createExtendedEnvironment(func.environment, func.parameters, callArguments); + + const blockValue = this.evaluateBlock(func.body, env); + if (blockValue.type !== "return") { + // TODO: better error with range + throw new Error(`expected return value in function but it didn't`); + } + + const returnValue = blockValue.value; + return returnValue; + } + + private getBooleanComparisonInfixOperationValue(left: boolean, right: boolean, operator: ComparisonOperator): boolean { + return this.getComparisonInfixOperationValue(left, right, operator); + } + + private getNumericComparisonInfixOperationValue(left: number, right: number, operator: ComparisonOperator): boolean { + return this.getComparisonInfixOperationValue(left, right, operator); + } + + private getStringComparisonInfixOperationValue(left: string, right: string, operator: ComparisonOperator): boolean { + return this.getComparisonInfixOperationValue(left, right, operator); + } + + private getComparisonInfixOperationValue(left: T, right: T, operator: ComparisonOperator): boolean { + if (operator === "==") { + return left === right; + } + if (operator === "!=") { + return left !== right; + } + if (operator === ">") { + return left > right; + } + if (operator === "<") { + return left < right; + } + if (operator === ">=") { + return left >= right; + } + if (operator === "<=") { + return left <= right; + } + + const _never: never = operator; + return _never; + } + + private getArithmeticInfixOperationValue(left: number, right: number, operator: "+" | "-" | "*" | "/"): number { + if (operator === "+") { + return left + right; + } + if (operator === "-") { + return left - right; + } + if (operator === "*") { + return left * right; + } + if (operator === "/") { + return left / right; + } + + const _never: never = operator; + return _never; + } + + private evaluatePrefixNumberExpression(prefix: "+" | "-", node: Node.NumberNode): Value.NumberValue { + if (prefix === "+") { + return this.createNumberValue(node.value, node.range); + } + if (prefix === "-") { + return this.createNumberValue(-node.value, node.range); + } + + const _never: never = prefix; + return _never; + } + + private evaluatePrefixBooleanExpression(prefix: "!", node: Node.BooleanNode): Value.BooleanValue { + if (prefix === "!") { + return this.createBooleanValue(!node.value, node.range); + } + + const _never: never = prefix; + return _never; + } + + private createExtendedEnvironment(oldEnv: Environment, identifiers: Node.IdentifierNode[], values: Value.Value[]): Environment { + const newEnv = new Environment(oldEnv); + + for (let i = 0; i < identifiers.length; ++i) { + const name = identifiers[i].value; + const value = values[i]; + newEnv.set(name, value); + } + + return newEnv; + } + + // create value functions: wrappers for consistent representation + + private createNumberValue(val: number, range: Range): Value.NumberValue { + return value.createNumberValue({ value: val }, String(val), range); + } + + private createBooleanValue(val: boolean, range: Range): Value.BooleanValue { + return value.createBooleanValue({ value: val }, val ? "참" : "거짓", range); + } + + private createStringValue(val: string, range: Range): Value.StringValue { + return value.createStringValue({ value: val }, val, range); + } + + private createEmptyValue(range: Range): Value.EmptyValue { + return value.createEmptyValue({ value: null }, "(없음)", range); + } + + private createFunctionValue(parameters: Node.FunctionNode["parameters"], body: Node.FunctionNode["body"], environment: Environment, range: Range): Value.FunctionValue { + return value.createFunctionValue({ parameters, body, environment }, "(함수)", range); + } + + // util predicate functions + + private isArithmeticInfixOperator(operator: string): operator is "+" | "-" | "*" | "/" { + return ["+", "-", "*", "/"].some(infix => infix === operator); + } + + private isComparisonInfixOperator(operator: string): operator is ComparisonOperator { + return ["==", "!=", ">", "<", ">=", "<="].some(infix => infix === operator); + } +} + +export { default as Environment } from "./environment/v2"; diff --git a/src/evaluator/value/index.ts b/src/evaluator/value/index.ts new file mode 100644 index 0000000..aad540d --- /dev/null +++ b/src/evaluator/value/index.ts @@ -0,0 +1,61 @@ +import { copyRange, type Range } from "../../util/position"; +import type { FunctionNode } from "../../parser/v2"; +import Environment from "../environment/v2"; + +export interface ValueBase { + readonly type: T, + readonly representation: string, + readonly range: Range, +} + +export type Value = PrimitiveValue + | EmptyValue + +export type PrimitiveValue = NumberValue + | StringValue + | BooleanValue + | FunctionValue + +export interface NumberValue extends ValueBase<"number"> { + readonly value: number, +} +export interface StringValue extends ValueBase<"string"> { + readonly value: string, +} +export interface BooleanValue extends ValueBase<"boolean"> { + readonly value: boolean, +} +export interface FunctionValue extends ValueBase<"function"> { + readonly parameters: FunctionNode["parameters"], + readonly body: FunctionNode["body"], + readonly environment: Environment, +} +export interface EmptyValue extends ValueBase<"empty"> { + readonly value: null, +} + +export interface ReturnValue { + readonly type: "return", + readonly value: Value, +} + +const createValueCreator = >(type: T) => { + return (fields: Omit>, representation: string, range: Range) => ({ + type, + range: copyRange(range.begin, range.end), + representation, + ...fields, + }); +}; +type CreateValue> = (fields: Omit>, representation: string, range: Range) => V; + +export const createNumberValue: CreateValue<"number", NumberValue> = createValueCreator<"number", NumberValue>("number"); +export const createBooleanValue: CreateValue<"boolean", BooleanValue> = createValueCreator<"boolean", BooleanValue>("boolean"); +export const createStringValue: CreateValue<"string", StringValue> = createValueCreator<"string", StringValue>("string"); +export const createEmptyValue: CreateValue<"empty", EmptyValue> = createValueCreator<"empty", EmptyValue>("empty"); +export const createFunctionValue: CreateValue<"function", FunctionValue> = createValueCreator<"function", FunctionValue>("function"); + +export const createReturnValue = (value: Value): ReturnValue => ({ + type: "return", + value, +}); diff --git a/src/index.test.ts b/src/index.test.ts index f227f53..f19e464 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -24,12 +24,12 @@ it("execute 1 == 2", () => { expect(execute("1 == 2")).toBe("거짓"); }); -it("execute 2 > 1 == 참", () => { - expect(execute("2 > 1 == 참")).toBe("참"); +it("execute 참 == 2 > 1", () => { + expect(execute("참 == 2 > 1")).toBe("참"); }); -it("execute 1 != 1 == 거짓", () => { // note that comparison is left associative - expect(execute("1 != 1 == 거짓")).toBe("참"); +it("execute 거짓 == 1 != 1", () => { // note that comparison is left associative + expect(execute("거짓 == 1 != 1")).toBe("참"); }); it("execute 거짓 == (1 < 1+1)", () => { diff --git a/src/index.ts b/src/index.ts index 278f9ac..86652f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ import Lexer from "./lexer"; -import Parser from "./parser"; -import Evaluator, { Environment } from "./evaluator"; +import Parser from "./parser/v2"; +import Evaluator, { Environment } from "./evaluator/v2"; export const execute = (input: string): string => { const lexer = new Lexer(input); const parser = new Parser(lexer); - const parsed = parser.parseProgram(); + const parsed = parser.parseSource(); const evaluator = new Evaluator(); const environment = new Environment(); diff --git a/src/parser/v2.ts b/src/parser/v2.ts index a639696..ab4a025 100644 --- a/src/parser/v2.ts +++ b/src/parser/v2.ts @@ -96,7 +96,7 @@ export default class Parser { } private parseBranchStatement(): Node.BranchNode { - const { range } = this.reader.read(); + const firstToken = this.reader.read(); this.reader.advance(); const predicate = this.parseExpression(bindingPowers.lowest); @@ -104,19 +104,22 @@ export default class Parser { const maybeElseToken = this.reader.read(); if (maybeElseToken.type !== "keyword" || maybeElseToken.value !== "아니면") { + const range = { begin: firstToken.range.begin, end: consequence.range.end }; return node.createBranchNode({ predicate, consequence }, range); } this.reader.advance(); const alternative = this.parseBlock(); + const range = { begin: firstToken.range.begin, end: alternative.range.end }; return node.createBranchNode({ predicate, consequence, alternative }, range); } private parseReturnStatement(): Node.ReturnNode { - const { range } = this.reader.read(); + const firstToken = this.reader.read(); this.reader.advance(); const expression = this.parseExpression(bindingPowers.lowest); + const range = { begin: firstToken.range.begin, end: expression.range.end }; return node.createReturnNode({ expression }, range); } From b49c6b67d1b455451bc72fb590b3d52590eb6005 Mon Sep 17 00:00:00 2001 From: Won-hee Cho <89635107+wcho21@users.noreply.github.com> Date: Wed, 31 Jan 2024 19:55:55 +0900 Subject: [PATCH 7/7] remove deprecated part and build (#54) --- dist/index.min.js | 2 +- src/evaluator/environment/index.test.ts | 8 +- src/evaluator/environment/index.ts | 10 +- src/evaluator/environment/v2.test.ts | 52 - src/evaluator/environment/v2.ts | 34 - src/evaluator/evaluated/index.test.ts | 94 - src/evaluator/evaluated/index.ts | 106 - src/evaluator/index.test.ts | 65 +- src/evaluator/index.ts | 471 ++-- src/evaluator/v2.test.ts | 398 ---- src/evaluator/v2.ts | 352 --- src/evaluator/value/index.ts | 4 +- src/index.ts | 4 +- .../char-buffer/char-reader/index.test.ts | 39 - src/lexer/char-buffer/char-reader/index.ts | 18 - src/lexer/char-buffer/index.test.ts | 50 - src/lexer/char-buffer/index.ts | 15 - src/lexer/index.test.ts | 249 --- src/lexer/index.ts | 236 -- src/lexer/token/index.test.ts | 138 -- src/lexer/token/index.ts | 146 -- src/parser/index.test.ts | 1932 ++++++++--------- src/parser/index.ts | 559 +++-- src/parser/syntax-tree/expression/index.ts | 114 - src/parser/syntax-tree/group/index.ts | 25 - src/parser/syntax-tree/index.ts | 12 - src/parser/syntax-tree/statement/index.ts | 48 - src/parser/token-reader/index.test.ts | 52 - src/parser/token-reader/index.ts | 28 - src/parser/v2.test.ts | 1046 --------- src/parser/v2.ts | 400 ---- 31 files changed, 1516 insertions(+), 5191 deletions(-) delete mode 100644 src/evaluator/environment/v2.test.ts delete mode 100644 src/evaluator/environment/v2.ts delete mode 100644 src/evaluator/evaluated/index.test.ts delete mode 100644 src/evaluator/evaluated/index.ts delete mode 100644 src/evaluator/v2.test.ts delete mode 100644 src/evaluator/v2.ts delete mode 100644 src/lexer/token/index.test.ts delete mode 100644 src/lexer/token/index.ts delete mode 100644 src/parser/syntax-tree/expression/index.ts delete mode 100644 src/parser/syntax-tree/group/index.ts delete mode 100644 src/parser/syntax-tree/index.ts delete mode 100644 src/parser/syntax-tree/statement/index.ts delete mode 100644 src/parser/token-reader/index.test.ts delete mode 100644 src/parser/token-reader/index.ts delete mode 100644 src/parser/v2.test.ts delete mode 100644 src/parser/v2.ts diff --git a/dist/index.min.js b/dist/index.min.js index 2a7fc1d..a61ba1d 100644 --- a/dist/index.min.js +++ b/dist/index.min.js @@ -1 +1 @@ -(()=>{"use strict";var e={404:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.default=class{constructor(e=null){this.superEnvironment=e,this.table=new Map}get(e){const t=this.table.get(e);return void 0!==t?t:null===this.superEnvironment?null:this.superEnvironment.get(e)}set(e,t){this.table.set(e,t)}}},290:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.wrapReturnValue=t.makeEvaluatedEmpty=t.makeEvaluatedFunction=t.makeEvaluatedBoolean=t.makeEvaluatedString=t.makeEvaluatedNumber=void 0,t.makeEvaluatedNumber=e=>({type:"number",value:e,get representation(){return`${e}`}}),t.makeEvaluatedString=e=>({type:"string",value:e,get representation(){return`'${e}'`}}),t.makeEvaluatedBoolean=e=>({type:"boolean",value:e,get representation(){return e?"참":"거짓"}}),t.makeEvaluatedFunction=(e,t,r)=>({type:"function",parameters:e,body:t,environment:r,get representation(){return"(함수)"}}),t.makeEvaluatedEmpty=()=>({type:"empty",get representation(){return"(비어있음)"}}),t.wrapReturnValue=e=>({type:"return value",value:e})},673:function(e,t,r){var a=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.Environment=void 0;const n=r(290),i=a(r(404));t.default=class{evaluate(e,t){return this.evaluateProgram(e,t)}evaluateProgram(e,t){const{statements:r}=e;if(0===r.length)return(0,n.makeEvaluatedEmpty)();for(let e=0;e"===r)return(0,n.makeEvaluatedBoolean)(a.value>i.value);if("<"===r)return(0,n.makeEvaluatedBoolean)(a.value="===r)return(0,n.makeEvaluatedBoolean)(a.value>=i.value);if("<="===r)return(0,n.makeEvaluatedBoolean)(a.value<=i.value)}if("number"===a.type&&"number"===i.type){if("+"===r)return(0,n.makeEvaluatedNumber)(a.value+i.value);if("-"===r)return(0,n.makeEvaluatedNumber)(a.value-i.value);if("*"===r)return(0,n.makeEvaluatedNumber)(a.value*i.value);if("/"===r)return(0,n.makeEvaluatedNumber)(a.value/i.value);throw new Error(`bad infix ${r} for number operands`)}throw new Error(`bad infix ${r}, with left '${a}' and right '${i}'`)}parseCallArguments(e,t){const r=[];for(const a of e){const e=this.evaluateExpression(a,t);r.push(e)}return r}evaluateFunctionCall(e,t){const r=new i.default(e.environment);for(let a=0;a{const t=new u.default(e),r=new o.default(t).parseProgram(),a=new l.default,n=new l.Environment,i=a.evaluate(r,n);return String(i.representation)}},197:function(e,t,r){var a=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const n=a(r(13));class i{constructor(e){this.reader=new n.default(e,i.END_OF_INPUT)}pop(){const e=this.reader.read();return this.reader.next(),e}peek(){return this.reader.read()}}i.END_OF_INPUT="\0",t.default=i},13:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.default=class{constructor(e,t){this.chars=e,this.fallbackChar=t,this.index=0}read(){return this.index===this.chars.length?this.fallbackChar:this.chars[this.index]}next(){this.index!==this.chars.length&&this.index++}}},439:function(e,t,r){var a=this&&this.__createBinding||(Object.create?function(e,t,r,a){void 0===a&&(a=r);var n=Object.getOwnPropertyDescriptor(t,r);n&&!("get"in n?!t.__esModule:n.writable||n.configurable)||(n={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,a,n)}:function(e,t,r,a){void 0===a&&(a=r),e[a]=t[r]}),n=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),i=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)"default"!==r&&Object.prototype.hasOwnProperty.call(e,r)&&a(t,e,r);return n(t,e),t},s=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const u=s(r(197)),o=i(r(632)),l=i(r(352));t.default=class{constructor(e){this.charBuffer=new u.default(e)}getToken(){this.skipWhitespaces();const e=this.charBuffer.peek();switch(e){case"+":case"-":case"*":case"/":{const e=this.charBuffer.pop();return o.operator(e)}case"(":case")":{const e=this.charBuffer.pop();return o.groupDelimiter(e)}case"{":case"}":{const e=this.charBuffer.pop();return o.blockDelimiter(e)}case",":{const e=this.charBuffer.pop();return o.separator(e)}case"!":{this.charBuffer.pop();const e=this.readOperatorStartingWithBang();return o.operator(e)}case"=":{this.charBuffer.pop();const e=this.readOperatorStartingWithEqual();return o.operator(e)}case">":{this.charBuffer.pop();const e=this.readOperatorStartingWithGreaterThan();return o.operator(e)}case"<":{this.charBuffer.pop();const e=this.readOperatorStartingWithLessThan();return o.operator(e)}case"'":{this.charBuffer.pop();const[e,t]=this.readStringLiteral();return t?o.stringLiteral(e):o.illegal("'"+e)}case u.default.END_OF_INPUT:return o.end;default:if(l.isDigit(e)){const e=this.readNumberLiteral();return o.numberLiteral(e)}if(l.isLetter(e)){const e=this.readLettersAndDigits();return"참"===e||"거짓"===e?o.booleanLiteral(e):"만약"===e||"아니면"===e||"함수"===e||"결과"===e?o.keyword(e):o.identifier(e)}return this.charBuffer.pop(),o.illegal(e)}}skipWhitespaces(){for(;l.isWhitespace(this.charBuffer.peek());)this.charBuffer.pop()}readOperatorStartingWithBang(){return"="===this.charBuffer.peek()?(this.charBuffer.pop(),"!="):"!"}readOperatorStartingWithEqual(){return"="===this.charBuffer.peek()?(this.charBuffer.pop(),"=="):"="}readOperatorStartingWithGreaterThan(){return"="===this.charBuffer.peek()?(this.charBuffer.pop(),">="):">"}readOperatorStartingWithLessThan(){return"="===this.charBuffer.peek()?(this.charBuffer.pop(),"<="):"<"}readStringLiteral(){const e=[];for(;;){const t=this.charBuffer.pop();if("'"===t||t===u.default.END_OF_INPUT)return[e.join(""),"'"===t];e.push(t)}}readNumberLiteral(){return this.readDigits()+this.readDecimalPart()}readDigits(){const e=[];for(;l.isDigit(this.charBuffer.peek());)e.push(this.charBuffer.pop());return e.join("")}readDecimalPart(){return"."!==this.charBuffer.peek()?"":this.charBuffer.pop()+this.readDigits()}readLettersAndDigits(){const e=[];for(;l.isLetter(this.charBuffer.peek())||l.isDigit(this.charBuffer.peek());)e.push(this.charBuffer.pop());return e.join("")}}},632:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.end=t.illegal=t.keyword=t.separator=t.blockDelimiter=t.groupDelimiter=t.stringLiteral=t.booleanLiteral=t.numberLiteral=t.identifier=t.operator=t.END_VALUE=void 0,t.END_VALUE="$end",t.operator=e=>({type:"operator",value:e}),t.identifier=e=>({type:"identifier",value:e}),t.numberLiteral=e=>({type:"number literal",value:e}),t.booleanLiteral=e=>({type:"boolean literal",value:e}),t.stringLiteral=e=>({type:"string literal",value:e}),t.groupDelimiter=e=>({type:"group delimiter",value:e}),t.blockDelimiter=e=>({type:"block delimiter",value:e}),t.separator=e=>({type:"separator",value:e}),t.keyword=e=>({type:"keyword",value:e}),t.illegal=e=>({type:"illegal",value:e}),t.end={type:"end",value:t.END_VALUE}},352:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.isWhitespace=t.isDigit=t.isLetter=void 0,t.isLetter=e=>1===e.length&&/^[a-zA-Z가-힣_]$/.test(e),t.isDigit=e=>1===e.length&&/^[0-9]$/.test(e),t.isWhitespace=e=>1===e.length&&/^[ \t\r\n]$/.test(e)},522:function(e,t,r){var a=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const n=r(763),i=a(r(461)),s=e=>{switch(e){case"=":return 30;case"==":case"!=":case">":case"<":case">=":case"<=":return 40;case"+":case"-":return 50;case"*":case"/":return 60;case"(":return 80;default:return 0}};t.default=class{constructor(e){this.buffer=new i.default(e)}parseProgram(){const e=(0,n.makeProgram)();for(;!this.buffer.isEnd();){const t=this.parseStatement();null!==t&&e.statements.push(t)}return e}parseBlock(){const e=this.buffer.read();if("block delimiter"!==e.type||"{"!==e.value)throw new Error(`expected { but received ${e.type}`);this.buffer.next();const t=[];for(;;){const e=this.buffer.read();if("block delimiter"===e.type&&"}"===e.value){this.buffer.next();break}const r=this.parseStatement();null!==r&&t.push(r)}return(0,n.makeBlock)(t)}parseStatement(){const e=this.buffer.read();return"keyword"===e.type&&"만약"===e.value?(this.buffer.next(),this.parseBranchStatement()):"keyword"===e.type&&"결과"===e.value?(this.buffer.next(),this.parseReturnStatement()):this.parseExpressionStatement()}parseBranchStatement(){const e=this.parseExpression(0),t=this.parseBlock(),r=this.buffer.read();if("keyword"!==r.type||"아니면"!==r.value)return(0,n.makeBranchStatement)(e,t);this.buffer.next();const a=this.parseBlock();return(0,n.makeBranchStatement)(e,t,a)}parseReturnStatement(){const e=this.parseExpression(0);return(0,n.makeReturnStatement)(e)}parseExpressionStatement(){const e=this.parseExpression(0);return(0,n.makeExpressionStatement)(e)}parseExpression(e){let t=this.parsePrefixExpression();for(;!(s(this.buffer.read().value)<=e);){const e=this.parseInfixExpression(t);if(null===e)break;t=e}return t}parsePrefixExpression(){const e=this.buffer.read();if(this.buffer.next(),"number literal"===e.type)return this.parseNumberLiteral(e.value);if("boolean literal"===e.type)return this.parseBooleanLiteral(e.value);if("string literal"===e.type)return this.parseStringLiteral(e.value);if("identifier"===e.type)return(0,n.makeIdentifier)(e.value);if("operator"===e.type&&("+"===e.value||"-"===e.value||"!"===e.value)){const t=this.parseExpression(70),r=e.value;return(0,n.makePrefixExpression)(r,t)}if("keyword"===e.type&&"함수"===e.value){const e=this.parseParameters(),t=this.parseBlock();return(0,n.makeFunctionExpression)(t,e)}if("group delimiter"===e.type&&"("===e.value){const e=this.parseExpression(0),t=this.buffer.read();if(this.buffer.next(),"group delimiter"!==t.type||")"!==t.value)throw new Error(`expected ) but received ${t.type}`);return e}throw new Error(`bad token type ${e.type} (${e.value}) for prefix expression`)}parseParameters(){const e=[],t=this.buffer.read();if("group delimiter"!==t.type||"("!==t.value)throw new Error(`expected ( but received ${t.type}`);this.buffer.next();const r=this.buffer.read();if(this.buffer.next(),"group delimiter"===r.type&&")"===r.value)return[];const a=r;if("identifier"!==a.type)throw new Error(`expected identifier but received ${a}`);const n=a;for(e.push(n);;){const t=this.buffer.read();if(this.buffer.next(),"group delimiter"===t.type&&")"===t.value)break;const r=t;if("separator"!==r.type)throw new Error(`expected comma but received ${r}`);const a=this.buffer.read();if(this.buffer.next(),"identifier"!==a.type)throw new Error(`expected identifier but received ${a}`);const n=a;e.push(n)}return e}parseInfixExpression(e){const t=this.buffer.read();if("group delimiter"===t.type&&"("===t.value)return"function expression"!==e.type&&"identifier"!==e.type?null:(this.buffer.next(),this.parseCall(e));if("operator"!==t.type)return null;const r=t.value;return"="===r&&"identifier"===e.type?(this.buffer.next(),this.parseAssignment(e)):"+"===r||"-"===r||"*"===r||"/"===r||"!="===r||"=="===r||">"===r||"<"===r||">="===r||"<="===r?(this.buffer.next(),this.parseArithmeticInfixExpression(e,r)):null}parseCall(e){const t=this.parseCallArguments();return(0,n.makeCall)(e,t)}parseCallArguments(){const e=this.buffer.read();if("group delimiter"===e.type&&")"===e.value)return this.buffer.next(),[];const t=[this.parseExpression(0)];for(;"separator"===this.buffer.read().type;){this.buffer.next();const e=this.parseExpression(0);t.push(e)}const r=this.buffer.read();if(this.buffer.next(),"group delimiter"!==r.type||")"!==r.value)throw new Error(`expect ) but received ${r.type}`);return t}parseAssignment(e){const t=s("="),r=this.parseExpression(t);return(0,n.makeAssignment)(e,r)}parseArithmeticInfixExpression(e,t){const r=s(t),a=this.parseExpression(r);return(0,n.makeInfixExpression)(t,e,a)}parseNumberLiteral(e){const t=Number(e);if(Number.isNaN(t))throw new Error(`expected non-NaN number, but received '${e}'`);return(0,n.makeNumberNode)(t)}parseBooleanLiteral(e){const t="참"===e;return(0,n.makeBooleanNode)(t)}parseStringLiteral(e){return(0,n.makeStringNode)(e)}}},813:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.makeAssignment=t.makeCall=t.makeFunctionExpression=t.makeInfixExpression=t.makePrefixExpression=t.makeStringNode=t.makeBooleanNode=t.makeNumberNode=t.makeIdentifier=void 0,t.makeIdentifier=e=>({type:"identifier",value:e}),t.makeNumberNode=e=>({type:"number node",value:e}),t.makeBooleanNode=e=>({type:"boolean node",value:e}),t.makeStringNode=e=>({type:"string node",value:e}),t.makePrefixExpression=(e,t)=>({type:"prefix expression",prefix:e,expression:t}),t.makeInfixExpression=(e,t,r)=>({type:"infix expression",infix:e,left:t,right:r}),t.makeFunctionExpression=(e,t=[])=>({type:"function expression",parameter:t,body:e}),t.makeCall=(e,t)=>({type:"call",functionToCall:e,callArguments:t}),t.makeAssignment=(e,t)=>({type:"assignment",left:e,right:t})},54:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.makeBlock=t.makeProgram=void 0,t.makeProgram=(e=[])=>({type:"program",statements:e}),t.makeBlock=(e=[])=>({type:"block",statements:e})},763:function(e,t,r){var a=this&&this.__createBinding||(Object.create?function(e,t,r,a){void 0===a&&(a=r);var n=Object.getOwnPropertyDescriptor(t,r);n&&!("get"in n?!t.__esModule:n.writable||n.configurable)||(n={enumerable:!0,get:function(){return t[r]}}),Object.defineProperty(e,a,n)}:function(e,t,r,a){void 0===a&&(a=r),e[a]=t[r]}),n=this&&this.__exportStar||function(e,t){for(var r in e)"default"===r||Object.prototype.hasOwnProperty.call(t,r)||a(t,e,r)};Object.defineProperty(t,"__esModule",{value:!0}),n(r(813),t),n(r(602),t),n(r(54),t)},602:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.makeExpressionStatement=t.makeReturnStatement=t.makeBranchStatement=void 0,t.makeBranchStatement=(e,t,r)=>({type:"branch statement",predicate:e,consequence:t,alternative:r}),t.makeReturnStatement=e=>({type:"return statement",expression:e}),t.makeExpressionStatement=e=>({type:"expression statement",expression:e})},461:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.default=class{constructor(e){this.lexer=e,this.token=e.getToken()}isEnd(){return"end"===this.token.type}read(){return this.token}next(){this.token=this.lexer.getToken()}}}},t={},r=function r(a){var n=t[a];if(void 0!==n)return n.exports;var i=t[a]={exports:{}};return e[a].call(i.exports,i,i.exports,r),i.exports}(436);window.kal=r})(); \ No newline at end of file +(()=>{"use strict";var e={404:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.default=class{constructor(e=null){this.superEnvironment=e,this.table=new Map}get(e){const r=this.table.get(e);return void 0!==r?r:null===this.superEnvironment?null:this.superEnvironment.get(e)}set(e,r){this.table.set(e,r)}}},673:function(e,r,t){var a=this&&this.__createBinding||(Object.create?function(e,r,t,a){void 0===a&&(a=t);var n=Object.getOwnPropertyDescriptor(r,t);n&&!("get"in n?!r.__esModule:n.writable||n.configurable)||(n={enumerable:!0,get:function(){return r[t]}}),Object.defineProperty(e,a,n)}:function(e,r,t,a){void 0===a&&(a=t),e[a]=r[t]}),n=this&&this.__setModuleDefault||(Object.create?function(e,r){Object.defineProperty(e,"default",{enumerable:!0,value:r})}:function(e,r){e.default=r}),i=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var r={};if(null!=e)for(var t in e)"default"!==t&&Object.prototype.hasOwnProperty.call(e,t)&&a(r,e,t);return n(r,e),r},o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r,"__esModule",{value:!0}),r.Environment=r.BadIdentifierError=r.BadInfixExpressionError=r.BadPrefixExpressionError=r.BadAssignmentLeftError=r.BadPredicateError=r.TopLevelReturnError=r.EvalError=void 0;const s=i(t(994)),c=o(t(404));class u extends Error{constructor(e,r){super(),this.range=e,this.received=r}}r.EvalError=u;class l extends u{}r.TopLevelReturnError=l;class d extends u{}r.BadPredicateError=d;class p extends u{}r.BadAssignmentLeftError=p;class h extends u{}r.BadPrefixExpressionError=h;class f extends u{}r.BadInfixExpressionError=f;class g extends u{}r.BadIdentifierError=g,r.default=class{evaluate(e,r){return this.evaluateProgram(e,r)}evaluateProgram(e,r){const{statements:t}=e;let a=null;for(let n=0;nthis.evaluateExpression(e,r)))}evaluateFunctionCall(e,r){const t=this.createExtendedEnvironment(e.environment,e.parameters,r),a=this.evaluateBlock(e.body,t);if("return"!==a.type)throw new Error("expected return value in function but it didn't");return a.value}getBooleanComparisonInfixOperationValue(e,r,t){return this.getComparisonInfixOperationValue(e,r,t)}getNumericComparisonInfixOperationValue(e,r,t){return this.getComparisonInfixOperationValue(e,r,t)}getStringComparisonInfixOperationValue(e,r,t){return this.getComparisonInfixOperationValue(e,r,t)}getComparisonInfixOperationValue(e,r,t){return"=="===t?e===r:"!="===t?e!==r:">"===t?e>r:"<"===t?e="===t?e>=r:"<="===t?e<=r:t}getArithmeticInfixOperationValue(e,r,t){return"+"===t?e+r:"-"===t?e-r:"*"===t?e*r:"/"===t?e/r:t}evaluatePrefixNumberExpression(e,r){return"+"===e?this.createNumberValue(r.value,r.range):"-"===e?this.createNumberValue(-r.value,r.range):e}evaluatePrefixBooleanExpression(e,r){return"!"===e?this.createBooleanValue(!r.value,r.range):e}createExtendedEnvironment(e,r,t){const a=new c.default(e);for(let e=0;er===e))}isComparisonInfixOperator(e){return["==","!=",">","<",">=","<="].some((r=>r===e))}};var v=t(404);Object.defineProperty(r,"Environment",{enumerable:!0,get:function(){return o(v).default}})},994:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createReturnValue=r.createFunctionValue=r.createEmptyValue=r.createStringValue=r.createBooleanValue=r.createNumberValue=void 0;const a=t(548),n=e=>(r,t,n)=>Object.assign({type:e,range:(0,a.copyRange)(n.begin,n.end),representation:t},r);r.createNumberValue=n("number"),r.createBooleanValue=n("boolean"),r.createStringValue=n("string"),r.createEmptyValue=n("empty"),r.createFunctionValue=n("function"),r.createReturnValue=e=>({type:"return",value:e})},436:function(e,r,t){var a=this&&this.__createBinding||(Object.create?function(e,r,t,a){void 0===a&&(a=t);var n=Object.getOwnPropertyDescriptor(r,t);n&&!("get"in n?!r.__esModule:n.writable||n.configurable)||(n={enumerable:!0,get:function(){return r[t]}}),Object.defineProperty(e,a,n)}:function(e,r,t,a){void 0===a&&(a=t),e[a]=r[t]}),n=this&&this.__setModuleDefault||(Object.create?function(e,r){Object.defineProperty(e,"default",{enumerable:!0,value:r})}:function(e,r){e.default=r}),i=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var r={};if(null!=e)for(var t in e)"default"!==t&&Object.prototype.hasOwnProperty.call(e,t)&&a(r,e,t);return n(r,e),r},o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r,"__esModule",{value:!0}),r.execute=void 0;const s=o(t(439)),c=o(t(522)),u=i(t(673));r.execute=e=>{const r=new s.default(e),t=new c.default(r).parseSource(),a=new u.default,n=new u.Environment,i=a.evaluate(t,n);return String(i.representation)}},545:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.default=class{constructor(e,r){this.chars=e,this.fallbackChar=r,this.index=0,this.row=0,this.col=0}readChar(){if(this.index===this.chars.length)return{value:this.fallbackChar,position:{row:this.row,col:this.col}};const e=this.peekNewLine();return null!==e?{value:e,position:{row:this.row,col:this.col}}:{value:this.chars[this.index],position:{row:this.row,col:this.col}}}advance(){if(this.index===this.chars.length)return;const e=this.peekNewLine();if(null!==e)return this.index+=e.length,++this.row,void(this.col=0);++this.index,++this.col}peekNewLine(){if(this.index===this.chars.length)return null;const e=this.chars[this.index];if("\r"!==e&&"\n"!==e)return null;if(this.index+1===this.chars.length)return e;const r=this.chars[this.index+1];return"\r"!==r&&"\n"!==r?e:e+r}}},197:function(e,r,t){var a=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r,"__esModule",{value:!0});const n=a(t(545));class i{constructor(e){this.reader=new n.default(e,i.END_OF_INPUT)}popChar(){const e=this.reader.readChar();return this.reader.advance(),e}peekChar(){return this.reader.readChar()}}i.END_OF_INPUT="\0",r.default=i},439:function(e,r,t){var a=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r,"__esModule",{value:!0});const n=a(t(197)),i=t(562),o=t(352);r.default=class{constructor(e){this.charBuffer=new n.default(e)}getSourceToken(){this.skipWhitespaceChars();const e=this.charBuffer.peekChar();switch(e.value){case"+":case"-":case"*":case"/":{const{position:r}=this.charBuffer.popChar(),t=e.value;return(0,i.createOperatorToken)(t,r,r)}case"(":case")":{const{position:r}=this.charBuffer.popChar(),t=e.value;return(0,i.createGroupDelimiterToken)(t,r,r)}case"{":case"}":{const{position:r}=this.charBuffer.popChar(),t=e.value;return(0,i.createBlockDelimiterToken)(t,r,r)}case",":{const{position:r}=this.charBuffer.popChar(),t=e.value;return(0,i.createSeparatorToken)(t,r,r)}case"!":{const{position:e}=this.charBuffer.popChar();return this.lexCharsStartingWithBang(e)}case"=":{const{position:e}=this.charBuffer.popChar();return this.lexCharsStartingWithEqual(e)}case">":{const{position:e}=this.charBuffer.popChar();return this.lexCharsStartingWithGreaterThan(e)}case"<":{const{position:e}=this.charBuffer.popChar();return this.lexCharsStartingWithLessThan(e)}case"'":{const{position:e}=this.charBuffer.popChar();return this.lexCharsStartingWithSingleQuote(e)}case n.default.END_OF_INPUT:{const{position:e}=this.charBuffer.popChar();return(0,i.createEndToken)("$end",e,e)}default:{if((0,o.isDigit)(e.value))return this.lexNumberLiteral();if((0,o.isLetter)(e.value))return this.lexLetters();const{position:r}=this.charBuffer.popChar();return(0,i.createIllegalToken)(e.value,r,r)}}}skipWhitespaceChars(){for(;;){const e=this.charBuffer.peekChar();if(!(0,o.isWhitespace)(e.value))break;this.charBuffer.popChar()}}lexCharsStartingWithBang(e){if("="===this.charBuffer.peekChar().value){const{position:r}=this.charBuffer.popChar();return(0,i.createOperatorToken)("!=",e,r)}return(0,i.createOperatorToken)("!",e,e)}lexCharsStartingWithEqual(e){if("="===this.charBuffer.peekChar().value){const{position:r}=this.charBuffer.popChar();return(0,i.createOperatorToken)("==",e,r)}return(0,i.createOperatorToken)("=",e,e)}lexCharsStartingWithGreaterThan(e){if("="===this.charBuffer.peekChar().value){const{position:r}=this.charBuffer.popChar();return(0,i.createOperatorToken)(">=",e,r)}return(0,i.createOperatorToken)(">",e,e)}lexCharsStartingWithLessThan(e){if("="===this.charBuffer.peekChar().value){const{position:r}=this.charBuffer.popChar();return(0,i.createOperatorToken)("<=",e,r)}return(0,i.createOperatorToken)("<",e,e)}lexCharsStartingWithSingleQuote(e){const r=[];for(;;){const t=this.charBuffer.popChar(),a=r.map((e=>e.value)).join(""),o=e,s=t.position;if("'"===t.value)return(0,i.createStringLiteralToken)(a,o,s);if(t.value===n.default.END_OF_INPUT)return(0,i.createIllegalStringLiteralToken)(a,o,s);r.push(t)}}lexNumberLiteral(){const e=this.readDigitChars(),r=this.readDecimalChars(),t=e.concat(r),a=t.map((e=>e.value)).join(""),n=t[0].position,o=t[t.length-1].position;return(0,i.createNumberLiteralToken)(a,n,o)}lexLetters(){const e=this.readLetterChars(),r=e.map((e=>e.value)).join(""),t=e[0].position,a=e[e.length-1].position;switch(r){case"참":case"거짓":return(0,i.createBooleanLiteralToken)(r,t,a);case"만약":case"아니면":case"함수":case"결과":return(0,i.createKeywordToken)(r,t,a);default:return(0,i.createIdentifierToken)(r,t,a)}}readDigitChars(){const e=[];for(;;){const r=this.charBuffer.peekChar();if(!(0,o.isDigit)(r.value))break;e.push(this.charBuffer.popChar())}return e}readDecimalChars(){if("."!==this.charBuffer.peekChar().value)return[];const e=this.charBuffer.popChar(),r=this.readDigitChars();return[e].concat(r)}readLetterChars(){const e=[];for(;;){const r=this.charBuffer.peekChar();if(!(0,o.isLetter)(r.value)&&!(0,o.isDigit)(r.value))break;e.push(this.charBuffer.popChar())}return e}}},854:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createTokenCreator=void 0,r.createTokenCreator=function(e){return function(r,t,a){return void 0!==a?{type:e,value:r,range:{begin:t,end:a}}:{type:e,value:r,range:t}}}},287:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createSeparatorToken=r.createBlockDelimiterToken=r.createGroupDelimiterToken=void 0;const a=t(854);r.createGroupDelimiterToken=(0,a.createTokenCreator)("group delimiter"),r.createBlockDelimiterToken=(0,a.createTokenCreator)("block delimiter"),r.createSeparatorToken=(0,a.createTokenCreator)("separator")},762:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createKeywordToken=r.createIdentifierToken=void 0;const a=t(854);r.createIdentifierToken=(0,a.createTokenCreator)("identifier"),r.createKeywordToken=(0,a.createTokenCreator)("keyword")},562:function(e,r,t){var a=this&&this.__createBinding||(Object.create?function(e,r,t,a){void 0===a&&(a=t);var n=Object.getOwnPropertyDescriptor(r,t);n&&!("get"in n?!r.__esModule:n.writable||n.configurable)||(n={enumerable:!0,get:function(){return r[t]}}),Object.defineProperty(e,a,n)}:function(e,r,t,a){void 0===a&&(a=t),e[a]=r[t]}),n=this&&this.__exportStar||function(e,r){for(var t in e)"default"===t||Object.prototype.hasOwnProperty.call(r,t)||a(r,e,t)};Object.defineProperty(r,"__esModule",{value:!0}),n(t(547),r),n(t(762),r),n(t(768),r),n(t(287),r),n(t(763),r)},768:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createStringLiteralToken=r.createBooleanLiteralToken=r.createNumberLiteralToken=void 0;const a=t(854);r.createNumberLiteralToken=(0,a.createTokenCreator)("number literal"),r.createBooleanLiteralToken=(0,a.createTokenCreator)("boolean literal"),r.createStringLiteralToken=(0,a.createTokenCreator)("string literal")},547:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createOperatorToken=void 0;const a=t(854);r.createOperatorToken=(0,a.createTokenCreator)("operator")},763:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createEndToken=r.createIllegalStringLiteralToken=r.createIllegalToken=r.END_VALUE=void 0;const a=t(854);r.END_VALUE="$end",r.createIllegalToken=(0,a.createTokenCreator)("illegal"),r.createIllegalStringLiteralToken=(0,a.createTokenCreator)("illegal string"),r.createEndToken=(0,a.createTokenCreator)("end")},352:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.isWhitespace=r.isDigit=r.isLetter=void 0,r.isLetter=e=>1===e.length&&/^[a-zA-Z가-힣_]$/.test(e),r.isDigit=e=>1===e.length&&/^[0-9]$/.test(e),r.isWhitespace=e=>!(e.length>2)&&/^(\r\n|[ \t\r\n])$/.test(e)},817:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.getInfixBindingPower=r.bindingPowers=void 0,r.bindingPowers={lowest:{left:0,right:1},assignment:{left:31,right:30},comparison:{left:41,right:40},summative:{left:50,right:51},productive:{left:60,right:61},prefix:{left:70,right:71},call:{left:80,right:81}},r.getInfixBindingPower=e=>{switch(e){case"=":return r.bindingPowers.assignment;case"==":case"!=":case">":case"<":case">=":case"<=":return r.bindingPowers.comparison;case"+":case"-":return r.bindingPowers.summative;case"*":case"/":return r.bindingPowers.productive;case"(":return r.bindingPowers.call;default:return r.bindingPowers.lowest}}},522:function(e,r,t){var a=this&&this.__createBinding||(Object.create?function(e,r,t,a){void 0===a&&(a=t);var n=Object.getOwnPropertyDescriptor(r,t);n&&!("get"in n?!r.__esModule:n.writable||n.configurable)||(n={enumerable:!0,get:function(){return r[t]}}),Object.defineProperty(e,a,n)}:function(e,r,t,a){void 0===a&&(a=t),e[a]=r[t]}),n=this&&this.__setModuleDefault||(Object.create?function(e,r){Object.defineProperty(e,"default",{enumerable:!0,value:r})}:function(e,r){e.default=r}),i=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var r={};if(null!=e)for(var t in e)"default"!==t&&Object.prototype.hasOwnProperty.call(e,t)&&a(r,e,t);return n(r,e),r},o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(r,"__esModule",{value:!0}),r.BadSeparatorError=r.BadIdentifierError=r.BadFunctionKeywordError=r.BadAssignmentError=r.BadBlockDelimiterError=r.BadGroupDelimiterError=r.BadExpressionError=r.BadInfixError=r.BadPrefixError=r.BadBooleanLiteralError=r.BadNumberLiteralError=r.ParserError=void 0;const s=i(t(39)),c=t(817),u=t(548);class l extends Error{constructor(e,r,t){super(),this.received=e,this.expected=r,this.range=t}}r.ParserError=l;class d extends l{}r.BadNumberLiteralError=d;class p extends l{}r.BadBooleanLiteralError=p;class h extends l{}r.BadPrefixError=h;class f extends l{}r.BadInfixError=f;class g extends l{}r.BadExpressionError=g;class v extends l{}r.BadGroupDelimiterError=v;class x extends l{}r.BadBlockDelimiterError=x;class m extends l{}r.BadAssignmentError=m;class b extends l{}r.BadFunctionKeywordError=b;class y extends l{}r.BadIdentifierError=y;class E extends l{}r.BadSeparatorError=E;const w=o(t(405));class _{constructor(e){this.reader=new w.default(e)}parseSource(){const e=[];for(;!this.reader.isEnd();)e.push(this.parseStatement());const r={row:0,col:0},t=e.length>0?e[0].range.begin:r,a=e.length>0?e[e.length-1].range.end:r;return s.createProgramNode({statements:e},t,a)}parseBlock(){const e=this.reader.read();this.advanceOrThrow("block delimiter","{",x);const r=[];for(;;){const t=this.reader.read();if("block delimiter"===t.type&&"}"===t.value){this.reader.advance();const a=(0,u.copyRange)(e.range.begin,t.range.end);return s.createBlockNode({statements:r},a)}const a=this.parseStatement();r.push(a)}}parseStatement(){const e=this.reader.read(),{type:r,value:t}=e;return"keyword"===r&&"만약"===t?this.parseBranchStatement():"keyword"===r&&"결과"===t?this.parseReturnStatement():this.parseExpressionStatement()}parseBranchStatement(){const e=this.reader.read();this.reader.advance();const r=this.parseExpression(c.bindingPowers.lowest),t=this.parseBlock(),a=this.reader.read();if("keyword"!==a.type||"아니면"!==a.value){const a={begin:e.range.begin,end:t.range.end};return s.createBranchNode({predicate:r,consequence:t},a)}this.reader.advance();const n=this.parseBlock(),i={begin:e.range.begin,end:n.range.end};return s.createBranchNode({predicate:r,consequence:t,alternative:n},i)}parseReturnStatement(){const e=this.reader.read();this.reader.advance();const r=this.parseExpression(c.bindingPowers.lowest),t={begin:e.range.begin,end:r.range.end};return s.createReturnNode({expression:r},t)}parseExpressionStatement(){const e=this.parseExpression(c.bindingPowers.lowest),r=e.range;return s.createExpressionStatementNode({expression:e},r)}parseExpression(e){let r=this.parseExpressionStart();for(;!((0,c.getInfixBindingPower)(this.reader.read().value).left<=e.right);){const e=this.parseExpressionMiddle(r);if(null===e)break;r=e}return r}parseExpressionStart(){const{type:e,value:r,range:t}=this.reader.read();if("number literal"===e)return this.parseNumberLiteral();if("boolean literal"===e)return this.parseBooleanLiteral();if("string literal"===e)return this.parseStringLiteral();if("identifier"===e)return this.parseIdentifier();if("operator"===e&&this.isPrefixOperator(r))return this.parsePrefix();if("keyword"===e&&"함수"===r)return this.parseFunction();if("group delimiter"===e&&"("===r)return this.parseGroupedExpression();throw new g(e,"expression",t)}parseExpressionMiddle(e){const{type:r,value:t}=this.reader.read();return"group delimiter"===r&&"("===t?"function"!==e.type&&"identifier"!==e.type?null:this.parseCall(e):"operator"===r&&this.isInfixOperator(t)?this.parseInfix(e):"operator"===r&&"="===t&&"identifier"===e.type?this.parseAssignment(e):null}parseCall(e){this.advanceOrThrow("group delimiter","(",v);const r=this.reader.read();if("group delimiter"===r.type&&")"===r.value){this.reader.advance();const t=(0,u.copyRange)(e.range.begin,r.range.end);return s.createCallNode({func:e,args:[]},t)}const t=[this.parseExpression(c.bindingPowers.lowest)];for(;"separator"===this.reader.read().type;)this.reader.advance(),t.push(this.parseExpression(c.bindingPowers.lowest));const a=this.reader.read();this.advanceOrThrow("group delimiter",")",v);const n=(0,u.copyRange)(e.range.begin,a.range.end);return s.createCallNode({func:e,args:t},n)}parseAssignment(e){const{value:r,range:t}=this.reader.read();if(this.reader.advance(),"="!==r)throw new m(r,"=",t);const a=r,n=(0,c.getInfixBindingPower)(a),i=this.parseExpression(n),o={begin:e.range.begin,end:i.range.end};return s.createAssignmentNode({left:e,right:i},o)}parseNumberLiteral(){const{value:e,range:r}=this.reader.read();this.reader.advance();const t=Number(e);if(Number.isNaN(t))throw new d(e,"non NaN",r);return s.createNumberNode({value:t},r)}parseBooleanLiteral(){const{value:e,range:r}=this.reader.read();let t;if(this.reader.advance(),"참"===e)t=!0;else{if("거짓"!==e)throw new p(e,"참, 거짓",r);t=!1}return s.createBooleanNode({value:t},r)}parseStringLiteral(){const{value:e,range:r}=this.reader.read();return this.reader.advance(),s.createStringNode({value:e},r)}parseIdentifier(){const{type:e,value:r,range:t}=this.reader.read();if(this.reader.advance(),"identifier"!==e)throw new y(e,"identifier",t);return s.createIdentifierNode({value:r},t)}parsePrefix(){const{value:e,range:r}=this.reader.read();if(this.reader.advance(),!this.isPrefixOperator(e))throw new h(e,"prefix operator",r);const t=e,a=this.parseExpression(c.bindingPowers.prefix);return s.createPrefixNode({prefix:t,right:a},r)}parseInfix(e){const{value:r,range:t}=this.reader.read();if(this.reader.advance(),!this.isInfixOperator(r))throw new f(r,"infix operator",t);const a=r,n=(0,c.getInfixBindingPower)(a),i=this.parseExpression(n),o=(0,u.copyRange)(e.range.begin,i.range.end);return s.createInfixNode({infix:a,left:e,right:i},o)}parseFunction(){const e=this.reader.read();this.advanceOrThrow("keyword","함수",b);const r=this.parseParameters(),t=this.parseBlock(),a=(0,u.copyRange)(e.range.begin,t.range.end);return s.createFunctionNode({parameters:r,body:t},a)}parseParameters(){this.advanceOrThrow("group delimiter","(",v);const e=this.reader.read();if("group delimiter"===e.type&&")"===e.value)return this.reader.advance(),[];const r=[this.parseIdentifier()];for(;;){const e=this.reader.read();if(this.reader.advance(),"group delimiter"===e.type&&")"===e.value)return r;if("separator"!==e.type)throw new E(e.type,",",e.range);r.push(this.parseIdentifier())}}parseGroupedExpression(){this.advanceOrThrow("group delimiter","(",v);const e=this.parseExpression(c.bindingPowers.lowest);this.advanceOrThrow("group delimiter",")",v);const r=(0,u.copyRange)(e.range.begin,e.range.end,{begin:{row:0,col:-1},end:{row:0,col:1}});return Object.assign(Object.assign({},e),{range:r})}advanceOrThrow(e,r,t){const a=this.reader.read();if(this.reader.advance(),a.type!==e||a.value!==r)throw new t(a.value,r,a.range)}isPrefixOperator(e){return _.PREFIX_OPERATORS.some((r=>r===e))}isInfixOperator(e){return _.INFIX_OPERATORS.some((r=>r===e))}}_.PREFIX_OPERATORS=["+","-","!"],_.INFIX_OPERATORS=["+","-","*","/","!=","==",">","<",">=","<="],r.default=_},405:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.default=class{constructor(e){this.lexer=e,this.token=e.getSourceToken()}read(){return this.token}advance(){this.token=this.lexer.getSourceToken()}isEnd(){return"end"===this.token.type}}},662:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createNodeCreator=void 0;const a=t(548);r.createNodeCreator=function(e){return function(r,t,n){if(void 0!==n)return Object.assign({type:e,range:(0,a.copyRange)(t,n)},r);const i=t;return Object.assign({type:e,range:(0,a.copyRange)(i.begin,i.end)},r)}}},878:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createAssignmentNode=r.createCallNode=r.createFunctionNode=r.createInfixNode=r.createPrefixNode=r.createStringNode=r.createBooleanNode=r.createNumberNode=r.createIdentifierNode=void 0;const a=t(662);r.createIdentifierNode=(0,a.createNodeCreator)("identifier"),r.createNumberNode=(0,a.createNodeCreator)("number"),r.createBooleanNode=(0,a.createNodeCreator)("boolean"),r.createStringNode=(0,a.createNodeCreator)("string"),r.createPrefixNode=(0,a.createNodeCreator)("prefix"),r.createInfixNode=(0,a.createNodeCreator)("infix"),r.createFunctionNode=(0,a.createNodeCreator)("function"),r.createCallNode=(0,a.createNodeCreator)("call"),r.createAssignmentNode=(0,a.createNodeCreator)("assignment")},701:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createBlockNode=r.createProgramNode=void 0;const a=t(662);r.createProgramNode=(0,a.createNodeCreator)("program"),r.createBlockNode=(0,a.createNodeCreator)("block")},39:function(e,r,t){var a=this&&this.__createBinding||(Object.create?function(e,r,t,a){void 0===a&&(a=t);var n=Object.getOwnPropertyDescriptor(r,t);n&&!("get"in n?!r.__esModule:n.writable||n.configurable)||(n={enumerable:!0,get:function(){return r[t]}}),Object.defineProperty(e,a,n)}:function(e,r,t,a){void 0===a&&(a=t),e[a]=r[t]}),n=this&&this.__exportStar||function(e,r){for(var t in e)"default"===t||Object.prototype.hasOwnProperty.call(r,t)||a(r,e,t)};Object.defineProperty(r,"__esModule",{value:!0}),n(t(701),r),n(t(234),r),n(t(878),r)},234:(e,r,t)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.createExpressionStatementNode=r.createReturnNode=r.createBranchNode=void 0;const a=t(662);r.createBranchNode=(0,a.createNodeCreator)("branch"),r.createReturnNode=(0,a.createNodeCreator)("return"),r.createExpressionStatementNode=(0,a.createNodeCreator)("expression statement")},548:(e,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.copyRange=r.copyPosition=void 0,r.copyPosition=(e,r)=>{var t,a;return{row:e.row+(null!==(t=null==r?void 0:r.row)&&void 0!==t?t:0),col:e.col+(null!==(a=null==r?void 0:r.col)&&void 0!==a?a:0)}},r.copyRange=(e,t,a)=>({begin:(0,r.copyPosition)(e,null==a?void 0:a.begin),end:(0,r.copyPosition)(t,null==a?void 0:a.end)})}},r={},t=function t(a){var n=r[a];if(void 0!==n)return n.exports;var i=r[a]={exports:{}};return e[a].call(i.exports,i,i.exports,t),i.exports}(436);window.kal=t})(); \ No newline at end of file diff --git a/src/evaluator/environment/index.test.ts b/src/evaluator/environment/index.test.ts index 91741c3..bd145a8 100644 --- a/src/evaluator/environment/index.test.ts +++ b/src/evaluator/environment/index.test.ts @@ -1,11 +1,11 @@ -import type { Evaluated } from "../evaluated"; +import type { Value } from "../value"; import Environment from "./"; describe("set()", () => { it("set name and value", () => { const env = new Environment(); const varName = "foo"; - const varValue = {} as Evaluated; + const varValue = {} as Value; expect(() => env.set(varName, varValue)).not.toThrow(); }); @@ -15,7 +15,7 @@ describe("get()", () => { it("get value after setting the value", () => { const env = new Environment(); const varName = "foo"; - const varValue = {} as Evaluated; + const varValue = {} as Value; env.set(varName, varValue); @@ -33,7 +33,7 @@ describe("get()", () => { describe("linked environment", () => { it("set super environment and get via sub environment", () => { const varNameInSuper = "foo"; - const varValueInSuper = {} as Evaluated; + const varValueInSuper = {} as Value; const superEnv = new Environment(); superEnv.set(varNameInSuper, varValueInSuper); diff --git a/src/evaluator/environment/index.ts b/src/evaluator/environment/index.ts index a27a929..5fd9a4d 100644 --- a/src/evaluator/environment/index.ts +++ b/src/evaluator/environment/index.ts @@ -1,8 +1,8 @@ -import type { Evaluated } from "../evaluated"; +import type { Value } from "../value"; export interface EnvironmentType { - get: (name: string) => Evaluated | null; - set: (name: string, value: Evaluated) => void; + get: (name: string) => Value | null; + set: (name: string, value: Value) => void; } export default class Environment implements EnvironmentType { @@ -14,7 +14,7 @@ export default class Environment implements EnvironmentType { this.table = new Map; } - get(name: string): Evaluated | null { + get(name: string): Value | null { // return if found in current environment const fetched = this.table.get(name); if (fetched !== undefined) { @@ -28,7 +28,7 @@ export default class Environment implements EnvironmentType { return this.superEnvironment.get(name); } - set(name: string, value: Evaluated): void { + set(name: string, value: Value): void { this.table.set(name, value); } } diff --git a/src/evaluator/environment/v2.test.ts b/src/evaluator/environment/v2.test.ts deleted file mode 100644 index 7ba9f52..0000000 --- a/src/evaluator/environment/v2.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { Value } from "../value"; -import Environment from "./v2"; - -describe("set()", () => { - it("set name and value", () => { - const env = new Environment(); - const varName = "foo"; - const varValue = {} as Value; - - 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 Value; - - env.set(varName, varValue); - - expect(env.get(varName)).toBe(varValue); - }); - - it("get null if not found", () => { - const env = new Environment(); - const varNameNotSet = "foo"; - - expect(env.get(varNameNotSet)).toBe(null); - }); -}); - -describe("linked environment", () => { - it("set super environment and get via sub environment", () => { - const varNameInSuper = "foo"; - const varValueInSuper = {} as Value; - - const superEnv = new Environment(); - superEnv.set(varNameInSuper, varValueInSuper); - const subEnv = new Environment(superEnv); - - 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 varNameSetNowhere = "foo"; - expect(subEnv.get(varNameSetNowhere)).toBe(null); - }); -}); diff --git a/src/evaluator/environment/v2.ts b/src/evaluator/environment/v2.ts deleted file mode 100644 index 5fd9a4d..0000000 --- a/src/evaluator/environment/v2.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Value } from "../value"; - -export interface EnvironmentType { - get: (name: string) => Value | null; - set: (name: string, value: Value) => void; -} - -export default class Environment implements EnvironmentType { - private readonly superEnvironment: Environment | null; - private readonly table: Map; - - constructor(superEnvironment: Environment | null = null) { - this.superEnvironment = superEnvironment; - this.table = new Map; - } - - get(name: string): Value | 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: Value): void { - this.table.set(name, value); - } -} diff --git a/src/evaluator/evaluated/index.test.ts b/src/evaluator/evaluated/index.test.ts deleted file mode 100644 index c2ee547..0000000 --- a/src/evaluator/evaluated/index.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { - makeEvaluatedNumber, - makeEvaluatedString, - makeEvaluatedBoolean, - makeEvaluatedFunction, - makeEvaluatedEmpty, - wrapReturnValue, -} from "./"; -import type { Evaluated } 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("(비어있음)"); - }); -}); - -describe("wrapReturnValue()", () => { - it("wrap return value", () => { - const valueMock = {} as Evaluated; - - const wrapped = wrapReturnValue(valueMock); - - expect(wrapped.type).toBe("return value"); - }); -}); diff --git a/src/evaluator/evaluated/index.ts b/src/evaluator/evaluated/index.ts deleted file mode 100644 index b534735..0000000 --- a/src/evaluator/evaluated/index.ts +++ /dev/null @@ -1,106 +0,0 @@ -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 interface ReturnValue { - readonly type: "return value"; - readonly value: Evaluated; -} - -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 "(비어있음)"; - }, -}); - -export type WrapReturnValue = (value: Evaluated) => ReturnValue; -export const wrapReturnValue: WrapReturnValue = value => ({ - type: "return value", - value, -}); diff --git a/src/evaluator/index.test.ts b/src/evaluator/index.test.ts index 6118bf8..09daf46 100644 --- a/src/evaluator/index.test.ts +++ b/src/evaluator/index.test.ts @@ -1,29 +1,33 @@ import Lexer from "../lexer"; import Parser from "../parser"; -import type { EvaluatedPrimitive, EvaluatedFunction, EvaluatedEmpty } from "./evaluated"; -import Evaluator from "./"; +import Evaluator, * as Eval from "./"; import Environment from "./environment"; const evaluateInput = (input: string) => { const lexer = new Lexer(input); const parser = new Parser(lexer); - const program = parser.parseProgram(); + const parsed = parser.parseSource(); const evaluator = new Evaluator(); - const environment = new Environment(); - const evaluated = evaluator.evaluate(program, environment); - + const env = new Environment(); + const evaluated = evaluator.evaluate(parsed, env); return evaluated; }; const testEvaluatingPrimitive = ({ input, expected }: { input: string, expected: any }): void => { - const evaluated = evaluateInput(input) as EvaluatedPrimitive; + const evaluated = evaluateInput(input) as any; expect(evaluated.value).toBe(expected); }; +const testEvaluatingEmpty = ({ input }: { input: string }): void => { + const evaluated = evaluateInput(input) as any; + + expect(evaluated.value).toBe(null); +}; + const testEvaluatingFunction = ({ input, expectedParamsLength }: { input: string, expectedParamsLength: number }): void => { - const evaluated = evaluateInput(input) as EvaluatedFunction; + const evaluated = evaluateInput(input) as any; expect(evaluated).toHaveProperty("parameters"); expect(evaluated.parameters.length).toBe(expectedParamsLength); @@ -31,12 +35,6 @@ const testEvaluatingFunction = ({ input, expectedParamsLength }: { input: string expect(evaluated).toHaveProperty("environment"); }; -const testEvaluatingEmpty = ({ input }: { input: string }): void => { - const evaluated = evaluateInput(input) as EvaluatedEmpty; - - expect(evaluated.type).toBe("empty"); -}; - describe("evaluate()", () => { describe("single numbers", () => { const cases = [ @@ -358,4 +356,43 @@ describe("evaluate()", () => { it.each(cases)("evaluate $name", testEvaluatingPrimitive); }); + + describe("errors", () => { + const cases = [ + { + name: "top level return error", + input: "결과 11", + expected: Eval.TopLevelReturnError, + range: { begin: { row: 0, col: 0 }, end: { row: 0, col: 4 } }, + }, + { + name: "bad predicate error", + input: "만약 11 {\n 22\n}", + expected: Eval.BadPredicateError, + range: { begin: { row: 0, col: 3 }, end: { row: 0, col: 4 } }, + received: "11", + }, + { + name: "bad identifier error", + input: "사과", + expected: Eval.BadIdentifierError, + range: { begin: { row: 0, col: 0 }, end: { row: 0, col: 1 } }, + received: "사과", + }, + ]; + + it.each(cases)("$name", ({ input, expected, range, received }) => { + expect(() => evaluateInput(input)).toThrow(expected); + try { + evaluateInput(input); + } catch (err) { + const e = err as typeof expected; + + expect(e).toMatchObject({ range }); + if (received !== undefined) { + expect(e).toMatchObject({ received }); + } + } + }); + }); }); diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index 9ab12e1..3064b69 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -1,183 +1,193 @@ -import type { - Program, - Block, - Statement, - BranchStatement, - ExpressionStatement, - Expression, - Identifier, - PrefixExpression, - InfixExpression, - FunctionExpression, - Call, - Assignment, -} from "../parser"; -import { - makeEvaluatedNumber, - makeEvaluatedBoolean, - makeEvaluatedString, - makeEvaluatedFunction, - makeEvaluatedEmpty, - wrapReturnValue, -} from "./evaluated"; -import type { - Evaluated, - EvaluatedNumber, - EvaluatedBoolean, - EvaluatedFunction, - ReturnValue, -} from "./evaluated"; +import type * as Node from "../parser"; +import type * as Value from "./value"; +import * as value from "./value"; import Environment from "./environment"; +import type { Range } from "../util/position"; + +export class EvalError extends Error { + public range: Range; + public received?: string; + + constructor(range: Range, received?: string) { + super(); + this.range = range; + this.received = received; + } +} + +export class TopLevelReturnError extends EvalError {}; +export class BadPredicateError extends EvalError {}; +export class BadAssignmentLeftError extends EvalError {}; +export class BadPrefixExpressionError extends EvalError {}; +export class BadInfixExpressionError extends EvalError {}; +export class BadIdentifierError extends EvalError {}; + +type ComparisonOperator = "==" | "!=" | ">" | "<" | ">=" | "<="; -/** @deprecated */ export default class Evaluator { - evaluate(node: Program, env: Environment): Evaluated { + evaluate(node: Node.ProgramNode, env: Environment): Value.Value { return this.evaluateProgram(node, env); } - private evaluateProgram(node: Program, env: Environment): Evaluated { + private evaluateProgram(node: Node.ProgramNode, env: Environment): Value.Value { const { statements } = node; - if (statements.length === 0) { - return makeEvaluatedEmpty(); - } - // loop except the last statement + let lastEvaluated: Value.Value | null = null; for (let i = 0; i < statements.length; ++i) { - const statement = statements[i]; - const evaluated = this.evaluateStatement(statement, env); - if (evaluated.type === "return value") { - throw new Error(`return value cannot appear in top level scope`); + const evaluated = this.evaluateStatement(statements[i], env); + + if (evaluated.type === "return") { + throw new TopLevelReturnError(node.range); } - } - // return the last evaluated value - const lastStatement = statements[statements.length-1]; - const evaluated = this.evaluateStatement(lastStatement, env); - if (evaluated.type === "return value") { - throw new Error(`return value cannot appear in top level scope`); + lastEvaluated = evaluated; } - return evaluated; + + return lastEvaluated ?? this.createEmptyValue(node.range); } - private evaluateBlock(node: Block, env: Environment): Evaluated | ReturnValue { - const { statements } = node; - if (statements.length === 0) { - throw new Error(`block cannot be empty`); + private evaluateStatement(node: Node.StatementNode, env: Environment): Value.Value | Value.ReturnValue { + if (node.type === "branch") { + return this.evaluateBranchStatement(node, env); } - - // loop except the last statement - for (let i = 0; i < statements.length; ++i) { - const statement = statements[i]; - const evaluated = this.evaluateStatement(statement, env); - if (evaluated.type === "return value") { // early return if return statement encoutered - return evaluated; - } + if (node.type === "expression statement") { + return this.evaluateExpressionStatement(node, env); + } + if (node.type === "return") { + const val = this.evaluateExpression(node.expression, env); + return value.createReturnValue(val); } - const lastStatement = statements[statements.length-1]; - const evaluated = this.evaluateStatement(lastStatement, env); - return evaluated; + const nothing: never = node; + return nothing; } - private evaluatePrefixExpression(node: PrefixExpression, env: Environment): Evaluated { - const subExpression = this.evaluateExpression(node.expression, env); + private evaluateBranchStatement(node: Node.BranchNode, env: Environment): Value.Value | Value.ReturnValue { + const pred = this.evaluateExpression(node.predicate, env); + if (pred.type !== "boolean") { + throw new BadPredicateError(pred.range, pred.representation); + } - if ( - (node.prefix === "+" || node.prefix === "-") && - subExpression.type == "number" - ) { - return this.evaluatePrefixNumberExpression(node.prefix, subExpression); + if (pred.value) { + return this.evaluateBlock(node.consequence, env); } - if (node.prefix === "!" && subExpression.type === "boolean") { - return this.evaluatePrefixBooleanExpression(node.prefix, subExpression); + + if (node.alternative === undefined) { + return this.createEmptyValue(node.range); } - throw new Error(`bad prefix expression: prefix: '${node.prefix}' with type: '${typeof subExpression}'`); + return this.evaluateBlock(node.alternative, env); } - private evaluatePrefixNumberExpression(prefix: string, operand: EvaluatedNumber): EvaluatedNumber { - if (prefix === "+") { - return operand; - } - if (prefix === "-") { - return makeEvaluatedNumber(-operand.value); + private evaluateBlock(node: Node.BlockNode, env: Environment): Value.Value | Value.ReturnValue { + let lastEvaluated: Value.Value | null = null; + + for (let i = 0; i < node.statements.length; ++i) { + const evaluated = this.evaluateStatement(node.statements[i], env); + + if (evaluated.type === "return") { + return evaluated; + } + + lastEvaluated = evaluated; } - throw new Error(`bad prefix ${prefix}`); + return lastEvaluated ?? this.createEmptyValue(node.range); } - private evaluatePrefixBooleanExpression(prefix: string, operand: EvaluatedBoolean): EvaluatedBoolean { - if (prefix === "!") { - return makeEvaluatedBoolean(!operand.value); + private evaluateExpressionStatement(node: Node.ExpressionStatementNode, env: Environment): Value.Value { + return this.evaluateExpression(node.expression, env); + } + + private evaluateExpression(node: Node.ExpressionNode, env: Environment): Value.Value { + if (node.type === "number") { + return this.createNumberValue(node.value, node.range); + } + if (node.type === "boolean") { + return this.createBooleanValue(node.value, node.range); + } + if (node.type === "string") { + return this.createStringValue(node.value, node.range); + } + if (node.type === "prefix") { + return this.evaluatePrefixExpression(node, env); + } + if (node.type === "infix") { + return this.evaluateInfixExpression(node, env); + } + if (node.type === "assignment") { + return this.evaluateAssignment(node, env); + } + if (node.type === "identifier") { + return this.evaluateIdentifier(node, env); + } + if (node.type === "function") { + return this.evaluateFunctionExpression(node, env); + } + if (node.type === "call") { + return this.evaluateCall(node, env); } - throw new Error(`bad prefix ${prefix}`); + const _never: never = node; + return _never; } - private evaluateStatement(node: Statement, env: Environment): Evaluated | ReturnValue { - if (node.type === "branch statement") { - return this.evaluateBranchStatement(node, env); - } + private evaluatePrefixExpression(node: Node.PrefixNode, env: Environment): Value.Value { + const right = this.evaluateExpression(node.right, env); - if (node.type === "expression statement") { - return this.evaluateExpressionStatement(node, env); + if ((node.prefix === "+" || node.prefix === "-") && right.type == "number") { + return this.evaluatePrefixNumberExpression(node.prefix, right); } - - if (node.type === "return statement") { - const value = this.evaluateExpression(node.expression, env); - return wrapReturnValue(value); + if (node.prefix === "!" && right.type === "boolean") { + return this.evaluatePrefixBooleanExpression(node.prefix, right); } - const nothing: never = node; - return nothing; + throw new BadPrefixExpressionError(node.range); } - private evaluateBranchStatement(node: BranchStatement, env: Environment): Evaluated | ReturnValue { - const predicate = this.evaluateExpression(node.predicate, env); - if (predicate.type !== "boolean") { - throw new Error(`expected boolean expression predicate, but received ${predicate.type}`); + private evaluateInfixExpression(node: Node.InfixNode, env: Environment): Value.Value { + const left = this.evaluateExpression(node.left, env); + const right = this.evaluateExpression(node.right, env); + + if (left.type === "number" && right.type === "number" && this.isArithmeticInfixOperator(node.infix)) { + const value = this.getArithmeticInfixOperationValue(left.value, right.value, node.infix); + return this.createNumberValue(value, node.range); } - if (predicate.value) { - const consequence = this.evaluateBlock(node.consequence, env); - return consequence; + if (left.type === "number" && right.type === "number" && this.isComparisonInfixOperator(node.infix)) { + const value = this.getNumericComparisonInfixOperationValue(left.value, right.value, node.infix); + return this.createBooleanValue(value, node.range); } - // early return if no else block - if (typeof node.alternative === "undefined") { - return makeEvaluatedEmpty(); + if (left.type === "boolean" && right.type === "boolean" && this.isComparisonInfixOperator(node.infix)) { + const value = this.getBooleanComparisonInfixOperationValue(left.value, right.value, node.infix); + return this.createBooleanValue(value, node.range); } - const alternative = this.evaluateBlock(node.alternative, env); - return alternative; - } + if (left.type === "string" && right.type === "string" && this.isComparisonInfixOperator(node.infix)) { + const value = this.getStringComparisonInfixOperationValue(left.value, right.value, node.infix); + return this.createBooleanValue(value, node.range); + } - private evaluateExpressionStatement(node: ExpressionStatement, env: Environment): Evaluated { - return this.evaluateExpression(node.expression, env); + throw new BadInfixExpressionError(node.range); } - private evaluateFunctionExpression(node: FunctionExpression, env: Environment): Evaluated { - const parameters = node.parameter; - const body = node.body; - return makeEvaluatedFunction(parameters, body, env); - } + private evaluateIdentifier(node: Node.IdentifierNode, env: Environment): Value.Value { + const varName = node.value; + const value = env.get(varName); - private evaluateCall(node: Call, env: Environment): Evaluated { - const functionToCall = this.evaluateExpression(node.functionToCall, env); - if (functionToCall.type !== "function") { - throw new Error(`expected function but received ${functionToCall.type}`); + if (value === null) { + throw new BadIdentifierError(node.range, varName); } - const callArguments = this.parseCallArguments(node.callArguments, env); - - const value = this.evaluateFunctionCall(functionToCall, callArguments); return value; } - private evaluateAssignment(node: Assignment, env: Environment): Evaluated { + private evaluateAssignment(node: Node.AssignmentNode, env: Environment): Value.Value { if (node.left.type !== "identifier") { - throw new Error(`expected identifier on left value, but received ${typeof node.left.type}`); + throw new BadAssignmentLeftError(node.range); } + const varName = node.left.value; const varValue = this.evaluateExpression(node.right, env); @@ -186,135 +196,156 @@ export default class Evaluator { return varValue; // evaluated value of assignment is the evaluated value of variable } - private evaluateIdentifier(node: Identifier, env: Environment): Evaluated { - const varName = node.value; - const value = env.get(varName); + private evaluateFunctionExpression(node: Node.FunctionNode, env: Environment): Value.Value { + return this.createFunctionValue(node.parameters, node.body, env, node.range); + } - if (value === null) { - throw new Error(`identifier '${varName}' not found`); + private evaluateCall(node: Node.CallNode, env: Environment): Value.Value { + const func = this.evaluateExpression(node.func, env); + if (func.type !== "function") { + throw new Error(`expected function but received ${func.type}`); } + const callArguments = this.evaluateCallArguments(node.args, env); + + const value = this.evaluateFunctionCall(func, callArguments); return value; } - private evaluateExpression(node: Expression, env: Environment): Evaluated { - if (node.type === "number node") { - return makeEvaluatedNumber(node.value); - } + private evaluateCallArguments(args: Node.ExpressionNode[], env: Environment): Value.Value[] { + return args.map(arg => this.evaluateExpression(arg, env)); + } - if (node.type === "boolean node") { - return makeEvaluatedBoolean(node.value); - } + private evaluateFunctionCall(func: Value.FunctionValue, callArguments: Value.Value[]): Value.Value { + const env = this.createExtendedEnvironment(func.environment, func.parameters, callArguments); - if (node.type === "string node") { - return makeEvaluatedString(node.value); + const blockValue = this.evaluateBlock(func.body, env); + if (blockValue.type !== "return") { + // TODO: better error with range + throw new Error(`expected return value in function but it didn't`); } - if (node.type === "infix expression") { - return this.evaluateInfixExpression(node, env); - } + const returnValue = blockValue.value; + return returnValue; + } - if (node.type === "prefix expression") { - return this.evaluatePrefixExpression(node, env); - } + private getBooleanComparisonInfixOperationValue(left: boolean, right: boolean, operator: ComparisonOperator): boolean { + return this.getComparisonInfixOperationValue(left, right, operator); + } - if (node.type === "function expression") { - return this.evaluateFunctionExpression(node, env); - } + private getNumericComparisonInfixOperationValue(left: number, right: number, operator: ComparisonOperator): boolean { + return this.getComparisonInfixOperationValue(left, right, operator); + } - if (node.type === "call") { - return this.evaluateCall(node, env); - } + private getStringComparisonInfixOperationValue(left: string, right: string, operator: ComparisonOperator): boolean { + return this.getComparisonInfixOperationValue(left, right, operator); + } - if (node.type === "assignment") { - return this.evaluateAssignment(node, env); + private getComparisonInfixOperationValue(left: T, right: T, operator: ComparisonOperator): boolean { + if (operator === "==") { + return left === right; } - - if (node.type === "identifier") { - return this.evaluateIdentifier(node, env); + if (operator === "!=") { + return left !== right; + } + if (operator === ">") { + return left > right; + } + if (operator === "<") { + return left < right; + } + if (operator === ">=") { + return left >= right; + } + if (operator === "<=") { + return left <= right; } - const nothing: never = node; - return nothing; + const _never: never = operator; + return _never; } - private evaluateInfixExpression(node: InfixExpression, env: Environment): Evaluated { - const infix = node.infix; - const left = this.evaluateExpression(node.left, env); - const right = this.evaluateExpression(node.right, env); + private getArithmeticInfixOperationValue(left: number, right: number, operator: "+" | "-" | "*" | "/"): number { + if (operator === "+") { + return left + right; + } + if (operator === "-") { + return left - right; + } + if (operator === "*") { + return left * right; + } + if (operator === "/") { + return left / right; + } - // type matching order is important: more inclusive case first + const _never: never = operator; + return _never; + } - if ( - (left.type === "boolean" && right.type === "boolean") || - (left.type === "number" && right.type === "number") || - (left.type === "string" && right.type === "string") - ) { - if (infix === "==") { - return makeEvaluatedBoolean(left.value == right.value); - } - if (infix === "!=") { - return makeEvaluatedBoolean(left.value != right.value); - } - if (infix === ">") { - return makeEvaluatedBoolean(left.value > right.value); - } - if (infix === "<") { - return makeEvaluatedBoolean(left.value < right.value); - } - if (infix === ">=") { - return makeEvaluatedBoolean(left.value >= right.value); - } - if (infix === "<=") { - return makeEvaluatedBoolean(left.value <= right.value); - } + private evaluatePrefixNumberExpression(prefix: "+" | "-", node: Node.NumberNode): Value.NumberValue { + if (prefix === "+") { + return this.createNumberValue(node.value, node.range); + } + if (prefix === "-") { + return this.createNumberValue(-node.value, node.range); } - if (left.type === "number" && right.type === "number") { - if (infix === "+") { - return makeEvaluatedNumber(left.value + right.value); - } - if (infix === "-") { - return makeEvaluatedNumber(left.value - right.value); - } - if (infix === "*") { - return makeEvaluatedNumber(left.value * right.value); - } - if (infix === "/") { - // TODO: guard division by zero - return makeEvaluatedNumber(left.value / right.value); - } + const _never: never = prefix; + return _never; + } - throw new Error(`bad infix ${infix} for number operands`); + private evaluatePrefixBooleanExpression(prefix: "!", node: Node.BooleanNode): Value.BooleanValue { + if (prefix === "!") { + return this.createBooleanValue(!node.value, node.range); } - throw new Error(`bad infix ${infix}, with left '${left}' and right '${right}'`); + const _never: never = prefix; + return _never; } - private parseCallArguments(callArguments: Expression[], env: Environment): Evaluated[] { - const values = []; - for (const arg of callArguments) { - const value = this.evaluateExpression(arg, env); - values.push(value); + private createExtendedEnvironment(oldEnv: Environment, identifiers: Node.IdentifierNode[], values: Value.Value[]): Environment { + const newEnv = new Environment(oldEnv); + + for (let i = 0; i < identifiers.length; ++i) { + const name = identifiers[i].value; + const value = values[i]; + newEnv.set(name, value); } - return values; + + return newEnv; } - 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; - const value = callArguments[i]; - functionEnv.set(name, value); - } + // create value functions: wrappers for consistent representation - const evaluated = this.evaluateBlock(functionToCall.body, functionEnv); - if (evaluated.type !== "return value") { - throw new Error(`expected return value in function but it didn't`); - } + private createNumberValue(val: number, range: Range): Value.NumberValue { + return value.createNumberValue({ value: val }, String(val), range); + } - const returnValue = evaluated.value; - return returnValue; + private createBooleanValue(val: boolean, range: Range): Value.BooleanValue { + return value.createBooleanValue({ value: val }, val ? "참" : "거짓", range); + } + + private createStringValue(val: string, range: Range): Value.StringValue { + return value.createStringValue({ value: val }, val, range); + } + + private createEmptyValue(range: Range): Value.EmptyValue { + return value.createEmptyValue({ value: null }, "(없음)", range); + } + + private createFunctionValue(parameters: Node.FunctionNode["parameters"], body: Node.FunctionNode["body"], environment: Environment, range: Range): Value.FunctionValue { + return value.createFunctionValue({ parameters, body, environment }, "(함수)", range); + } + + // util predicate functions + + private isArithmeticInfixOperator(operator: string): operator is "+" | "-" | "*" | "/" { + return ["+", "-", "*", "/"].some(infix => infix === operator); + } + + private isComparisonInfixOperator(operator: string): operator is ComparisonOperator { + return ["==", "!=", ">", "<", ">=", "<="].some(infix => infix === operator); } } diff --git a/src/evaluator/v2.test.ts b/src/evaluator/v2.test.ts deleted file mode 100644 index 5c82d76..0000000 --- a/src/evaluator/v2.test.ts +++ /dev/null @@ -1,398 +0,0 @@ -import Lexer from "../lexer"; -import Parser from "../parser/v2"; -import Evaluator, * as Eval from "./v2"; -import Environment from "./environment/v2"; - -const evaluateInput = (input: string) => { - const lexer = new Lexer(input); - const parser = new Parser(lexer); - const parsed = parser.parseSource(); - - const evaluator = new Evaluator(); - const env = new Environment(); - const evaluated = evaluator.evaluate(parsed, env); - return evaluated; -}; - -const testEvaluatingPrimitive = ({ input, expected }: { input: string, expected: any }): void => { - const evaluated = evaluateInput(input) as any; - - expect(evaluated.value).toBe(expected); -}; - -const testEvaluatingEmpty = ({ input }: { input: string }): void => { - const evaluated = evaluateInput(input) as any; - - expect(evaluated.value).toBe(null); -}; - -const testEvaluatingFunction = ({ input, expectedParamsLength }: { input: string, expectedParamsLength: number }): void => { - const evaluated = evaluateInput(input) as any; - - expect(evaluated).toHaveProperty("parameters"); - expect(evaluated.parameters.length).toBe(expectedParamsLength); - expect(evaluated).toHaveProperty("body"); - expect(evaluated).toHaveProperty("environment"); -}; - -describe("evaluate()", () => { - 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 }, - { input: "100/25", expected: 4 }, - { 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 }, - ]; - - it.each(cases)("evaluate $input", testEvaluatingPrimitive); - }); - - describe("logical not expressions", () => { - const cases = [ - { input: "!참", expected: false }, - { input: "!거짓", expected: true }, - { input: "!!참", expected: true }, - { input: "!!거짓", expected: false }, - ]; - - 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 }, - { input: "거짓 == 거짓", expected: true }, - { input: "참 != 참", expected: false }, - { 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 }, - { input: "2 >= 1", expected: true }, - { input: "1 >= 1", expected: true }, - { input: "1 >= 2", expected: false }, - { input: "2 < 1", expected: false }, - { input: "1 < 1", expected: false }, - { input: "1 < 2", expected: true }, - { input: "2 <= 1", expected: false }, - { input: "1 <= 1", expected: true }, - { input: "1 <= 2", expected: true }, - ]; - - it.each(cases)("evaluate $input", testEvaluatingPrimitive); - }); - - 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", testEvaluatingPrimitive); - }); - - 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 yielding something", () => { - const cases = [ - { - name: "simple if statement with boolean literal predicate", - input: "만약 참 { 3 }", - expected: 3 - }, - { - name: "simple if statement with boolean expression predicate", - input: "만약 1 != 2 { 4 }", - expected: 4 - }, - { - name: "simple if statement with variable comparison predicate", - input: "사과 = 3 바나나 = 4 만약 사과 < 바나나 { 5 }", - expected: 5 - }, - { - name: "simple if-else statement with true boolean literal predicate", - input: "만약 참 { 6 } 아니면 { 7 }", - expected: 6 - }, - { - name: "simple if-else statement with false boolean literal predicate", - input: "만약 거짓 { 6 } 아니면 { 7 }", - expected: 7 - }, - { - name: "simple if-else statement with boolean expression predicate", - input: "만약 1 == 2 { 34 } 아니면 { 56 }", - expected: 56 - }, - ]; - - 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 }", - }, - ]; - - it.each(cases)("evaluate $input", testEvaluatingEmpty); - }); - - describe("nested branch statements yielding something", () => { - const cases = [ - { - 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 $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 - }, - ]; - - it.each(cases)("evaluate $name", testEvaluatingPrimitive); - }); - - describe("function expressions", () => { - const cases = [ - { - name: "simple function expression", - input: "함수 () { 1 }", - expectedParamsLength: 0, - }, - { - name: "simple function expression", - input: "함수 (사과, 바나나, 포도) { 1 }", - expectedParamsLength: 3, - }, - ]; - - it.each(cases)("evaluate $name", testEvaluatingFunction); - }); - - describe("call expressions", () => { - const cases = [ - { - name: "function call with function literal", - input: "함수(바나나) { 결과 바나나 + 1 }(42)", - expected: 43, - }, - { - name: "function call with identifier", - input: "더하기 = 함수(숫자1, 숫자2) { 결과 숫자1 + 숫자2 } 더하기(3, 4)", - expected: 7, - }, - ]; - - it.each(cases)("evaluate $name", testEvaluatingPrimitive); - }); - - describe("complex statements with function and calls", () => { - const cases = [ - { - name: "make and call function containing branch statement", - input: "과일 = 함수(색깔) { 만약 (색깔 == '빨강') { 결과 '사과' } 아니면 { '포도' } } 과일('빨강')", - expected: "사과", - }, - { - name: "make and call closure", - input: "더하기 = 함수(숫자1) { 결과 함수(숫자2) { 결과 숫자1+숫자2 } } 하나더하기 = 더하기(1) 하나더하기(4)", - expected: 5, - }, - ]; - - it.each(cases)("evaluate $name", testEvaluatingPrimitive); - }); - - describe("errors", () => { - const cases = [ - { - name: "top level return error", - input: "결과 11", - expected: Eval.TopLevelReturnError, - range: { begin: { row: 0, col: 0 }, end: { row: 0, col: 4 } }, - }, - { - name: "bad predicate error", - input: "만약 11 {\n 22\n}", - expected: Eval.BadPredicateError, - range: { begin: { row: 0, col: 3 }, end: { row: 0, col: 4 } }, - received: "11", - }, - { - name: "bad identifier error", - input: "사과", - expected: Eval.BadIdentifierError, - range: { begin: { row: 0, col: 0 }, end: { row: 0, col: 1 } }, - received: "사과", - }, - ]; - - it.each(cases)("$name", ({ input, expected, range, received }) => { - expect(() => evaluateInput(input)).toThrow(expected); - try { - evaluateInput(input); - } catch (err) { - const e = err as typeof expected; - - expect(e).toMatchObject({ range }); - if (received !== undefined) { - expect(e).toMatchObject({ received }); - } - } - }); - }); -}); diff --git a/src/evaluator/v2.ts b/src/evaluator/v2.ts deleted file mode 100644 index 204791d..0000000 --- a/src/evaluator/v2.ts +++ /dev/null @@ -1,352 +0,0 @@ -import type * as Node from "../parser/v2"; -import type * as Value from "./value"; -import * as value from "./value"; -import Environment from "./environment/v2"; -import type { Range } from "../util/position"; - -export class EvalError extends Error { - public range: Range; - public received?: string; - - constructor(range: Range, received?: string) { - super(); - this.range = range; - this.received = received; - } -} - -export class TopLevelReturnError extends EvalError {}; -export class BadPredicateError extends EvalError {}; -export class BadAssignmentLeftError extends EvalError {}; -export class BadPrefixExpressionError extends EvalError {}; -export class BadInfixExpressionError extends EvalError {}; -export class BadIdentifierError extends EvalError {}; - -type ComparisonOperator = "==" | "!=" | ">" | "<" | ">=" | "<="; - -export default class Evaluator { - evaluate(node: Node.ProgramNode, env: Environment): Value.Value { - return this.evaluateProgram(node, env); - } - - private evaluateProgram(node: Node.ProgramNode, env: Environment): Value.Value { - const { statements } = node; - - let lastEvaluated: Value.Value | null = null; - for (let i = 0; i < statements.length; ++i) { - const evaluated = this.evaluateStatement(statements[i], env); - - if (evaluated.type === "return") { - throw new TopLevelReturnError(node.range); - } - - lastEvaluated = evaluated; - } - - return lastEvaluated ?? this.createEmptyValue(node.range); - } - - private evaluateStatement(node: Node.StatementNode, env: Environment): Value.Value | Value.ReturnValue { - if (node.type === "branch") { - return this.evaluateBranchStatement(node, env); - } - if (node.type === "expression statement") { - return this.evaluateExpressionStatement(node, env); - } - if (node.type === "return") { - const val = this.evaluateExpression(node.expression, env); - return value.createReturnValue(val); - } - - const nothing: never = node; - return nothing; - } - - private evaluateBranchStatement(node: Node.BranchNode, env: Environment): Value.Value | Value.ReturnValue { - const pred = this.evaluateExpression(node.predicate, env); - if (pred.type !== "boolean") { - throw new BadPredicateError(pred.range, pred.representation); - } - - if (pred.value) { - return this.evaluateBlock(node.consequence, env); - } - - if (node.alternative === undefined) { - return this.createEmptyValue(node.range); - } - - return this.evaluateBlock(node.alternative, env); - } - - private evaluateBlock(node: Node.BlockNode, env: Environment): Value.Value | Value.ReturnValue { - let lastEvaluated: Value.Value | null = null; - - for (let i = 0; i < node.statements.length; ++i) { - const evaluated = this.evaluateStatement(node.statements[i], env); - - if (evaluated.type === "return") { - return evaluated; - } - - lastEvaluated = evaluated; - } - - return lastEvaluated ?? this.createEmptyValue(node.range); - } - - private evaluateExpressionStatement(node: Node.ExpressionStatementNode, env: Environment): Value.Value { - return this.evaluateExpression(node.expression, env); - } - - private evaluateExpression(node: Node.ExpressionNode, env: Environment): Value.Value { - if (node.type === "number") { - return this.createNumberValue(node.value, node.range); - } - if (node.type === "boolean") { - return this.createBooleanValue(node.value, node.range); - } - if (node.type === "string") { - return this.createStringValue(node.value, node.range); - } - if (node.type === "prefix") { - return this.evaluatePrefixExpression(node, env); - } - if (node.type === "infix") { - return this.evaluateInfixExpression(node, env); - } - if (node.type === "assignment") { - return this.evaluateAssignment(node, env); - } - if (node.type === "identifier") { - return this.evaluateIdentifier(node, env); - } - if (node.type === "function") { - return this.evaluateFunctionExpression(node, env); - } - if (node.type === "call") { - return this.evaluateCall(node, env); - } - - const _never: never = node; - return _never; - } - - private evaluatePrefixExpression(node: Node.PrefixNode, env: Environment): Value.Value { - const right = this.evaluateExpression(node.right, env); - - if ((node.prefix === "+" || node.prefix === "-") && right.type == "number") { - return this.evaluatePrefixNumberExpression(node.prefix, right); - } - if (node.prefix === "!" && right.type === "boolean") { - return this.evaluatePrefixBooleanExpression(node.prefix, right); - } - - throw new BadPrefixExpressionError(node.range); - } - - private evaluateInfixExpression(node: Node.InfixNode, env: Environment): Value.Value { - const left = this.evaluateExpression(node.left, env); - const right = this.evaluateExpression(node.right, env); - - if (left.type === "number" && right.type === "number" && this.isArithmeticInfixOperator(node.infix)) { - const value = this.getArithmeticInfixOperationValue(left.value, right.value, node.infix); - return this.createNumberValue(value, node.range); - } - - if (left.type === "number" && right.type === "number" && this.isComparisonInfixOperator(node.infix)) { - const value = this.getNumericComparisonInfixOperationValue(left.value, right.value, node.infix); - return this.createBooleanValue(value, node.range); - } - - if (left.type === "boolean" && right.type === "boolean" && this.isComparisonInfixOperator(node.infix)) { - const value = this.getBooleanComparisonInfixOperationValue(left.value, right.value, node.infix); - return this.createBooleanValue(value, node.range); - } - - if (left.type === "string" && right.type === "string" && this.isComparisonInfixOperator(node.infix)) { - const value = this.getStringComparisonInfixOperationValue(left.value, right.value, node.infix); - return this.createBooleanValue(value, node.range); - } - - throw new BadInfixExpressionError(node.range); - } - - private evaluateIdentifier(node: Node.IdentifierNode, env: Environment): Value.Value { - const varName = node.value; - const value = env.get(varName); - - if (value === null) { - throw new BadIdentifierError(node.range, varName); - } - - return value; - } - - private evaluateAssignment(node: Node.AssignmentNode, env: Environment): Value.Value { - if (node.left.type !== "identifier") { - throw new BadAssignmentLeftError(node.range); - } - - const varName = node.left.value; - const varValue = this.evaluateExpression(node.right, env); - - env.set(varName, varValue); - - return varValue; // evaluated value of assignment is the evaluated value of variable - } - - private evaluateFunctionExpression(node: Node.FunctionNode, env: Environment): Value.Value { - return this.createFunctionValue(node.parameters, node.body, env, node.range); - } - - private evaluateCall(node: Node.CallNode, env: Environment): Value.Value { - const func = this.evaluateExpression(node.func, env); - if (func.type !== "function") { - throw new Error(`expected function but received ${func.type}`); - } - - const callArguments = this.evaluateCallArguments(node.args, env); - - const value = this.evaluateFunctionCall(func, callArguments); - return value; - } - - private evaluateCallArguments(args: Node.ExpressionNode[], env: Environment): Value.Value[] { - return args.map(arg => this.evaluateExpression(arg, env)); - } - - private evaluateFunctionCall(func: Value.FunctionValue, callArguments: Value.Value[]): Value.Value { - const env = this.createExtendedEnvironment(func.environment, func.parameters, callArguments); - - const blockValue = this.evaluateBlock(func.body, env); - if (blockValue.type !== "return") { - // TODO: better error with range - throw new Error(`expected return value in function but it didn't`); - } - - const returnValue = blockValue.value; - return returnValue; - } - - private getBooleanComparisonInfixOperationValue(left: boolean, right: boolean, operator: ComparisonOperator): boolean { - return this.getComparisonInfixOperationValue(left, right, operator); - } - - private getNumericComparisonInfixOperationValue(left: number, right: number, operator: ComparisonOperator): boolean { - return this.getComparisonInfixOperationValue(left, right, operator); - } - - private getStringComparisonInfixOperationValue(left: string, right: string, operator: ComparisonOperator): boolean { - return this.getComparisonInfixOperationValue(left, right, operator); - } - - private getComparisonInfixOperationValue(left: T, right: T, operator: ComparisonOperator): boolean { - if (operator === "==") { - return left === right; - } - if (operator === "!=") { - return left !== right; - } - if (operator === ">") { - return left > right; - } - if (operator === "<") { - return left < right; - } - if (operator === ">=") { - return left >= right; - } - if (operator === "<=") { - return left <= right; - } - - const _never: never = operator; - return _never; - } - - private getArithmeticInfixOperationValue(left: number, right: number, operator: "+" | "-" | "*" | "/"): number { - if (operator === "+") { - return left + right; - } - if (operator === "-") { - return left - right; - } - if (operator === "*") { - return left * right; - } - if (operator === "/") { - return left / right; - } - - const _never: never = operator; - return _never; - } - - private evaluatePrefixNumberExpression(prefix: "+" | "-", node: Node.NumberNode): Value.NumberValue { - if (prefix === "+") { - return this.createNumberValue(node.value, node.range); - } - if (prefix === "-") { - return this.createNumberValue(-node.value, node.range); - } - - const _never: never = prefix; - return _never; - } - - private evaluatePrefixBooleanExpression(prefix: "!", node: Node.BooleanNode): Value.BooleanValue { - if (prefix === "!") { - return this.createBooleanValue(!node.value, node.range); - } - - const _never: never = prefix; - return _never; - } - - private createExtendedEnvironment(oldEnv: Environment, identifiers: Node.IdentifierNode[], values: Value.Value[]): Environment { - const newEnv = new Environment(oldEnv); - - for (let i = 0; i < identifiers.length; ++i) { - const name = identifiers[i].value; - const value = values[i]; - newEnv.set(name, value); - } - - return newEnv; - } - - // create value functions: wrappers for consistent representation - - private createNumberValue(val: number, range: Range): Value.NumberValue { - return value.createNumberValue({ value: val }, String(val), range); - } - - private createBooleanValue(val: boolean, range: Range): Value.BooleanValue { - return value.createBooleanValue({ value: val }, val ? "참" : "거짓", range); - } - - private createStringValue(val: string, range: Range): Value.StringValue { - return value.createStringValue({ value: val }, val, range); - } - - private createEmptyValue(range: Range): Value.EmptyValue { - return value.createEmptyValue({ value: null }, "(없음)", range); - } - - private createFunctionValue(parameters: Node.FunctionNode["parameters"], body: Node.FunctionNode["body"], environment: Environment, range: Range): Value.FunctionValue { - return value.createFunctionValue({ parameters, body, environment }, "(함수)", range); - } - - // util predicate functions - - private isArithmeticInfixOperator(operator: string): operator is "+" | "-" | "*" | "/" { - return ["+", "-", "*", "/"].some(infix => infix === operator); - } - - private isComparisonInfixOperator(operator: string): operator is ComparisonOperator { - return ["==", "!=", ">", "<", ">=", "<="].some(infix => infix === operator); - } -} - -export { default as Environment } from "./environment/v2"; diff --git a/src/evaluator/value/index.ts b/src/evaluator/value/index.ts index aad540d..1261beb 100644 --- a/src/evaluator/value/index.ts +++ b/src/evaluator/value/index.ts @@ -1,6 +1,6 @@ import { copyRange, type Range } from "../../util/position"; -import type { FunctionNode } from "../../parser/v2"; -import Environment from "../environment/v2"; +import type { FunctionNode } from "../../parser"; +import Environment from "../environment"; export interface ValueBase { readonly type: T, diff --git a/src/index.ts b/src/index.ts index 86652f1..c52c336 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import Lexer from "./lexer"; -import Parser from "./parser/v2"; -import Evaluator, { Environment } from "./evaluator/v2"; +import Parser from "./parser"; +import Evaluator, { Environment } from "./evaluator"; export const execute = (input: string): string => { const lexer = new Lexer(input); diff --git a/src/lexer/char-buffer/char-reader/index.test.ts b/src/lexer/char-buffer/char-reader/index.test.ts index da97225..c011c26 100644 --- a/src/lexer/char-buffer/char-reader/index.test.ts +++ b/src/lexer/char-buffer/char-reader/index.test.ts @@ -72,42 +72,3 @@ describe("advance()", () => { expect(char2).toEqual(expected2); }); }); - -describe("read()", () => { - it("read a character", () => { - const reader = new Reader("a", "\0"); - - expect(reader.read()).toBe("a"); - }); - - it("read the same character twice", () => { - const reader = new Reader("a", "\0"); - - expect(reader.read()).toBe("a"); - expect(reader.read()).toBe("a"); - }); - - it("read fallback character if end of input", () => { - const reader = new Reader("", "\0"); - - expect(reader.read()).toBe("\0"); - expect(reader.read()).toBe("\0"); - }); -}); - -describe("next()", () => { - it("increment index and read next character", () => { - const reader = new Reader("ab", "\0"); - - reader.next(); - expect(reader.read()).toBe("b"); - }); - - it("not increment index if end of input", () => { - const reader = new Reader("a", "\0"); - - reader.next(); - reader.next(); - expect(reader.read()).toBe("\0"); - }); -}); diff --git a/src/lexer/char-buffer/char-reader/index.ts b/src/lexer/char-buffer/char-reader/index.ts index dc4de63..5f24dce 100644 --- a/src/lexer/char-buffer/char-reader/index.ts +++ b/src/lexer/char-buffer/char-reader/index.ts @@ -105,24 +105,6 @@ export default class CharReader { // return two-character new line return char + nextChar; } - - /** @deprecated Returns current character; if end of input, return fallback character */ - read(): string { - if (this.index === this.chars.length) { - return this.fallbackChar; - } - - return this.chars[this.index]; - } - - /** @deprecated Increment index to get next character with get() */ - next(): void { - if (this.index === this.chars.length) { - return; - } - - this.index++; - } } export type { SourceChar }; diff --git a/src/lexer/char-buffer/index.test.ts b/src/lexer/char-buffer/index.test.ts index b9903c9..4fa5b1b 100644 --- a/src/lexer/char-buffer/index.test.ts +++ b/src/lexer/char-buffer/index.test.ts @@ -39,53 +39,3 @@ describe("peekChar()", () => { expect(buffer.peekChar()).toEqual(expected); }); }); - -describe("pop()", () => { - it("pop characters", () => { - const buffer = new CharBuffer("ab"); - - expect(buffer.pop()).toBe("a"); - expect(buffer.pop()).toBe("b"); - }); - - it("pop null character if nothing to pop", () => { - const buffer = new CharBuffer(""); - - expect(buffer.pop()).toBe("\0"); - }); - - it("pop null character more than once if nothing to pop", () => { - const buffer = new CharBuffer(""); - - expect(buffer.pop()).toBe("\0"); - expect(buffer.pop()).toBe("\0"); - }); -}); - -describe("peek()", () => { - it("peek character", () => { - const buffer = new CharBuffer("a"); - - expect(buffer.peek()).toBe("a"); - }); - - it("peek the same character twice", () => { - const buffer = new CharBuffer("a"); - - expect(buffer.peek()).toBe("a"); - expect(buffer.peek()).toBe("a"); - }); - - it("peek null character if nothing to pop", () => { - const buffer = new CharBuffer(""); - - expect(buffer.peek()).toBe("\0"); - }); - - it("peek null character more than once if nothing to pop", () => { - const buffer = new CharBuffer(""); - - expect(buffer.peek()).toBe("\0"); - expect(buffer.peek()).toBe("\0"); - }); -}); diff --git a/src/lexer/char-buffer/index.ts b/src/lexer/char-buffer/index.ts index 11e9dcd..bcea770 100644 --- a/src/lexer/char-buffer/index.ts +++ b/src/lexer/char-buffer/index.ts @@ -21,21 +21,6 @@ export default class CharBuffer { return char; } - - /** @deprecated */ - pop(): string { - const char = this.reader.read(); - this.reader.next(); - - return char; - } - - /** @deprecated */ - peek(): string { - const char = this.reader.read(); - - return char; - } } export type { SourceChar } from "./char-reader"; diff --git a/src/lexer/index.test.ts b/src/lexer/index.test.ts index 41814cc..a069362 100644 --- a/src/lexer/index.test.ts +++ b/src/lexer/index.test.ts @@ -1,32 +1,4 @@ import Lexer from "./"; -import { - operator, - identifier, - numberLiteral, - booleanLiteral, - stringLiteral, - groupDelimiter, - blockDelimiter, - keyword, - separator, - illegal, - end, -} from "./token"; -import type { - TokenType, - Operator, - Identifier, - NumberLiteral, - BooleanLiteral, - StringLiteral, - GroupDelimiter, - BlockDelimiter, - Keyword, - Separator, - Illegal, - End, -} from "./token"; - import type { SourceToken, } from "./source-token"; @@ -587,224 +559,3 @@ describe("getSourceToken()", () => { }); }); }); - -describe("getToken()", () => { - describe("single token", () => { - const testLexing = ({ input, expected }: { input: string, expected: TokenType }) => { - const lexer = new Lexer(input); - - const token = lexer.getToken(); - - expect(token).toEqual(expected); - }; - - describe("operators", () => { - const cases: { input: string, expected: Operator }[] = [ - { input: "+", expected: operator("+") }, - { input: "-", expected: operator("-") }, - { input: "*", expected: operator("*") }, - { input: "/", expected: operator("/") }, - { input: "=", expected: operator("=") }, - { input: "!", expected: operator("!") }, - { input: "==", expected: operator("==") }, - { input: "!=", expected: operator("!=") }, - { input: ">", expected: operator(">") }, - { input: "<", expected: operator("<") }, - { input: ">=", expected: operator(">=") }, - { input: "<=", expected: operator("<=") }, - ]; - - it.each(cases)("get operator token '$input'", testLexing); - }); - - describe("identifiers", () => { - const cases: { input: string, expected: Identifier }[] = [ - { input: "foo", expected: identifier("foo") }, - { input: "이름", expected: identifier("이름") }, - { input: "foo이름", expected: identifier("foo이름") }, - { input: "foo123", expected: identifier("foo123") }, - { input: "이름foo", expected: identifier("이름foo") }, - { input: "_foo이름", expected: identifier("_foo이름") }, - ]; - - it.each(cases)("get identifier token '$input'", testLexing); - }); - - describe("number literals", () => { - const cases: { input: string, expected: NumberLiteral }[] = [ - { input: "0", expected: numberLiteral("0") }, - { input: "123", expected: numberLiteral("123") }, - { input: "12.75", expected: numberLiteral("12.75") }, - { input: "0.875", expected: numberLiteral("0.875") }, - { input: "2.00", expected: numberLiteral("2.00") }, - ]; - - it.each(cases)("get number literal token '$input'", testLexing); - }); - - describe("boolean literals", () => { - const cases: { input: string, expected: BooleanLiteral }[] = [ - { input: "참", expected: booleanLiteral("참") }, - { input: "거짓", expected: booleanLiteral("거짓") }, - ]; - - it.each(cases)("get boolean literal token '$input'", testLexing); - }); - - describe("string literals", () => { - const cases: { input: string, expected: StringLiteral }[] = [ - { input: "'foo bar'", expected: stringLiteral("foo bar") }, - { input: "'123'", expected: stringLiteral("123") }, - { input: "'!@#$'", expected: stringLiteral("!@#$") }, - { input: "' '", expected: stringLiteral(" ") }, - { input: "'참'", expected: stringLiteral("참") }, - ]; - - it.each(cases)("get string literal token '$input'", testLexing); - }); - - describe("group delimiters", () => { - const cases: { input: string, expected: GroupDelimiter }[] = [ - { input: "(", expected: groupDelimiter("(") }, - { input: ")", expected: groupDelimiter(")") }, - ]; - - it.each(cases)("get group delimiter token '$input'", testLexing); - }); - - describe("block delimiters", () => { - const cases: { input: string, expected: BlockDelimiter }[] = [ - { input: "{", expected: blockDelimiter("{") }, - { input: "}", expected: blockDelimiter("}") }, - ]; - - it.each(cases)("get group delimiter token '$input'", testLexing); - }); - - describe("keywords", () => { - const cases: { input: string, expected: Keyword }[] = [ - { input: "만약", 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("$") }, - { input: "'foo", expected: illegal("'foo") }, - ]; - - it.each(cases)("get illegal token '$input'", testLexing); - }); - - describe("end", () => { - const cases: { input: string, expected: End }[] = [ - { input: "", expected: end }, - ]; - - it.each(cases)("get end token '$input'", testLexing); - }); - }); - - describe("multiple tokens", () => { - const cases: { input: string, expectedTokens: TokenType[] }[] = [ - { - input: "12 + 34 * 5 / 67 - 89", - expectedTokens: [ - numberLiteral("12"), - operator("+"), - numberLiteral("34"), - operator("*"), - numberLiteral("5"), - operator("/"), - numberLiteral("67"), - operator("-"), - numberLiteral("89"), - end, - ] - }, - { - input: "_이름 = foo123", - expectedTokens: [ - identifier("_이름"), - operator("="), - identifier("foo123"), - end, - ] - }, - { - input: "'foo' 'bar'", - expectedTokens: [ - stringLiteral("foo"), - stringLiteral("bar"), - end, - ] - }, - { - input: "만약 참 { \n 12 \n } 아니면 { \n 34 \n}", - expectedTokens: [ - keyword("만약"), - booleanLiteral("참"), - blockDelimiter("{"), - numberLiteral("12"), - blockDelimiter("}"), - keyword("아니면"), - blockDelimiter("{"), - numberLiteral("34"), - blockDelimiter("}"), - end, - ] - }, - { - input: "함수(사과, 바나나) { 결과 사과 + 바나나 }", - expectedTokens:[ - keyword("함수"), - groupDelimiter("("), - identifier("사과"), - separator(","), - identifier("바나나"), - groupDelimiter(")"), - blockDelimiter("{"), - keyword("결과"), - identifier("사과"), - operator("+"), - identifier("바나나"), - blockDelimiter("}"), - ], - }, - ]; - - it.each(cases)("get tokens from input '$input'", ({ input, expectedTokens }) => { - const lexer = new Lexer(input); - - for (const expected of expectedTokens) { - const token = lexer.getToken(); - expect(token).toEqual(expected); - } - }); - }); - - describe("no token", () => { - it("get only end token", () => { - const input = " \r\r\n\n\t\t"; - const expected = end; - - const lexer = new Lexer(input); - - const token = lexer.getToken(); - expect(token).toEqual(expected); - }); - }); -}); diff --git a/src/lexer/index.ts b/src/lexer/index.ts index af6d69e..4ccccb8 100644 --- a/src/lexer/index.ts +++ b/src/lexer/index.ts @@ -1,6 +1,4 @@ import CharBuffer, { type SourceChar } from "./char-buffer"; -import * as Token from "./token"; -import * as Util from "./util"; import { createOperatorToken, @@ -327,240 +325,6 @@ export default class Lexer { return chars; } - - /** @deprecated */ - getToken(): Token.TokenType { - this.skipWhitespaces(); - - const char = this.charBuffer.peek(); - switch (char) { - case "+": - case "-": - case "*": - case "/": - { - const operator = this.charBuffer.pop() as typeof char; - return Token.operator(operator); - } - - case "(": - case ")": - { - const delimiter = this.charBuffer.pop() as typeof char; - return Token.groupDelimiter(delimiter); - } - - case "{": - case "}": - { - const delimiter = this.charBuffer.pop() as typeof char; - return Token.blockDelimiter(delimiter); - } - - case ",": - { - const separator = this.charBuffer.pop() as typeof char; - return Token.separator(separator); - } - - case "!": - { - this.charBuffer.pop(); - - const operator = this.readOperatorStartingWithBang(); - return Token.operator(operator); - } - - case "=": - { - this.charBuffer.pop(); - - const operator: "=" | "==" = this.readOperatorStartingWithEqual(); - return Token.operator(operator); - } - - case ">": - { - this.charBuffer.pop(); - - const operator: ">" | ">=" = this.readOperatorStartingWithGreaterThan(); - return Token.operator(operator); - } - - case "<": - { - this.charBuffer.pop(); - - const operator: "<" | "<=" = this.readOperatorStartingWithLessThan(); - return Token.operator(operator); - } - - case "'": - { - this.charBuffer.pop(); - - const [str, ok] = this.readStringLiteral(); - return ok ? Token.stringLiteral(str) : Token.illegal("'" + str); - } - - case CharBuffer.END_OF_INPUT: - return Token.end; - - default: - if (Util.isDigit(char)) { - const number = this.readNumberLiteral(); - - return Token.numberLiteral(number); - } - - if (Util.isLetter(char)) { - const read = this.readLettersAndDigits(); - - // order is important: match keywords first, before identifier - if (read === "참" || read === "거짓") { - return Token.booleanLiteral(read); - } - - if ( - read === "만약" || - read === "아니면" || - read === "함수" || - read === "결과" - ) { - return Token.keyword(read); - } - - return Token.identifier(read); - } - - this.charBuffer.pop(); - return Token.illegal(char); - } - } - - /** @deprecated */ - private skipWhitespaces(): void { - while (Util.isWhitespace(this.charBuffer.peek())) { - this.charBuffer.pop(); - } - } - - /** @deprecated assume the bang character popped */ - private readOperatorStartingWithBang(): "!" | "!=" { - switch (this.charBuffer.peek()) { - case "=": - this.charBuffer.pop(); - return "!="; - - default: - return "!"; - } - } - - /** @deprecated assume the equal character popped */ - private readOperatorStartingWithEqual(): "=" | "==" { - switch (this.charBuffer.peek()) { - case "=": - this.charBuffer.pop(); - return "=="; - - default: - return "="; - } - } - - /** @deprecated assume the greater-than character popped */ - private readOperatorStartingWithGreaterThan(): ">" | ">=" { - switch (this.charBuffer.peek()) { - case "=": - this.charBuffer.pop(); - return ">="; - - default: - return ">"; - } - } - - /** @deprecated assume the less-than character popped */ - private readOperatorStartingWithLessThan(): "<" | "<=" { - switch (this.charBuffer.peek()) { - case "=": - this.charBuffer.pop(); - return "<="; - - default: - return "<"; - } - } - - /** @deprecated return [string-literal, true] if ok; otherwise [string-read-so-far, false] */ - private readStringLiteral(): [string, boolean] { - const read: string[] = []; - - // read string until string closing symbol (') - while (true) { - const char = this.charBuffer.pop(); - - if (char === "'" || char === CharBuffer.END_OF_INPUT) { - const str = read.join(""); - const legalString = char === "'"; // true if closing quote encoutered - - return [str, legalString]; - } - - read.push(char); - } - } - - /** @deprecated */ - private readNumberLiteral(): string { - const wholeNumberPart = this.readDigits(); - const decimalPart = this.readDecimalPart(); - - const number = wholeNumberPart + decimalPart; - - return number; - } - - /** @deprecated */ - private readDigits(): string { - const read: string[] = []; - while (Util.isDigit(this.charBuffer.peek())) { - read.push(this.charBuffer.pop()); - } - - const digits = read.join(""); - return digits; - } - - /** @deprecated helper function for readNumberLiteral() method */ - private readDecimalPart(): string { - // read decimal point; if not, early return - const maybeDecimalPoint = this.charBuffer.peek(); - if (maybeDecimalPoint !== ".") { - return ""; - } - const decimalPoint = this.charBuffer.pop(); - - // read and return decimal part - const digits = this.readDigits(); - const decimalPart = decimalPoint + digits; - return decimalPart; - } - - /** @deprecated */ - private readLettersAndDigits(): string { - const read = []; - while ( - Util.isLetter(this.charBuffer.peek()) || - Util.isDigit(this.charBuffer.peek()) - ) { - read.push(this.charBuffer.pop()); - } - - return read.join(""); - } } -export type { TokenType } from "./token"; export type { SourceToken } from "./source-token"; diff --git a/src/lexer/token/index.test.ts b/src/lexer/token/index.test.ts deleted file mode 100644 index 70e4927..0000000 --- a/src/lexer/token/index.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { - operator, - identifier, - numberLiteral, - booleanLiteral, - stringLiteral, - groupDelimiter, - separator, - keyword, -} from "./"; -import type { - Operator, - Identifier, - NumberLiteral, - BooleanLiteral, - StringLiteral, - GroupDelimiter, - Separator, - Keyword, -} from "./"; - -describe("operator", () => { - const cases: { input: Operator["value"], expected: Operator }[] = [ - { input: "+", expected: operator("+") }, - { input: "-", expected: operator("-") }, - { input: "*", expected: operator("*") }, - { input: "/", expected: operator("/") }, - { input: "=", expected: operator("=") }, - { input: "!", expected: operator("!") }, - { input: "!=", expected: operator("!=") }, - { input: "==", expected: operator("==") }, - { input: ">", expected: operator(">") }, - { input: "<", expected: operator("<") }, - { input: ">=", expected: operator(">=") }, - { input: "<=", expected: operator("<=") }, - ]; - - it.each(cases)("make operator token for '$input'", ({ input, expected }) => { - const token = operator(input); - - expect(token).toEqual(expected); - }); -}); - -describe("identifier", () => { - const cases: { input: Identifier["value"], expected: Identifier }[] = [ - { input: "foo", expected: identifier("foo") }, - { input: "이름", expected: identifier("이름") }, - { input: "_foo이름123", expected: identifier("_foo이름123") }, - ]; - - it.each(cases)("make identifier token for '$input'", ({ input, expected }) => { - const token = identifier(input); - - expect(token).toEqual(expected); - }); -}); - -describe("number literal", () => { - const cases: { input: NumberLiteral["value"], expected: NumberLiteral }[] = [ - { input: "0", expected: numberLiteral("0") }, - { input: "123", expected: numberLiteral("123") }, - ]; - - it.each(cases)("make number literal token for '$input'", ({ input, expected }) => { - const token = numberLiteral(input); - - expect(token).toEqual(expected); - }); -}); - -describe("boolean literal", () => { - const cases: { input: BooleanLiteral["value"], expected: BooleanLiteral }[] = [ - { input: "참", expected: booleanLiteral("참") }, - { input: "거짓", expected: booleanLiteral("거짓") }, - ]; - - it.each(cases)("make boolean literal token for '$input'", ({ input, expected }) => { - const token = booleanLiteral(input); - - expect(token).toEqual(expected); - }); -}); - -describe("string literal", () => { - const cases: { input: StringLiteral["value"], expected: StringLiteral }[] = [ - { input: "foo bar", expected: stringLiteral("foo bar") }, - { input: " ", expected: stringLiteral(" ") }, - { input: "123", expected: stringLiteral("123") }, - { input: "참", expected: stringLiteral("참") }, - ]; - - it.each(cases)("make string literal token for '$input'", ({ input, expected }) => { - const token = stringLiteral(input); - - expect(token).toEqual(expected); - }); -}); - -describe("group delimiter", () => { - const cases: { input: GroupDelimiter["value"], expected: GroupDelimiter }[] = [ - { input: "(", expected: groupDelimiter("(") }, - { input: ")", expected: groupDelimiter(")") }, - ]; - - it.each(cases)("make group delimiter token for '$input'", ({ input, expected }) => { - const token = groupDelimiter(input); - - expect(token).toEqual(expected); - }); -}); - -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("함수") }, - { input: "결과", expected: keyword("결과") }, - ]; - - it.each(cases)("make keyword token for '$input'", ({ input, expected }) => { - const token = keyword(input); - - expect(token).toEqual(expected); - }); -}); diff --git a/src/lexer/token/index.ts b/src/lexer/token/index.ts deleted file mode 100644 index cced492..0000000 --- a/src/lexer/token/index.ts +++ /dev/null @@ -1,146 +0,0 @@ -/** @deprecated */ -export type TokenType = - Operator | - Identifier | - NumberLiteral | - BooleanLiteral | - StringLiteral | - GroupDelimiter | - BlockDelimiter | - Separator | - Keyword | - Illegal | - End; - -export const END_VALUE = "$end"; // unreadable character '$' used to avoid other token values - -type OperatorValue = ArithmeticOperatorValue | AssignmentOperatorValue | LogicalOperatorValue; -type ArithmeticOperatorValue = "+" | "-" | "*" | "/"; -type AssignmentOperatorValue = "="; -type LogicalOperatorValue = "!" | "!=" | "==" | ">" | "<" | ">=" | "<="; -type BooleanLiteralValue = "참" | "거짓"; -type GroupDelimiterValue = "(" | ")"; -type BlockDelimiterValue = "{" | "}"; -type SeparatorValue = ","; -type KeywordValue = BranchKeywordValue | FunctionKeywordValue | ReturnKeywordValue; -type BranchKeywordValue = "만약" | "아니면"; -type FunctionKeywordValue = "함수"; -type ReturnKeywordValue = "결과"; -type EndValue = typeof END_VALUE; - -export interface Operator { - type: "operator"; - value: OperatorValue; -} - -export interface Identifier { - type: "identifier"; - value: string; -} - -export interface NumberLiteral { - type: "number literal"; - value: string; -} - -export interface BooleanLiteral { - type: "boolean literal"; - value: BooleanLiteralValue; -} - -export interface StringLiteral { - type: "string literal"; - value: string; -} - -export interface GroupDelimiter { - type: "group delimiter"; - value: GroupDelimiterValue; -} - -export interface BlockDelimiter { - type: "block delimiter"; - value: BlockDelimiterValue; -} - -export interface Separator { - type: "separator", - value: SeparatorValue; -} - -export interface Keyword { - type: "keyword"; - value: KeywordValue; -} - -export interface Illegal { - type: "illegal"; - value: string; -} - -export interface End { - type: "end"; - value: EndValue -} - -/** @deprecated */ -export const operator = (value: Operator["value"]): Operator => ({ - type: "operator", - value, -}); - -/** @deprecated */ -export const identifier = (value: Identifier["value"]): Identifier => ({ - type: "identifier", - value, -}); - -/** @deprecated */ -export const numberLiteral = (value: NumberLiteral["value"]): NumberLiteral => ({ - type: "number literal", - value, -}); - -/** @deprecated */ -export const booleanLiteral = (value: BooleanLiteral["value"]): BooleanLiteral => ({ - type: "boolean literal", - value, -}); - -/** @deprecated */ -export const stringLiteral = (value: StringLiteral["value"]): StringLiteral => ({ - type: "string literal", - value, -}); - -/** @deprecated */ -export const groupDelimiter = (value: GroupDelimiter["value"]): GroupDelimiter => ({ - type: "group delimiter", - value, -}); - -/** @deprecated */ -export const blockDelimiter = (value: BlockDelimiter["value"]): BlockDelimiter => ({ - type: "block delimiter", - value, -}); - -/** @deprecated */ -export const separator = (value: Separator["value"]): Separator => ({ - type: "separator", - value, -}); - -/** @deprecated */ -export const keyword = (value: Keyword["value"]): Keyword => ({ - type: "keyword", - value, -}); - -/** @deprecated */ -export const illegal = (value: Illegal["value"]): Illegal => ({ - type: "illegal", - value, -}); - -export const end: End = { type: "end", value: END_VALUE }; diff --git a/src/parser/index.test.ts b/src/parser/index.test.ts index 581cec1..e8a788c 100644 --- a/src/parser/index.test.ts +++ b/src/parser/index.test.ts @@ -1,1126 +1,1046 @@ import Lexer from "../lexer"; import Parser from "./"; -import type { Program } from "./syntax-tree"; +import { + ParserError, + BadExpressionError, +} from "./"; +import type { + ProgramNode, +} from "./syntax-node"; -describe("parseProgram()", () => { - const testParsing = ({ input, expected }: { input: string, expected: Program }) => { +type SuccessTestCase = { name: string, input: string, expected: E }; +type FailureTestCase = { name: string, input: string, expected: E }; + +describe("parseSource()", () => { + const createParser = (input: string) => { const lexer = new Lexer(input); const parser = new Parser(lexer); - const node = parser.parseProgram(); + return parser; + }; + + const testSuccess = ({ input, expected }: { input: string, expected: ProgramNode }) => { + const parser = createParser(input); - expect(node).toEqual(expected); + const node = parser.parseSource(); + + expect(node).toMatchObject(expected); }; - describe("assignment", () => { - const cases: { name: string, input: string, expected: Program }[] = [ - { - name: "a single assignment statement", - input: "x = 42", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "assignment", - left: { type: "identifier", value: "x" }, - right: { type: "number node", value: 42 }, - }, - }, - ], - }, - }, - { - name: "multiple assignment statements", - input: "x = 42 한 = 9 _123 = 123", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "assignment", - left: { type: "identifier", value: "x" }, - right: { type: "number node", value: 42 }, - }, - }, - { - type: "expression statement", - expression: { - type: "assignment", - left: { type: "identifier", value: "한" }, - right: { type: "number node", value: 9 }, - }, - }, - { - type: "expression statement", - expression: { - type: "assignment", - left: { type: "identifier", value: "_123" }, - right: { type: "number node", value: 123 }, - }, - }, - ], - }, - }, - ]; + const testFailure = ({ input, expected }: { input: string, expected: typeof ParserError }) => { + const parser = createParser(input); - it.each(cases)("parse $name", testParsing); - }); + expect(() => parser.parseSource()).toThrow(expected); + }; - describe("logical expression", () => { - const cases: { name: string, input: string, expected: Program }[] = [ - { - name: "not operator", - input: "!x", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "prefix expression", - prefix: "!", - expression: { type: "identifier", value: "x" }, - }, - }, - ], - }, - }, - { - name: "double not operator", - input: "!!x", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "prefix expression", - prefix: "!", + describe("creating nodes", () => { + describe("literal expressions", () => { + const cases: SuccessTestCase[] = [ + { + name: "a number literal", + input: "42", + expected: { + type: "program", + statements: [ + { + type: "expression statement", expression: { - type: "prefix expression", - prefix: "!", - expression: { type: "identifier", value: "x" }, - }, - }, - }, - ], - }, - }, - { - name: "equal-to comparison", - input: "x == y", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "==", - left: { type: "identifier", value: "x" }, - right: { type: "identifier", value: "y" }, - }, - }, - ], - }, - }, - { - name: "not-equal-to comparison", - input: "x != y", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "!=", - left: { type: "identifier", value: "x" }, - right: { type: "identifier", value: "y" }, - }, - }, - ], - }, - }, - { - name: "greater-than comparison", - input: "x > y", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: ">", - left: { type: "identifier", value: "x" }, - right: { type: "identifier", value: "y" }, - }, - }, - ], - }, - }, - { - name: "less-than comparison", - input: "x < y", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "<", - left: { type: "identifier", value: "x" }, - right: { type: "identifier", value: "y" }, - }, - }, - ], - }, - }, - { - name: "greater-than-or-equal-to comparison", - input: "x >= y", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: ">=", - left: { type: "identifier", value: "x" }, - right: { type: "identifier", value: "y" }, + type: "number", + value: 42, + } }, - }, - ], + ], + }, }, - }, - { - name: "less-than-or-equal-to comparison", - input: "x <= y", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "<=", - left: { type: "identifier", value: "x" }, - right: { type: "identifier", value: "y" }, + { + name: "a boolean literal", + input: "참", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "boolean", + value: true, + } }, - }, - ], + ], + }, }, - }, - { - name: "left associative comparison", - input: "x <= y == z", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "==", - left: { - type: "infix expression", - infix: "<=", - left: { type: "identifier", value: "x" }, - right: { type: "identifier", value: "y" }, + { + name: "a string literal", + input: "'foo bar'", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "string", + value: "foo bar", }, - right: { type: "identifier", value: "z" }, }, - }, - ], + ], + }, }, - }, - { - name: "complex grouped comparison", - input: "x == (y >= z)", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "==", - left: { type: "identifier", value: "x" }, - right: { - type: "infix expression", - infix: ">=", - left: { type: "identifier", value: "y" }, - right: { type: "identifier", value: "z" }, - }, + { + name: "a identifer literal", + input: "foo", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "identifier", + value: "foo", + } }, - }, - ], + ], + }, }, - }, - ]; + ]; - it.each(cases)("parse $name", testParsing); - }); + it.each(cases)("$name", testSuccess); + }); - describe("return statement", () => { - const cases: { name: string, input: string, expected: Program }[] = [ - { - name: "return number literal", - input: "결과 42", - expected: { - type: "program", - statements: [ - { - type: "return statement", - expression: { type: "number node", value: 42 }, + describe("arithmetic expressions", () => { + describe("single number", () => { + const cases: SuccessTestCase[] = [ + { + name: "positive number", + input: "+42", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "prefix", + prefix: "+", + right: { + type: "number", + value: 42, + }, + } + }, + ], }, - ], - }, - }, - { - name: "return arithmetic expression", - input: "결과 사과 + 바나나", - expected: { - type: "program", - statements: [ - { - type: "return statement", - expression: { - type: "infix expression", - infix: "+", - left: { type: "identifier", value: "사과" }, - right: { type: "identifier", value: "바나나" }, - }, + }, + { + name: "negative number", + input: "-42", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "prefix", + prefix: "-", + right: { + type: "number", + value: 42, + }, + } + }, + ], }, - ], - }, - }, - { - name: "return function", - input: "결과 함수(사과) { 결과 사과 + 1 }", - expected: { - type: "program", - statements: [ - { - type: "return statement", - expression: { - type: "function expression", - parameter: [ - { type: "identifier", value: "사과" }, - ], - body: { - type: "block", - statements: [ - { - type: "return statement", - expression: { - type: "infix expression", - infix: "+", - left: { type: "identifier", value: "사과" }, - right: { type: "number node", value: 1 }, + }, + { + name: "doubly negative number", + input: "--42", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "prefix", + prefix: "-", + right: { + type: "prefix", + prefix: "-", + right: { + type: "number", + value: 42, }, }, - ], + }, }, - }, + ], }, - ], - }, - }, - ]; + }, + ]; - it.each(cases)("parse $name", testParsing); - }); + it.each(cases)("$name", testSuccess); + }); - describe("simple expression", () => { - const cases: { name: string, input: string, expected: Program }[] = [ - { - name: "an identifier", - input: "x", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { type: "identifier", value: "x" }, - }, - ], - }, - }, - { - name: "a number", - input: "123", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { type: "number node", value: 123 }, - }, - ], - }, - }, - { - name: "a negative number", - input: "-42", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "prefix expression", - prefix: "-", - expression: { type: "number node", value: 42 }, - }, - }, - ], - }, - }, - { - name: "a doubly negative number", - input: "--42", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "prefix expression", - prefix: "-", + describe("left associativity", () => { + const leftAssocCases = [ + { infix: "+", name: "left associative addition" }, + { infix: "-", name: "left associative subtraction" }, + { infix: "*", name: "left associative multiplication" }, + { infix: "/", name: "left associative division" }, + ]; + const leftAssocTestCases: SuccessTestCase[] = leftAssocCases.map(({ infix, name }) => ({ + name, + input: `11 ${infix} 22 ${infix} 33`, + expected: { + type: "program", + statements: [ + { + type: "expression statement", expression: { - type: "prefix expression", - prefix: "-", - expression: { type: "number node", value: 42 }, + type: "infix", + infix, + left: { + type: "infix", + infix, + left: { + type: "number", + value: 11, + }, + right: { + type: "number", + value: 22, + }, + }, + right: { + type: "number", + value: 33, + }, }, }, - }, - ], - }, - }, - { - name: "a positive number", - input: "+42", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "prefix expression", - prefix: "+", - expression: { type: "number node", value: 42 }, - }, - }, - ], - }, - }, - { - name: "an addition of two numbers", - input: "42+99", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 42 }, - right: { type: "number node", value: 99 }, - }, - }, - ], - }, - }, - { - name: "an addition with the first negative number", - input: "-42+99", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { - type: "prefix expression", - prefix: "-", - expression: { type: "number node", value: 42 }, + ], + }, + })); + + it.each(leftAssocTestCases)("$name", testSuccess); + }); + + describe("associativity among different operations", () => { + const cases: SuccessTestCase[] = [ + { + name: "four operations", + input: "11+22*33/44-55", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "infix", + infix: "-", + left: { + type: "infix", + infix: "+", + left: { + type: "number", + value: 11, + }, + right: { + type: "infix", + infix: "/", + left: { + type: "infix", + infix: "*", + left: { + type: "number", + value: 22 + }, + right: { + type: "number", + value: 33 + }, + }, + right: { + type: "number", + value: 44, + }, + }, + }, + right: { + type: "number", + value: 55, + }, + }, }, - right: { type: "number node", value: 99 }, - }, + ], }, - ], - }, - }, - { - name: "an addition with the second negative number", - input: "42+-99", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 42 }, - right: { - type: "prefix expression", - prefix: "-", - expression: { type: "number node", value: 99 }, + }, + { + name: "with grouped", + input: "11+(22+33)", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "infix", + infix: "+", + left: { + type: "number", + value: 11, + }, + right: { + type: "infix", + infix: "+", + left: { + type: "number", + value: 22, + }, + right: { + type: "number", + value: 33, + }, + }, + }, }, - }, + ], }, - ], - }, - }, - { - name: "an addition of two negative numbers", - input: "-42+-99", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { - type: "prefix expression", - prefix: "-", - expression: { type: "number node", value: 42 }, - }, - right: { - type: "prefix expression", - prefix: "-", - expression: { type: "number node", value: 99 }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + + }); + + describe("logical expressions", () => { + describe("unary operation", () => { + const cases: SuccessTestCase[] = [ + { + name: "negation expression", + input: "!foo", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "prefix", + prefix: "!", + right: { + type: "identifier", + value: "foo", + }, + }, }, - }, + ], }, - ], - }, - }, - { - name: "an addition of two positive numbers", - input: "+42++99", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { - type: "prefix expression", - prefix: "+", - expression: { type: "number node", value: 42 }, - }, - right: { - type: "prefix expression", - prefix: "+", - expression: { type: "number node", value: 99 }, + }, + { + name: "double negation expression", + input: "!!foo", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "prefix", + prefix: "!", + right: { + type: "prefix", + prefix: "!", + right: { + type: "identifier", + value: "foo", + }, + }, + }, }, - }, - }, - ], - }, - }, - { - name: "a subtraction of two numbers", - input: "42-99", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "-", - left: { type: "number node", value: 42 }, - right: { type: "number node", value: 99 }, - }, - }, - ], - }, - }, - { - name: "a multiplication of two numbers", - input: "42*99", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "*", - left: { type: "number node", value: 42 }, - right: { type: "number node", value: 99 }, - }, - }, - ], - }, - }, - { - name: "a division of two numbers", - input: "42/99", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "/", - left: { type: "number node", value: 42 }, - right: { type: "number node", value: 99 }, - }, + ], }, - ], - }, - }, - { - name: "an addition of three numbers, left associative", - input: "42+99+12", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 42 }, - right: { type: "number node", value: 99 }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + + describe("binary operation", () => { + const infixCases = [ + { name: "equal-to expression", infix: "==" }, + { name: "not-equal-to expression", infix: "!=" }, + { name: "greater-than expression", infix: ">" }, + { name: "less-than expression", infix: "<" }, + { name: "greater-than-or-equal-to expression", infix: ">=" }, + { name: "less-than-or-equal-to expression", infix: "<=" }, + ]; + const infixTestCases: SuccessTestCase[] = infixCases.map(({ name, infix }) => ({ + name, + input: `foo ${infix} bar`, + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "infix", + infix, + left: { + type: "identifier", + value: "foo", + }, + right: { + type: "identifier", + value: "bar", + }, }, - right: { type: "number node", value: 12 }, }, - }, - ], - }, - }, - { - name: "addition and multiplication", - input: "42+99*12", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 42 }, - right: { - type: "infix expression", - infix: "*", - left: { type: "number node", value: 99 }, - right: { type: "number node", value: 12 }, + ], + }, + })); + + it.each(infixTestCases)("$name", testSuccess); + }); + + describe("right associativity", () => { + const infixCases = [ + { name: "right associative equal-to expression", infix: "==" }, + { name: "right associative not-equal-to expression", infix: "!=" }, + ]; + const infixTestCases: SuccessTestCase[] = infixCases.map(({ name, infix }) => ({ + name, + input: `foo ${infix} bar ${infix} baz`, + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "infix", + infix, + left: { + type: "identifier", + value: "foo", + }, + right: { + type: "infix", + infix, + left: { + type: "identifier", + value: "bar", + }, + right: { + type: "identifier", + value: "baz", + }, + }, }, }, - }, - ], - }, - }, - { - name: "multiplication and addition", - input: "42*99+12", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { - type: "infix expression", - infix: "*", - left: { type: "number node", value: 42 }, - right: { type: "number node", value: 99 }, + ], + }, + })); + + it.each(infixTestCases)("$name", testSuccess); + }); + + describe("grouped expression", () => { + const cases: SuccessTestCase[] = [ + { + name: "equal-to and not-equal-to", + input: "(foo == bar) != baz", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "infix", + infix: "!=", + left: { + type: "infix", + infix: "==", + left: { + type: "identifier", + value: "foo", + }, + right: { + type: "identifier", + value: "bar", + }, + }, + right: { + type: "identifier", + value: "baz", + }, + }, }, - right: { type: "number node", value: 12 }, - }, + ], }, - ], - }, - }, - { - name: "an addition with grouped expression", - input: "12+(34+56)", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 12 }, - right: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 34 }, - right: { type: "number node", value: 56 }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + }); + + describe("assignment", () => { + const cases: SuccessTestCase[] = [ + { + name: "a single assignment statement", + input: "x = 42", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "assignment", + left: { + type: "identifier", + value: "x", + }, + right: { + type: "number", + value: 42, + }, }, }, - }, - ], + ], + }, }, - }, - { - name: "an addition with grouped more than once", - input: "12+(34+(56+(78+9)))", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 12 }, - right: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 34 }, + { + name: "right associative assignment", + input: "x = y = 42", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "assignment", + left: { + type: "identifier", + value: "x", + }, right: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 56 }, + type: "assignment", + left: { + type: "identifier", + value: "y", + }, right: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 78 }, - right: { type: "number node", value: 9 }, + type: "number", + value: 42, }, }, }, }, - }, - ], + ], + }, }, - }, - { - name: "arithmetic expression with grouped more than once", - input: "(12*(34/56))+(7-((8+9)*10))", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { - type: "infix expression", - infix: "*", - left: { type: "number node", value: 12 }, - right: { - type: "infix expression", - infix: "/", - left: { type: "number node", value: 34 }, - right: { type: "number node", value: 56 }, + ]; + + it.each(cases)("$name", testSuccess); + }); + + describe("call expressions", () => { + const cases: SuccessTestCase[] = [ + { + name: "call function with identifier", + input: "foo(bar, 42)", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "call", + func: { + type: "identifier", + value: "foo", }, - }, - right: { - type: "infix expression", - infix: "-", - left: { type: "number node", value: 7 }, - right: { - type: "infix expression", - infix: "*", - left: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 8 }, - right: { type: "number node", value: 9 }, + args: [ + { + type: "identifier", + value: "bar", }, - right: { type: "number node", value: 10 }, - }, + { + type: "number", + value: 42, + }, + ], }, }, - }, - ], + ], + }, }, - }, - { - name: "arithmetic expression with floating point numbers", - input: "0.75 + 1.25", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "+", - left: { type: "number node", value: 0.75 }, - right: { type: "number node", value: 1.25 }, + { + name: "call function with function literal", + input: "함수(foo){ foo }(42)", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "call", + func: { + type: "function", + parameters: {}, // omit + body: {}, // omit + }, + args: [ + { + type: "number", + value: 42, + }, + ], + }, }, - }, - ], - }, - }, - { - name: "true boolean literal", - input: "참", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { type: "boolean node", value: true }, - }, - ], + ], + }, }, - }, - { - name: "false boolean literal", - input: "거짓", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { type: "boolean node", value: false }, - }, - ], + ]; + + it.each(cases)("$name", testSuccess); + }); + + describe("function expressions", () => { + const cases: SuccessTestCase[] = [ + { + name: "function expression with parameters", + input: "함수 (foo, bar) { foo + bar }", + expected: { + type: "program", + statements: [ + { + type: "expression statement", + expression: { + type: "function", + parameters: [ + { + type: "identifier", + value: "foo", + }, + { + type: "identifier", + value: "bar", + }, + ], + body: { + type: "block", + statements: [ + { + type: "expression statement", + expression: { + type: "infix", + }, + }, + ], + }, + }, + }, + ], + }, }, - }, - { - name: "string literal", - input: "'foo bar'", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { type: "string node", value: "foo bar" }, - }, - ], + ]; + + it.each(cases)("$name", testSuccess); + }); + + describe("return statement", () => { + const cases: SuccessTestCase[] = [ + { + name: "return number literal", + input: "결과 42", + expected: { + type: "program", + statements: [ + { + type: "return", + expression: { + type: "number", + value: 42, + }, + }, + ], + }, }, - }, - ]; + ]; - it.each(cases)("parse $name", testParsing); - }); + it.each(cases)("$name", testSuccess); + }); - 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: { + describe("branch statement", () => { + const cases: SuccessTestCase[] = [ + { + name: "predicate and consequence", + input: "만약 foo { bar }", + expected: { + type: "program", + statements: [ + { + type: "branch", + predicate: { + type: "identifier", + value: "foo", + }, + consequence: { type: "block", statements: [ { type: "expression statement", expression: { - type: "infix expression", - infix: "+", - left: { type: "identifier", value: "사과" }, - right: { type: "identifier", value: "바나나" }, + type: "identifier", + value: "bar", }, }, ], }, }, - }, - ], + ], + }, }, - }, - { - name: "function expression with no parameters", - input: "함수 () { 사과 + 바나나 }", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "function expression", - parameter: [], - body: { + { + name: "predicate and consequence with alternative", + input: "만약 foo { bar } 아니면 { baz }", + expected: { + type: "program", + statements: [ + { + type: "branch", + predicate: { + type: "identifier", + value: "foo", + }, + consequence: { + type: "block", + statements: [ + { + type: "expression statement", + expression: { + type: "identifier", + value: "bar", + }, + }, + ], + }, + alternative: { type: "block", statements: [ { type: "expression statement", expression: { - type: "infix expression", - infix: "+", - left: { type: "identifier", value: "사과" }, - right: { type: "identifier", value: "바나나" }, + type: "identifier", + value: "baz", }, }, ], }, }, - }, - ], + ], + }, }, - }, - ]; + ]; - it.each(cases)("parse $name", testParsing); + it.each(cases)("$name", testSuccess); + }); }); - 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: [], - }, + describe("marking positions", () => { + describe("single statements", () => { + describe("literal expressions", () => { + const literalCases = [ + { + name: "number literal", + type: "number", + input: "12345", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 4 }, }, - ], - }, - }, - { - 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: "string literal", + type: "string", + input: "'foo bar'", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, }, - ], - }, - }, - { - 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: "boolean literal", + type: "boolean", + input: "거짓", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, }, - ], - }, - }, - { - 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: "바나나" }, - }, - }, - ], - }, + }, + { + name: "identifier literal", + type: "identifier", + input: "foo", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 2 }, + }, + }, + ]; + const cases: SuccessTestCase[] = literalCases.map(({ name, input, range, type }) => ({ + name, + input, + expected: { + type: "program", + range, + statements: [ + { + type: "expression statement", + range, + expression: { + type, + range, }, - callArguments: [ - { type: "number node", value: 1 }, - { type: "number node", value: 2 }, - ], }, - }, - ], - }, - }, - ]; + ], + }, + })); - it.each(cases)("parse $name", testParsing); - }); + it.each(cases)("$name", testSuccess); + }); - describe("branch statements", () => { - const cases: { name: string, input: string, expected: Program }[] = [ - { - name: "simple if statement with boolean predicate", - input: "만약 참 { 1 }", - expected: { - type: "program", - statements: [ - { - type: "branch statement", - predicate: { type: "boolean node", value: true }, - consequence: { - type: "block", - statements: [ - { - type: "expression statement", - expression: { type: "number node", value: 1 }, - }, - ], + describe("single expressions", () => { + const cases: SuccessTestCase[] = [ + { + name: "assignment", + input: "x = 42", + expected: { + type: "program", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 5 }, }, + statements: [ + { + type: "expression statement", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 5 }, + }, + expression: { + type: "assignment", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 5 }, + }, + left: { + type: "identifier", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 0 }, + }, + }, + right: { + type: "number", + range: { + begin: { row: 0, col: 4 }, + end: { row: 0, col: 5 }, + }, + }, + }, + }, + ], }, - ], - }, - }, - { - name: "simple if statement with expression predicate", - input: "만약 사과 == 1 { 2 }", - expected: { - type: "program", - statements: [ - { - type: "branch statement", - predicate: { - type: "infix expression", - infix: "==", - left: { type: "identifier", value: "사과" }, - right: { type: "number node", value: 1 }, + }, + { + name: "arithmetic expression", + input: "11 + 22", + expected: { + type: "program", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 6 }, }, - consequence: { - type: "block", - statements: [ - { - type: "expression statement", - expression: { type: "number node", value: 2 }, + statements: [ + { + type: "expression statement", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 6 }, }, - ], - }, + expression: { + type: "infix", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 6 }, + }, + left: { + type: "number", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 1 }, + }, + }, + right: { + type: "number", + range: { + begin: { row: 0, col: 5 }, + end: { row: 0, col: 6 }, + }, + }, + }, + }, + ], }, - ], - }, - }, - { - name: "simple if-else statement with boolean predicate", - input: "만약 참 { 3 } 아니면 { 4 }", - expected: { - type: "program", - statements: [ - { - type: "branch statement", - predicate: { type: "boolean node", value: true }, - consequence: { - type: "block", - statements: [ - { - type: "expression statement", - expression: { type: "number node", value: 3 }, + }, + { + name: "grouped expression", + input: "(11 + 22)", + expected: { + type: "program", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, + }, + statements: [ + { + type: "expression statement", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, + }, + expression: { + type: "infix", + range: { + begin: { row: 0, col: 0 }, + end: { row: 0, col: 8 }, + }, + left: { + type: "number", + range: { + begin: { row: 0, col: 1 }, + end: { row: 0, col: 2 }, + }, + }, + right: { + type: "number", + range: { + begin: { row: 0, col: 6 }, + end: { row: 0, col: 7 }, + }, + }, }, - ], + }, + ], + }, + }, + { + name: "function expression", + input: "함수(foo) {\n foo\n}", + expected: { + type: "program", + range: { + begin: { row: 0, col: 0, }, + end: { row: 2, col: 0, }, }, - alternative: { - type: "block", - statements: [ - { - type: "expression statement", - expression: { type: "number node", value: 4 }, + statements: [ + { + type: "expression statement", + expression: { + type: "function", + range: { + begin: { row: 0, col: 0, }, + end: { row: 2, col: 0, }, + }, + body: { + type: "block", + range: { + begin: { row: 0, col: 8, }, + end: { row: 2, col: 0, }, + }, + }, }, - ], + }, + ], + }, + }, + { + name: "call expression", + input: "foo(bar, baz)", + expected: { + type: "program", + range: { }, + statements: [ + { + type: "expression statement", + expression: { + type: "call", + range: { + begin: { row: 0, col: 0, }, + end: { row: 0, col: 12, }, + }, + }, + }, + ], }, - ], - }, - }, - ]; + }, + ]; - it.each(cases)("parse $name", testParsing); - }); + it.each(cases)("$name", testSuccess); + }); - 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 }, + describe("single statements", () => { + const cases: SuccessTestCase[] = [ + { + name: "branch statement", + input: "만약 foo {\n 11\n} 아니면 {\n 22\n}", + expected: { + type: "program", + range: { }, - }, - { - type: "expression statement", - expression: { - type: "infix expression", - infix: "*", - left: { - type: "infix expression", - infix: "+", - left: { type: "identifier", value: "변수1" }, - right: { type: "identifier", value: "변수1" }, + statements: [ + { + type: "branch", + range: { + }, + predicate: { + range: { + begin: { row: 0, col: 3, }, + end: { row: 0, col: 5, }, + }, + }, + consequence: { + type: "block", + range: { + begin: { row: 0, col: 7, }, + end: { row: 2, col: 0, }, + }, + }, + alternative: { + type: "block", + range: { + begin: { row: 2, col: 6, }, + end: { row: 4, col: 0, }, + }, + }, }, - right: { type: "identifier", value: "변수1" }, - }, + ], }, - ], - }, - }, + }, + ]; + + it.each(cases)("$name", testSuccess); + }); + }); + }); + + describe("error handling", () => { + const cases: FailureTestCase[] = [ + { + name: "not parsable expression start", + input: "*3", + expected: BadExpressionError, + } ]; - it.each(cases)("parse $name", testParsing); + it.each(cases)("$name", testFailure); }); }); diff --git a/src/parser/index.ts b/src/parser/index.ts index 97fba0f..ab4a025 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -1,411 +1,400 @@ -import { - makeProgram, - makeBlock, - makeIdentifier, - makeAssignment, - makeNumberNode, - makeBooleanNode, - makeStringNode, - makeBranchStatement, - makeReturnStatement, - makeExpressionStatement, - makePrefixExpression, - makeInfixExpression, - makeFunctionExpression, - makeCall, -} from "./syntax-tree"; -import type { - Program, - Block, - Statement, - NumberNode, - BooleanNode, - StringNode, - BranchStatement, - ReturnStatement, - ExpressionStatement, - Expression, - FunctionExpression, - Call, - Identifier, - InfixExpression, -} from "./syntax-tree"; -import Lexer from "../lexer"; -import TokenReader from "./token-reader"; - -type BindingPower = number; -const bindingPower = { - lowest: 0, - assignment: 30, - comparison: 40, - summative: 50, - productive: 60, - prefix: 70, - call: 80, -}; -const getBindingPower = (infix: string): BindingPower => { - switch (infix) { - case "=": - return bindingPower.assignment; - case "==": - case "!=": - case ">": - case "<": - case ">=": - case "<=": - return bindingPower.comparison; - case "+": - case "-": - return bindingPower.summative; - case "*": - case "/": - return bindingPower.productive; - case "(": // when '(' is used infix operator, it behaves as call operator - return bindingPower.call; - default: - return bindingPower.lowest; +import type * as Node from "./syntax-node"; +import * as node from "./syntax-node"; +import { getInfixBindingPower, bindingPowers, type BindingPowerEntry } from "./binding-power"; + +import { copyRange, type Range } from "../util/position"; + +export class ParserError extends Error { + public received: string; + public expected: string; + public range: Range; + + constructor(received: string, expected: string, range: Range) { + super(); + this.received = received; + this.expected = expected; + this.range = range; } }; -/** @deprecated */ +export class BadNumberLiteralError extends ParserError {}; +export class BadBooleanLiteralError extends ParserError {}; +export class BadPrefixError extends ParserError {}; +export class BadInfixError extends ParserError {}; +export class BadExpressionError extends ParserError {}; +export class BadGroupDelimiterError extends ParserError {}; +export class BadBlockDelimiterError extends ParserError {}; +export class BadAssignmentError extends ParserError {}; +export class BadFunctionKeywordError extends ParserError {}; +export class BadIdentifierError extends ParserError {}; +export class BadSeparatorError extends ParserError {}; + +import Lexer from "../lexer"; +import SourceTokenReader from "./source-token-reader"; + +type PrefixOperator = "+" | "-" | "!"; +type InfixOperator = "+" | "-" | "*" | "/" | "!=" | "==" | ">" | "<" | ">=" | "<="; + export default class Parser { - private buffer: TokenReader; + private static readonly PREFIX_OPERATORS = ["+", "-", "!"] as const; + private static readonly INFIX_OPERATORS = ["+", "-", "*", "/", "!=", "==", ">", "<", ">=", "<="] as const; + + private reader: SourceTokenReader; constructor(lexer: Lexer) { - this.buffer = new TokenReader(lexer); + this.reader = new SourceTokenReader(lexer); } - parseProgram(): Program { - const program = makeProgram(); + parseSource(): Node.ProgramNode { + const statements: Node.StatementNode[] = []; - while (!this.buffer.isEnd()) { - const statement = this.parseStatement(); - if (statement !== null) { - program.statements.push(statement); - } + while (!this.reader.isEnd()) { + statements.push(this.parseStatement()); } + const firstPos = { row: 0, col: 0 }; + const posBegin = statements.length > 0 ? statements[0].range.begin : firstPos; + const posEnd = statements.length > 0 ? statements[statements.length-1].range.end : firstPos; + + const program = node.createProgramNode({ statements }, posBegin, posEnd); + return program; } - private parseBlock(): Block { - // eat token if block start delimiter; otherwise throw error - const maybeBlockStart = this.buffer.read(); - if (maybeBlockStart.type !== "block delimiter" || maybeBlockStart.value !== "{") { - throw new Error(`expected { but received ${maybeBlockStart.type}`); - } - this.buffer.next(); + private parseBlock(): Node.BlockNode { + const firstToken = this.reader.read(); + this.advanceOrThrow("block delimiter", "{", BadBlockDelimiterError); - // populate statements in block - const statements: Statement[] = []; + const statements: Node.StatementNode[] = []; while (true) { - const token = this.buffer.read(); - - // end block delimiter token and break loop if end of block + const token = this.reader.read(); if (token.type === "block delimiter" && token.value === "}") { - this.buffer.next(); - break; + this.reader.advance(); + + const range = copyRange(firstToken.range.begin, token.range.end); + return node.createBlockNode({ statements }, range); } - // append statement to block const statement = this.parseStatement(); - if (statement !== null) { - statements.push(statement); - } + statements.push(statement); } - - // make and return block - const block = makeBlock(statements); - return block; } - private parseStatement(): Statement { - const token = this.buffer.read(); - - if (token.type === "keyword" && token.value === "만약") { - this.buffer.next(); + private parseStatement(): Node.StatementNode { + const token = this.reader.read(); + const { type, value } = token; + if (type === "keyword" && value === "만약") { return this.parseBranchStatement(); } - if (token.type === "keyword" && token.value === "결과") { - this.buffer.next(); - + if (type === "keyword" && value === "결과") { return this.parseReturnStatement(); } return this.parseExpressionStatement(); } - private parseBranchStatement(): BranchStatement { - const predicate = this.parseExpression(bindingPower.lowest); + private parseBranchStatement(): Node.BranchNode { + const firstToken = this.reader.read(); + this.reader.advance(); + + const predicate = this.parseExpression(bindingPowers.lowest); const consequence = this.parseBlock(); - // eat token if else token; otherwise early return without else block - const maybeElseToken = this.buffer.read(); + const maybeElseToken = this.reader.read(); if (maybeElseToken.type !== "keyword" || maybeElseToken.value !== "아니면") { - const branchStatement = makeBranchStatement(predicate, consequence); - return branchStatement; + const range = { begin: firstToken.range.begin, end: consequence.range.end }; + return node.createBranchNode({ predicate, consequence }, range); } - this.buffer.next(); + this.reader.advance(); - // return statement with else block const alternative = this.parseBlock(); - const branchStatement = makeBranchStatement(predicate, consequence, alternative); - return branchStatement; + const range = { begin: firstToken.range.begin, end: alternative.range.end }; + return node.createBranchNode({ predicate, consequence, alternative }, range); } - private parseReturnStatement(): ReturnStatement { - const expression = this.parseExpression(bindingPower.lowest); + private parseReturnStatement(): Node.ReturnNode { + const firstToken = this.reader.read(); + this.reader.advance(); - return makeReturnStatement(expression); + const expression = this.parseExpression(bindingPowers.lowest); + const range = { begin: firstToken.range.begin, end: expression.range.end }; + return node.createReturnNode({ expression }, range); } - private parseExpressionStatement(): ExpressionStatement { - const expression = this.parseExpression(bindingPower.lowest); + /** return expression statement node, which is just a statement wrapper for an expression */ + private parseExpressionStatement(): Node.ExpressionStatementNode { + const expression = this.parseExpression(bindingPowers.lowest); - return makeExpressionStatement(expression); + const range = expression.range; + return node.createExpressionStatementNode({ expression }, range); } - private parseExpression(threshold: BindingPower): Expression { - let expression = this.parsePrefixExpression(); + private parseExpression(threshold: BindingPowerEntry): Node.ExpressionNode { + let topNode = this.parseExpressionStart(); while (true) { - const nextBindingPower = getBindingPower(this.buffer.read().value); - if (nextBindingPower <= threshold) { + const nextBindingPower = getInfixBindingPower(this.reader.read().value); + if (nextBindingPower.left <= threshold.right) { break; } - const infixExpression = this.parseInfixExpression(expression); + const infixExpression = this.parseExpressionMiddle(topNode); if (infixExpression === null) { break; } - expression = infixExpression; + topNode = infixExpression; } - return expression; + return topNode; } - private parsePrefixExpression(): Expression { - const token = this.buffer.read(); - this.buffer.next(); // eat token + private parseExpressionStart(): Node.ExpressionNode { + const { type, value, range } = this.reader.read(); - if (token.type === "number literal") { - const numberNode = this.parseNumberLiteral(token.value); - return numberNode; + if (type === "number literal") { + return this.parseNumberLiteral(); } - if (token.type === "boolean literal") { - const booleanNode = this.parseBooleanLiteral(token.value); - return booleanNode; + if (type === "boolean literal") { + return this.parseBooleanLiteral(); } - if (token.type === "string literal") { - const stringNode = this.parseStringLiteral(token.value); - return stringNode; + if (type === "string literal") { + return this.parseStringLiteral(); } - if (token.type === "identifier") { - const identifier = makeIdentifier(token.value); - return identifier; + if (type === "identifier") { + return this.parseIdentifier(); } - if ( - token.type === "operator" && - (token.value === "+" || token.value === "-" || token.value === "!") - ) { - const subExpression = this.parseExpression(bindingPower.prefix); - const prefix = token.value; - const expression = makePrefixExpression(prefix, subExpression); - return expression; + if (type === "operator" && this.isPrefixOperator(value)) { + return this.parsePrefix(); } - if (token.type === "keyword" && token.value === "함수") { - const parameters = this.parseParameters(); - const body = this.parseBlock(); - - const functionExpression = makeFunctionExpression(body, parameters); - return functionExpression; + if (type === "keyword" && value === "함수") { + return this.parseFunction(); } - if (token.type === "group delimiter" && token.value === "(") { - const groupedExpression = this.parseExpression(bindingPower.lowest); - - const nextToken = this.buffer.read(); - this.buffer.next(); // eat token - if (nextToken.type !== "group delimiter" || nextToken.value !== ")") { - throw new Error(`expected ) but received ${nextToken.type}`); - } - - return groupedExpression; + if (type === "group delimiter" && value === "(") { + return this.parseGroupedExpression(); } - throw new Error(`bad token type ${token.type} (${token.value}) for prefix expression`); + throw new BadExpressionError(type, "expression", range); } - private parseParameters(): Identifier[] { - const parameters: Identifier[] = []; + /** return node if parsable; null otherwise **/ + private parseExpressionMiddle(left: Node.ExpressionNode): Node.ExpressionNode | null { + const { type, value } = this.reader.read(); + + if (type === "group delimiter" && value === "(") { + if (left.type !== "function" && left.type !== "identifier") { + return null; + } - const maybeGroupStart = this.buffer.read(); - if (maybeGroupStart.type !== "group delimiter" || maybeGroupStart.value !== "(") { - throw new Error(`expected ( but received ${maybeGroupStart.type}`); + return this.parseCall(left); } - 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 []; + if (type === "operator" && this.isInfixOperator(value)) { + return this.parseInfix(left); } - const maybeIdentifier = maybeIdentifierOrGroupEnd; - // read first parameter - if (maybeIdentifier.type !== "identifier") { - throw new Error(`expected identifier but received ${maybeIdentifier}`); + if (type === "operator" && value === "=" && left.type === "identifier") { + return this.parseAssignment(left); } - const identifier = maybeIdentifier; - parameters.push(identifier); - // read the rest parameters - while (true) { - const maybeCommaOrGroupEnd = this.buffer.read(); - this.buffer.next(); + return null; + } - // break if end of parameter list - if (maybeCommaOrGroupEnd.type === "group delimiter" && maybeCommaOrGroupEnd.value === ")") { - break; - } - const maybeComma = maybeCommaOrGroupEnd; + private parseCall(left: Node.FunctionNode | Node.IdentifierNode): Node.CallNode { + this.advanceOrThrow("group delimiter", "(", BadGroupDelimiterError); - // read comma - if (maybeComma.type !== "separator") { - throw new Error(`expected comma but received ${maybeComma}`); - } + const secondToken = this.reader.read(); + if (secondToken.type === "group delimiter" && secondToken.value === ")") { + this.reader.advance(); // eat delimiter - // read next identifier - const maybeIdentifier = this.buffer.read(); - this.buffer.next(); - if (maybeIdentifier.type !== "identifier") { - throw new Error(`expected identifier but received ${maybeIdentifier}`); + const range = copyRange(left.range.begin, secondToken.range.end); + return node.createCallNode({ func: left, args: [] }, range); + } + + const args = [this.parseExpression(bindingPowers.lowest)]; + while (true) { + const token = this.reader.read(); + if (token.type !== "separator") { + break; } - const identifier = maybeIdentifier; + this.reader.advance(); // eat comma - parameters.push(identifier); + args.push(this.parseExpression(bindingPowers.lowest)); } - return parameters; - } + const lastToken = this.reader.read(); + this.advanceOrThrow("group delimiter", ")", BadGroupDelimiterError); - private parseInfixExpression(left: Expression): Expression | null { - // note: do not eat token and just return null if not parsable - const token = this.buffer.read(); + const range = copyRange(left.range.begin, lastToken.range.end); - if (token.type === "group delimiter" && token.value === "(") { - if (left.type !== "function expression" && left.type !== "identifier") { - return null; - } + return node.createCallNode({ func: left, args }, range); + } - this.buffer.next(); // eat infix token - return this.parseCall(left); - } + private parseAssignment(left: Node.IdentifierNode): Node.ExpressionNode { + const { value, range } = this.reader.read(); + this.reader.advance(); - if (token.type !== "operator") { - return null; + if (value !== "=") { + throw new BadAssignmentError(value, "=", range); } + const infix = value; + + const infixBindingPower = getInfixBindingPower(infix); + const right = this.parseExpression(infixBindingPower); + const assignmentRange = { begin: left.range.begin, end: right.range.end }; + + return node.createAssignmentNode({ left, right }, assignmentRange); + } - const infix = token.value; - if (infix === "=" && left.type === "identifier") { - this.buffer.next(); // eat infix token - const a= this.parseAssignment(left); - return a; + private parseNumberLiteral(): Node.NumberNode { + const { value, range } = this.reader.read(); + this.reader.advance(); + + const parsed = Number(value); + if (Number.isNaN(parsed)) { + throw new BadNumberLiteralError(value, "non NaN", range); } - if ( - infix === "+" || - infix === "-" || - infix === "*" || - infix === "/" || - infix === "!=" || - infix === "==" || - infix === ">" || - infix === "<" || - infix === ">=" || - infix === "<=" - ) { - this.buffer.next(); // eat infix token - return this.parseArithmeticInfixExpression(left, infix); + + return node.createNumberNode({ value: parsed }, range); + } + + private parseBooleanLiteral(): Node.BooleanNode { + const { value, range } = this.reader.read(); + this.reader.advance(); + + let parsed: boolean; + if (value === "참") { + parsed = true; + } else if (value === "거짓") { + parsed = false; + } else { + throw new BadBooleanLiteralError(value, "참, 거짓", range); } - return null; + + return node.createBooleanNode({ value: parsed }, range); } - private parseCall(functionToCall: Identifier | FunctionExpression): Call { - const callArguments = this.parseCallArguments(); + private parseStringLiteral(): Node.StringNode { + const { value, range } = this.reader.read(); + this.reader.advance(); - return makeCall(functionToCall, callArguments); + return node.createStringNode({ value }, range); } - private parseCallArguments(): Expression[] { - const maybeExpressionOrGroupEnd = this.buffer.read(); - if (maybeExpressionOrGroupEnd.type === "group delimiter" && maybeExpressionOrGroupEnd.value === ")") { - this.buffer.next(); + private parseIdentifier(): Node.IdentifierNode { + const { type, value, range } = this.reader.read(); + this.reader.advance(); - return []; + if (type !== "identifier") { + throw new BadIdentifierError(type, "identifier", range); } - const firstArgument = this.parseExpression(bindingPower.lowest); + return node.createIdentifierNode({ value }, range); + } - const callArguments = [firstArgument]; - while (true) { - const maybeComma = this.buffer.read(); - if (maybeComma.type !== "separator") { - break; - } - this.buffer.next(); + private parsePrefix(): Node.PrefixNode { + const { value, range } = this.reader.read(); + this.reader.advance(); - const argument = this.parseExpression(bindingPower.lowest); - callArguments.push(argument); + if (!this.isPrefixOperator(value)) { + throw new BadPrefixError(value, "prefix operator", range); } - // expect ')' - const maybeGroupEnd = this.buffer.read(); - this.buffer.next(); - if (maybeGroupEnd.type !== "group delimiter" || maybeGroupEnd.value !== ")") { - throw new Error(`expect ) but received ${maybeGroupEnd.type}`); - } + const prefix = value; + const right = this.parseExpression(bindingPowers.prefix); - return callArguments; + return node.createPrefixNode({ prefix, right }, range); } - private parseAssignment(left: Identifier): Expression { - const infix = "="; - const infixBindingPower = getBindingPower(infix); + private parseInfix(left: Node.ExpressionNode): Node.InfixNode { + const { value, range } = this.reader.read(); + this.reader.advance(); + + if (!this.isInfixOperator(value)) { + throw new BadInfixError(value, "infix operator", range); + } + const infix = value; + const infixBindingPower = getInfixBindingPower(infix); const right = this.parseExpression(infixBindingPower); + const infixRange = copyRange(left.range.begin, right.range.end); - return makeAssignment(left, right); + return node.createInfixNode({ infix, left, right }, infixRange); } - private parseArithmeticInfixExpression(left: Expression, infix: InfixExpression["infix"]): Expression { - const infixBindingPower = getBindingPower(infix); + private parseFunction(): Node.FunctionNode { + const firstToken = this.reader.read(); - const right = this.parseExpression(infixBindingPower); + this.advanceOrThrow("keyword", "함수", BadFunctionKeywordError); + + const parameters = this.parseParameters(); + const body = this.parseBlock(); - return makeInfixExpression(infix, left, right); + const range = copyRange(firstToken.range.begin, body.range.end); + return node.createFunctionNode({ parameters, body }, range); } - private parseNumberLiteral(literal: string): NumberNode { - const parsedNumber = Number(literal); - if (Number.isNaN(parsedNumber)) { - throw new Error(`expected non-NaN number, but received '${literal}'`); + private parseParameters(): Node.IdentifierNode[] { + this.advanceOrThrow("group delimiter", "(", BadGroupDelimiterError); + + const groupEndOrIdentifier = this.reader.read(); + + // early return if empty parameter list + if (groupEndOrIdentifier.type === "group delimiter" && groupEndOrIdentifier.value === ")") { + this.reader.advance(); + return []; } - return makeNumberNode(parsedNumber); + const parameters = [this.parseIdentifier()]; + + while (true) { + const commaOrGroupEnd = this.reader.read(); + this.reader.advance(); + + if (commaOrGroupEnd.type === "group delimiter" && commaOrGroupEnd.value === ")") { + return parameters; + } + + if (commaOrGroupEnd.type !== "separator") { + throw new BadSeparatorError(commaOrGroupEnd.type, ",", commaOrGroupEnd.range); + } + + parameters.push(this.parseIdentifier()); + } } - private parseBooleanLiteral(literal: "참" | "거짓"): BooleanNode { - const parsedValue = literal === "참"; + private parseGroupedExpression(): Node.ExpressionNode { + this.advanceOrThrow("group delimiter", "(", BadGroupDelimiterError); - return makeBooleanNode(parsedValue); + const expression = this.parseExpression(bindingPowers.lowest); + + this.advanceOrThrow("group delimiter", ")", BadGroupDelimiterError); + + // range change due to group delimiters + const offset = { begin: { row: 0, col: -1 }, end: { row: 0, col: 1 } }; + const range = copyRange(expression.range.begin, expression.range.end, offset); + + return { ...expression, range }; + } + + private advanceOrThrow(type: string, value: string, ErrorClass: typeof ParserError): void { + const token = this.reader.read(); + this.reader.advance(); + + if (token.type !== type || token.value !== value) { + throw new ErrorClass(token.value, value, token.range); + } } - private parseStringLiteral(literal: string): StringNode { - return makeStringNode(literal); + private isPrefixOperator(operator: string): operator is PrefixOperator { + return Parser.PREFIX_OPERATORS.some(prefix => prefix === operator); } -} -export type * from "./syntax-tree"; + private isInfixOperator(operator: string): operator is InfixOperator { + return Parser.INFIX_OPERATORS.some(infix => infix === operator); + } +}; + +export type * from "./syntax-node"; diff --git a/src/parser/syntax-tree/expression/index.ts b/src/parser/syntax-tree/expression/index.ts deleted file mode 100644 index e5ea74d..0000000 --- a/src/parser/syntax-tree/expression/index.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Block } from "../group"; - -export type Expression = - Identifier | - NumberNode | - BooleanNode | - StringNode | - PrefixExpression | - InfixExpression | - FunctionExpression | - Call | - Assignment; - -export interface Identifier { - type: "identifier"; - value: string; -} - -export interface NumberNode { - type: "number node"; - value: number; -} - -export interface BooleanNode { - type: "boolean node"; - value: boolean; -} - -export interface StringNode { - type: "string node"; - value: string; -} - -export interface PrefixExpression { - type: "prefix expression"; - prefix: "+" | "-" | "!"; - expression: Expression; -} - -export interface InfixExpression { - type: "infix expression"; - infix: "+" | "-" | "*" | "/" | "=" | "==" | "!=" | ">" | "<" | ">=" | "<="; - left: Expression; - right: Expression; -} - -export interface FunctionExpression { - type: "function expression"; - parameter: Identifier[], - body: Block; -} - -export interface Call { - type: "call"; - functionToCall: Identifier | FunctionExpression; - callArguments: Expression[]; -} - -export interface Assignment { - type: "assignment"; - left: Identifier; - right: Expression; -} - -export const makeIdentifier = (value: Identifier["value"]): Identifier => ({ - type: "identifier", - value, -}); - -export const makeNumberNode = (value: NumberNode["value"]): NumberNode => ({ - type: "number node", - value, -}); - -export const makeBooleanNode = (value: BooleanNode["value"]): BooleanNode => ({ - type: "boolean node", - value, -}); - -export const makeStringNode = (value: StringNode["value"]): StringNode => ({ - type: "string node", - value, -}); - -export const makePrefixExpression = (prefix: PrefixExpression["prefix"], expression: PrefixExpression["expression"]): PrefixExpression => ({ - type: "prefix expression", - prefix, - expression, -}); - -export const makeInfixExpression = (infix: InfixExpression["infix"], left: InfixExpression["left"], right: InfixExpression["right"]): InfixExpression => ({ - type: "infix expression", - infix, - left, - right, -}); - -export const makeFunctionExpression = (body: FunctionExpression["body"], parameter: FunctionExpression["parameter"] = []): FunctionExpression => ({ - type: "function expression", - parameter, - 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, - right, -}); diff --git a/src/parser/syntax-tree/group/index.ts b/src/parser/syntax-tree/group/index.ts deleted file mode 100644 index 82b1170..0000000 --- a/src/parser/syntax-tree/group/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Statement } from "../statement"; - -export type Group = Program | Block; - -/** a root node for a syntax tree of a program */ -export interface Program { - type: "program"; - statements: Statement[]; -} - -/** a group of statements */ -export interface Block { - type: "block"; - statements: Statement[]; -} - -export const makeProgram = (statements: Program["statements"] = []): Program => ({ - type: "program", - statements, -}); - -export const makeBlock = (statements: Block["statements"] = []): Block => ({ - type: "block", - statements, -}); diff --git a/src/parser/syntax-tree/index.ts b/src/parser/syntax-tree/index.ts deleted file mode 100644 index 38b9138..0000000 --- a/src/parser/syntax-tree/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Group } from "./group"; -import type { Statement } from "./statement"; -import type { Expression } from "./expression"; - -export type Node = Group | Statement | Expression; - -export * from "./expression"; -export * from "./statement"; -export * from "./group"; -export type * from "./expression"; -export type * from "./statement"; -export type * from "./group"; diff --git a/src/parser/syntax-tree/statement/index.ts b/src/parser/syntax-tree/statement/index.ts deleted file mode 100644 index 38603cb..0000000 --- a/src/parser/syntax-tree/statement/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { Block } from "../group"; -import type { Expression } from "../expression"; - -export type Statement = - BranchStatement | - ReturnStatement | - ExpressionStatement; - -export interface BranchStatement { - type: "branch statement"; - predicate: Expression; - consequence: Block; - alternative?: Block; -} - -export interface ReturnStatement { - type: "return statement"; - expression: Expression; -} - -/** A wrapper type to treat a single expression as a statement. */ -export interface ExpressionStatement { - type: "expression statement"; - expression: Expression; -} - -type MakeBranchStatement = ( - predicate: BranchStatement["predicate"], - consequence: BranchStatement["consequence"], - alternative?: BranchStatement["alternative"] -) => BranchStatement; -export const makeBranchStatement: MakeBranchStatement = (predicate, consequence, alternative) => ({ - type: "branch statement", - predicate, - consequence, - alternative, -}); - -type MakeReturnStatement = (expression: ReturnStatement["expression"]) => ReturnStatement; -export const makeReturnStatement: MakeReturnStatement = expression => ({ - type: "return statement", - expression, -}); - -export const makeExpressionStatement = (expression: ExpressionStatement["expression"]): ExpressionStatement => ({ - type: "expression statement", - expression, -}); diff --git a/src/parser/token-reader/index.test.ts b/src/parser/token-reader/index.test.ts deleted file mode 100644 index e345f9c..0000000 --- a/src/parser/token-reader/index.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import Lexer from "../../lexer"; -import TokenReader from "./"; - -describe("read()", () => { - it("read a token", () => { - const input = "42"; - const lexer = new Lexer(input); - const reader = new TokenReader(lexer); - - expect(reader.read()).toEqual({ type: "number literal", value: "42" }); - }); -}); - -describe("next()", () => { - it("read next token", () => { - const input = "42 99"; - const lexer = new Lexer(input); - const reader = new TokenReader(lexer); - - expect(reader.read()).toEqual({ type: "number literal", value: "42" }); - reader.next(); - expect(reader.read()).toEqual({ type: "number literal", value: "99" }); - }); - - it("read end token if end", () => { - const input = ""; - const lexer = new Lexer(input); - const reader = new TokenReader(lexer); - - expect(reader.read().type).toEqual("end"); - reader.next(); - expect(reader.read().type).toEqual("end"); - }); -}); - -describe("isEnd()", () => { - it("return true if end", () => { - const input = ""; - const lexer = new Lexer(input); - const reader = new TokenReader(lexer); - - expect(reader.isEnd()).toBe(true); - }); - - it("return false if not end", () => { - const input = "42"; - const lexer = new Lexer(input); - const reader = new TokenReader(lexer); - - expect(reader.isEnd()).toBe(false); - }); -}); diff --git a/src/parser/token-reader/index.ts b/src/parser/token-reader/index.ts deleted file mode 100644 index 015b0c0..0000000 --- a/src/parser/token-reader/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Lexer from "../../lexer"; -import type { TokenType } from "../../lexer"; - -/** @deprecated */ -export default class TokenReader { - private readonly lexer: Lexer; - private token: TokenType; - - constructor(lexer: Lexer) { - this.lexer = lexer; - this.token = lexer.getToken(); - } - - /** @deprecated */ - isEnd(): boolean { - return this.token.type === "end"; - } - - /** @deprecated */ - read(): TokenType { - return this.token; - } - - /** @deprecated */ - next(): void { - this.token = this.lexer.getToken(); - } -} diff --git a/src/parser/v2.test.ts b/src/parser/v2.test.ts deleted file mode 100644 index 514f1e9..0000000 --- a/src/parser/v2.test.ts +++ /dev/null @@ -1,1046 +0,0 @@ -import Lexer from "../lexer"; -import Parser from "./v2"; -import { - ParserError, - BadExpressionError, -} from "./v2"; -import type { - ProgramNode, -} from "./syntax-node"; - -type SuccessTestCase = { name: string, input: string, expected: E }; -type FailureTestCase = { name: string, input: string, expected: E }; - -describe("parseSource()", () => { - const createParser = (input: string) => { - const lexer = new Lexer(input); - const parser = new Parser(lexer); - - return parser; - }; - - const testSuccess = ({ input, expected }: { input: string, expected: ProgramNode }) => { - const parser = createParser(input); - - const node = parser.parseSource(); - - expect(node).toMatchObject(expected); - }; - - const testFailure = ({ input, expected }: { input: string, expected: typeof ParserError }) => { - const parser = createParser(input); - - expect(() => parser.parseSource()).toThrow(expected); - }; - - describe("creating nodes", () => { - describe("literal expressions", () => { - const cases: SuccessTestCase[] = [ - { - name: "a number literal", - input: "42", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "number", - value: 42, - } - }, - ], - }, - }, - { - name: "a boolean literal", - input: "참", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "boolean", - value: true, - } - }, - ], - }, - }, - { - name: "a string literal", - input: "'foo bar'", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "string", - value: "foo bar", - }, - }, - ], - }, - }, - { - name: "a identifer literal", - input: "foo", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "identifier", - value: "foo", - } - }, - ], - }, - }, - ]; - - it.each(cases)("$name", testSuccess); - }); - - describe("arithmetic expressions", () => { - describe("single number", () => { - const cases: SuccessTestCase[] = [ - { - name: "positive number", - input: "+42", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "prefix", - prefix: "+", - right: { - type: "number", - value: 42, - }, - } - }, - ], - }, - }, - { - name: "negative number", - input: "-42", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "prefix", - prefix: "-", - right: { - type: "number", - value: 42, - }, - } - }, - ], - }, - }, - { - name: "doubly negative number", - input: "--42", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "prefix", - prefix: "-", - right: { - type: "prefix", - prefix: "-", - right: { - type: "number", - value: 42, - }, - }, - }, - }, - ], - }, - }, - ]; - - it.each(cases)("$name", testSuccess); - }); - - describe("left associativity", () => { - const leftAssocCases = [ - { infix: "+", name: "left associative addition" }, - { infix: "-", name: "left associative subtraction" }, - { infix: "*", name: "left associative multiplication" }, - { infix: "/", name: "left associative division" }, - ]; - const leftAssocTestCases: SuccessTestCase[] = leftAssocCases.map(({ infix, name }) => ({ - name, - input: `11 ${infix} 22 ${infix} 33`, - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix", - infix, - left: { - type: "infix", - infix, - left: { - type: "number", - value: 11, - }, - right: { - type: "number", - value: 22, - }, - }, - right: { - type: "number", - value: 33, - }, - }, - }, - ], - }, - })); - - it.each(leftAssocTestCases)("$name", testSuccess); - }); - - describe("associativity among different operations", () => { - const cases: SuccessTestCase[] = [ - { - name: "four operations", - input: "11+22*33/44-55", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix", - infix: "-", - left: { - type: "infix", - infix: "+", - left: { - type: "number", - value: 11, - }, - right: { - type: "infix", - infix: "/", - left: { - type: "infix", - infix: "*", - left: { - type: "number", - value: 22 - }, - right: { - type: "number", - value: 33 - }, - }, - right: { - type: "number", - value: 44, - }, - }, - }, - right: { - type: "number", - value: 55, - }, - }, - }, - ], - }, - }, - { - name: "with grouped", - input: "11+(22+33)", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix", - infix: "+", - left: { - type: "number", - value: 11, - }, - right: { - type: "infix", - infix: "+", - left: { - type: "number", - value: 22, - }, - right: { - type: "number", - value: 33, - }, - }, - }, - }, - ], - }, - }, - ]; - - it.each(cases)("$name", testSuccess); - }); - - }); - - describe("logical expressions", () => { - describe("unary operation", () => { - const cases: SuccessTestCase[] = [ - { - name: "negation expression", - input: "!foo", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "prefix", - prefix: "!", - right: { - type: "identifier", - value: "foo", - }, - }, - }, - ], - }, - }, - { - name: "double negation expression", - input: "!!foo", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "prefix", - prefix: "!", - right: { - type: "prefix", - prefix: "!", - right: { - type: "identifier", - value: "foo", - }, - }, - }, - }, - ], - }, - }, - ]; - - it.each(cases)("$name", testSuccess); - }); - - describe("binary operation", () => { - const infixCases = [ - { name: "equal-to expression", infix: "==" }, - { name: "not-equal-to expression", infix: "!=" }, - { name: "greater-than expression", infix: ">" }, - { name: "less-than expression", infix: "<" }, - { name: "greater-than-or-equal-to expression", infix: ">=" }, - { name: "less-than-or-equal-to expression", infix: "<=" }, - ]; - const infixTestCases: SuccessTestCase[] = infixCases.map(({ name, infix }) => ({ - name, - input: `foo ${infix} bar`, - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix", - infix, - left: { - type: "identifier", - value: "foo", - }, - right: { - type: "identifier", - value: "bar", - }, - }, - }, - ], - }, - })); - - it.each(infixTestCases)("$name", testSuccess); - }); - - describe("right associativity", () => { - const infixCases = [ - { name: "right associative equal-to expression", infix: "==" }, - { name: "right associative not-equal-to expression", infix: "!=" }, - ]; - const infixTestCases: SuccessTestCase[] = infixCases.map(({ name, infix }) => ({ - name, - input: `foo ${infix} bar ${infix} baz`, - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix", - infix, - left: { - type: "identifier", - value: "foo", - }, - right: { - type: "infix", - infix, - left: { - type: "identifier", - value: "bar", - }, - right: { - type: "identifier", - value: "baz", - }, - }, - }, - }, - ], - }, - })); - - it.each(infixTestCases)("$name", testSuccess); - }); - - describe("grouped expression", () => { - const cases: SuccessTestCase[] = [ - { - name: "equal-to and not-equal-to", - input: "(foo == bar) != baz", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "infix", - infix: "!=", - left: { - type: "infix", - infix: "==", - left: { - type: "identifier", - value: "foo", - }, - right: { - type: "identifier", - value: "bar", - }, - }, - right: { - type: "identifier", - value: "baz", - }, - }, - }, - ], - }, - }, - ]; - - it.each(cases)("$name", testSuccess); - }); - }); - - describe("assignment", () => { - const cases: SuccessTestCase[] = [ - { - name: "a single assignment statement", - input: "x = 42", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "assignment", - left: { - type: "identifier", - value: "x", - }, - right: { - type: "number", - value: 42, - }, - }, - }, - ], - }, - }, - { - name: "right associative assignment", - input: "x = y = 42", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "assignment", - left: { - type: "identifier", - value: "x", - }, - right: { - type: "assignment", - left: { - type: "identifier", - value: "y", - }, - right: { - type: "number", - value: 42, - }, - }, - }, - }, - ], - }, - }, - ]; - - it.each(cases)("$name", testSuccess); - }); - - describe("call expressions", () => { - const cases: SuccessTestCase[] = [ - { - name: "call function with identifier", - input: "foo(bar, 42)", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "call", - func: { - type: "identifier", - value: "foo", - }, - args: [ - { - type: "identifier", - value: "bar", - }, - { - type: "number", - value: 42, - }, - ], - }, - }, - ], - }, - }, - { - name: "call function with function literal", - input: "함수(foo){ foo }(42)", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "call", - func: { - type: "function", - parameters: {}, // omit - body: {}, // omit - }, - args: [ - { - type: "number", - value: 42, - }, - ], - }, - }, - ], - }, - }, - ]; - - it.each(cases)("$name", testSuccess); - }); - - describe("function expressions", () => { - const cases: SuccessTestCase[] = [ - { - name: "function expression with parameters", - input: "함수 (foo, bar) { foo + bar }", - expected: { - type: "program", - statements: [ - { - type: "expression statement", - expression: { - type: "function", - parameters: [ - { - type: "identifier", - value: "foo", - }, - { - type: "identifier", - value: "bar", - }, - ], - body: { - type: "block", - statements: [ - { - type: "expression statement", - expression: { - type: "infix", - }, - }, - ], - }, - }, - }, - ], - }, - }, - ]; - - it.each(cases)("$name", testSuccess); - }); - - describe("return statement", () => { - const cases: SuccessTestCase[] = [ - { - name: "return number literal", - input: "결과 42", - expected: { - type: "program", - statements: [ - { - type: "return", - expression: { - type: "number", - value: 42, - }, - }, - ], - }, - }, - ]; - - it.each(cases)("$name", testSuccess); - }); - - describe("branch statement", () => { - const cases: SuccessTestCase[] = [ - { - name: "predicate and consequence", - input: "만약 foo { bar }", - expected: { - type: "program", - statements: [ - { - type: "branch", - predicate: { - type: "identifier", - value: "foo", - }, - consequence: { - type: "block", - statements: [ - { - type: "expression statement", - expression: { - type: "identifier", - value: "bar", - }, - }, - ], - }, - }, - ], - }, - }, - { - name: "predicate and consequence with alternative", - input: "만약 foo { bar } 아니면 { baz }", - expected: { - type: "program", - statements: [ - { - type: "branch", - predicate: { - type: "identifier", - value: "foo", - }, - consequence: { - type: "block", - statements: [ - { - type: "expression statement", - expression: { - type: "identifier", - value: "bar", - }, - }, - ], - }, - alternative: { - type: "block", - statements: [ - { - type: "expression statement", - expression: { - type: "identifier", - value: "baz", - }, - }, - ], - }, - }, - ], - }, - }, - ]; - - it.each(cases)("$name", testSuccess); - }); - }); - - describe("marking positions", () => { - describe("single statements", () => { - describe("literal expressions", () => { - const literalCases = [ - { - name: "number literal", - type: "number", - input: "12345", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 4 }, - }, - }, - { - name: "string literal", - type: "string", - input: "'foo bar'", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 8 }, - }, - }, - { - name: "boolean literal", - type: "boolean", - input: "거짓", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 1 }, - }, - }, - { - name: "identifier literal", - type: "identifier", - input: "foo", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 2 }, - }, - }, - ]; - const cases: SuccessTestCase[] = literalCases.map(({ name, input, range, type }) => ({ - name, - input, - expected: { - type: "program", - range, - statements: [ - { - type: "expression statement", - range, - expression: { - type, - range, - }, - }, - ], - }, - })); - - it.each(cases)("$name", testSuccess); - }); - - describe("single expressions", () => { - const cases: SuccessTestCase[] = [ - { - name: "assignment", - input: "x = 42", - expected: { - type: "program", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 5 }, - }, - statements: [ - { - type: "expression statement", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 5 }, - }, - expression: { - type: "assignment", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 5 }, - }, - left: { - type: "identifier", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 0 }, - }, - }, - right: { - type: "number", - range: { - begin: { row: 0, col: 4 }, - end: { row: 0, col: 5 }, - }, - }, - }, - }, - ], - }, - }, - { - name: "arithmetic expression", - input: "11 + 22", - expected: { - type: "program", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 6 }, - }, - statements: [ - { - type: "expression statement", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 6 }, - }, - expression: { - type: "infix", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 6 }, - }, - left: { - type: "number", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 1 }, - }, - }, - right: { - type: "number", - range: { - begin: { row: 0, col: 5 }, - end: { row: 0, col: 6 }, - }, - }, - }, - }, - ], - }, - }, - { - name: "grouped expression", - input: "(11 + 22)", - expected: { - type: "program", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 8 }, - }, - statements: [ - { - type: "expression statement", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 8 }, - }, - expression: { - type: "infix", - range: { - begin: { row: 0, col: 0 }, - end: { row: 0, col: 8 }, - }, - left: { - type: "number", - range: { - begin: { row: 0, col: 1 }, - end: { row: 0, col: 2 }, - }, - }, - right: { - type: "number", - range: { - begin: { row: 0, col: 6 }, - end: { row: 0, col: 7 }, - }, - }, - }, - }, - ], - }, - }, - { - name: "function expression", - input: "함수(foo) {\n foo\n}", - expected: { - type: "program", - range: { - begin: { row: 0, col: 0, }, - end: { row: 2, col: 0, }, - }, - statements: [ - { - type: "expression statement", - expression: { - type: "function", - range: { - begin: { row: 0, col: 0, }, - end: { row: 2, col: 0, }, - }, - body: { - type: "block", - range: { - begin: { row: 0, col: 8, }, - end: { row: 2, col: 0, }, - }, - }, - }, - }, - ], - }, - }, - { - name: "call expression", - input: "foo(bar, baz)", - expected: { - type: "program", - range: { - }, - statements: [ - { - type: "expression statement", - expression: { - type: "call", - range: { - begin: { row: 0, col: 0, }, - end: { row: 0, col: 12, }, - }, - }, - }, - ], - }, - }, - ]; - - it.each(cases)("$name", testSuccess); - }); - - describe("single statements", () => { - const cases: SuccessTestCase[] = [ - { - name: "branch statement", - input: "만약 foo {\n 11\n} 아니면 {\n 22\n}", - expected: { - type: "program", - range: { - }, - statements: [ - { - type: "branch", - range: { - }, - predicate: { - range: { - begin: { row: 0, col: 3, }, - end: { row: 0, col: 5, }, - }, - }, - consequence: { - type: "block", - range: { - begin: { row: 0, col: 7, }, - end: { row: 2, col: 0, }, - }, - }, - alternative: { - type: "block", - range: { - begin: { row: 2, col: 6, }, - end: { row: 4, col: 0, }, - }, - }, - }, - ], - }, - }, - ]; - - it.each(cases)("$name", testSuccess); - }); - }); - }); - - describe("error handling", () => { - const cases: FailureTestCase[] = [ - { - name: "not parsable expression start", - input: "*3", - expected: BadExpressionError, - } - ]; - - it.each(cases)("$name", testFailure); - }); -}); diff --git a/src/parser/v2.ts b/src/parser/v2.ts deleted file mode 100644 index ab4a025..0000000 --- a/src/parser/v2.ts +++ /dev/null @@ -1,400 +0,0 @@ -import type * as Node from "./syntax-node"; -import * as node from "./syntax-node"; -import { getInfixBindingPower, bindingPowers, type BindingPowerEntry } from "./binding-power"; - -import { copyRange, type Range } from "../util/position"; - -export class ParserError extends Error { - public received: string; - public expected: string; - public range: Range; - - constructor(received: string, expected: string, range: Range) { - super(); - this.received = received; - this.expected = expected; - this.range = range; - } -}; - -export class BadNumberLiteralError extends ParserError {}; -export class BadBooleanLiteralError extends ParserError {}; -export class BadPrefixError extends ParserError {}; -export class BadInfixError extends ParserError {}; -export class BadExpressionError extends ParserError {}; -export class BadGroupDelimiterError extends ParserError {}; -export class BadBlockDelimiterError extends ParserError {}; -export class BadAssignmentError extends ParserError {}; -export class BadFunctionKeywordError extends ParserError {}; -export class BadIdentifierError extends ParserError {}; -export class BadSeparatorError extends ParserError {}; - -import Lexer from "../lexer"; -import SourceTokenReader from "./source-token-reader"; - -type PrefixOperator = "+" | "-" | "!"; -type InfixOperator = "+" | "-" | "*" | "/" | "!=" | "==" | ">" | "<" | ">=" | "<="; - -export default class Parser { - private static readonly PREFIX_OPERATORS = ["+", "-", "!"] as const; - private static readonly INFIX_OPERATORS = ["+", "-", "*", "/", "!=", "==", ">", "<", ">=", "<="] as const; - - private reader: SourceTokenReader; - - constructor(lexer: Lexer) { - this.reader = new SourceTokenReader(lexer); - } - - parseSource(): Node.ProgramNode { - const statements: Node.StatementNode[] = []; - - while (!this.reader.isEnd()) { - statements.push(this.parseStatement()); - } - - const firstPos = { row: 0, col: 0 }; - const posBegin = statements.length > 0 ? statements[0].range.begin : firstPos; - const posEnd = statements.length > 0 ? statements[statements.length-1].range.end : firstPos; - - const program = node.createProgramNode({ statements }, posBegin, posEnd); - - return program; - } - - private parseBlock(): Node.BlockNode { - const firstToken = this.reader.read(); - this.advanceOrThrow("block delimiter", "{", BadBlockDelimiterError); - - const statements: Node.StatementNode[] = []; - while (true) { - const token = this.reader.read(); - if (token.type === "block delimiter" && token.value === "}") { - this.reader.advance(); - - const range = copyRange(firstToken.range.begin, token.range.end); - return node.createBlockNode({ statements }, range); - } - - const statement = this.parseStatement(); - statements.push(statement); - } - } - - private parseStatement(): Node.StatementNode { - const token = this.reader.read(); - const { type, value } = token; - - if (type === "keyword" && value === "만약") { - return this.parseBranchStatement(); - } - - if (type === "keyword" && value === "결과") { - return this.parseReturnStatement(); - } - - return this.parseExpressionStatement(); - } - - private parseBranchStatement(): Node.BranchNode { - const firstToken = this.reader.read(); - this.reader.advance(); - - const predicate = this.parseExpression(bindingPowers.lowest); - const consequence = this.parseBlock(); - - const maybeElseToken = this.reader.read(); - if (maybeElseToken.type !== "keyword" || maybeElseToken.value !== "아니면") { - const range = { begin: firstToken.range.begin, end: consequence.range.end }; - return node.createBranchNode({ predicate, consequence }, range); - } - this.reader.advance(); - - const alternative = this.parseBlock(); - const range = { begin: firstToken.range.begin, end: alternative.range.end }; - return node.createBranchNode({ predicate, consequence, alternative }, range); - } - - private parseReturnStatement(): Node.ReturnNode { - const firstToken = this.reader.read(); - this.reader.advance(); - - const expression = this.parseExpression(bindingPowers.lowest); - const range = { begin: firstToken.range.begin, end: expression.range.end }; - return node.createReturnNode({ expression }, range); - } - - /** return expression statement node, which is just a statement wrapper for an expression */ - private parseExpressionStatement(): Node.ExpressionStatementNode { - const expression = this.parseExpression(bindingPowers.lowest); - - const range = expression.range; - return node.createExpressionStatementNode({ expression }, range); - } - - private parseExpression(threshold: BindingPowerEntry): Node.ExpressionNode { - let topNode = this.parseExpressionStart(); - - while (true) { - const nextBindingPower = getInfixBindingPower(this.reader.read().value); - if (nextBindingPower.left <= threshold.right) { - break; - } - - const infixExpression = this.parseExpressionMiddle(topNode); - if (infixExpression === null) { - break; - } - - topNode = infixExpression; - } - - return topNode; - } - - private parseExpressionStart(): Node.ExpressionNode { - const { type, value, range } = this.reader.read(); - - if (type === "number literal") { - return this.parseNumberLiteral(); - } - if (type === "boolean literal") { - return this.parseBooleanLiteral(); - } - if (type === "string literal") { - return this.parseStringLiteral(); - } - if (type === "identifier") { - return this.parseIdentifier(); - } - if (type === "operator" && this.isPrefixOperator(value)) { - return this.parsePrefix(); - } - if (type === "keyword" && value === "함수") { - return this.parseFunction(); - } - if (type === "group delimiter" && value === "(") { - return this.parseGroupedExpression(); - } - - throw new BadExpressionError(type, "expression", range); - } - - /** return node if parsable; null otherwise **/ - private parseExpressionMiddle(left: Node.ExpressionNode): Node.ExpressionNode | null { - const { type, value } = this.reader.read(); - - if (type === "group delimiter" && value === "(") { - if (left.type !== "function" && left.type !== "identifier") { - return null; - } - - return this.parseCall(left); - } - - if (type === "operator" && this.isInfixOperator(value)) { - return this.parseInfix(left); - } - - if (type === "operator" && value === "=" && left.type === "identifier") { - return this.parseAssignment(left); - } - - return null; - } - - private parseCall(left: Node.FunctionNode | Node.IdentifierNode): Node.CallNode { - this.advanceOrThrow("group delimiter", "(", BadGroupDelimiterError); - - const secondToken = this.reader.read(); - if (secondToken.type === "group delimiter" && secondToken.value === ")") { - this.reader.advance(); // eat delimiter - - const range = copyRange(left.range.begin, secondToken.range.end); - return node.createCallNode({ func: left, args: [] }, range); - } - - const args = [this.parseExpression(bindingPowers.lowest)]; - while (true) { - const token = this.reader.read(); - if (token.type !== "separator") { - break; - } - this.reader.advance(); // eat comma - - args.push(this.parseExpression(bindingPowers.lowest)); - } - - const lastToken = this.reader.read(); - this.advanceOrThrow("group delimiter", ")", BadGroupDelimiterError); - - const range = copyRange(left.range.begin, lastToken.range.end); - - return node.createCallNode({ func: left, args }, range); - } - - private parseAssignment(left: Node.IdentifierNode): Node.ExpressionNode { - const { value, range } = this.reader.read(); - this.reader.advance(); - - if (value !== "=") { - throw new BadAssignmentError(value, "=", range); - } - const infix = value; - - const infixBindingPower = getInfixBindingPower(infix); - const right = this.parseExpression(infixBindingPower); - const assignmentRange = { begin: left.range.begin, end: right.range.end }; - - return node.createAssignmentNode({ left, right }, assignmentRange); - } - - private parseNumberLiteral(): Node.NumberNode { - const { value, range } = this.reader.read(); - this.reader.advance(); - - const parsed = Number(value); - if (Number.isNaN(parsed)) { - throw new BadNumberLiteralError(value, "non NaN", range); - } - - return node.createNumberNode({ value: parsed }, range); - } - - private parseBooleanLiteral(): Node.BooleanNode { - const { value, range } = this.reader.read(); - this.reader.advance(); - - let parsed: boolean; - if (value === "참") { - parsed = true; - } else if (value === "거짓") { - parsed = false; - } else { - throw new BadBooleanLiteralError(value, "참, 거짓", range); - } - - return node.createBooleanNode({ value: parsed }, range); - } - - private parseStringLiteral(): Node.StringNode { - const { value, range } = this.reader.read(); - this.reader.advance(); - - return node.createStringNode({ value }, range); - } - - private parseIdentifier(): Node.IdentifierNode { - const { type, value, range } = this.reader.read(); - this.reader.advance(); - - if (type !== "identifier") { - throw new BadIdentifierError(type, "identifier", range); - } - - return node.createIdentifierNode({ value }, range); - } - - private parsePrefix(): Node.PrefixNode { - const { value, range } = this.reader.read(); - this.reader.advance(); - - if (!this.isPrefixOperator(value)) { - throw new BadPrefixError(value, "prefix operator", range); - } - - const prefix = value; - const right = this.parseExpression(bindingPowers.prefix); - - return node.createPrefixNode({ prefix, right }, range); - } - - private parseInfix(left: Node.ExpressionNode): Node.InfixNode { - const { value, range } = this.reader.read(); - this.reader.advance(); - - if (!this.isInfixOperator(value)) { - throw new BadInfixError(value, "infix operator", range); - } - const infix = value; - - const infixBindingPower = getInfixBindingPower(infix); - const right = this.parseExpression(infixBindingPower); - const infixRange = copyRange(left.range.begin, right.range.end); - - return node.createInfixNode({ infix, left, right }, infixRange); - } - - private parseFunction(): Node.FunctionNode { - const firstToken = this.reader.read(); - - this.advanceOrThrow("keyword", "함수", BadFunctionKeywordError); - - const parameters = this.parseParameters(); - const body = this.parseBlock(); - - const range = copyRange(firstToken.range.begin, body.range.end); - return node.createFunctionNode({ parameters, body }, range); - } - - private parseParameters(): Node.IdentifierNode[] { - this.advanceOrThrow("group delimiter", "(", BadGroupDelimiterError); - - const groupEndOrIdentifier = this.reader.read(); - - // early return if empty parameter list - if (groupEndOrIdentifier.type === "group delimiter" && groupEndOrIdentifier.value === ")") { - this.reader.advance(); - return []; - } - - const parameters = [this.parseIdentifier()]; - - while (true) { - const commaOrGroupEnd = this.reader.read(); - this.reader.advance(); - - if (commaOrGroupEnd.type === "group delimiter" && commaOrGroupEnd.value === ")") { - return parameters; - } - - if (commaOrGroupEnd.type !== "separator") { - throw new BadSeparatorError(commaOrGroupEnd.type, ",", commaOrGroupEnd.range); - } - - parameters.push(this.parseIdentifier()); - } - } - - private parseGroupedExpression(): Node.ExpressionNode { - this.advanceOrThrow("group delimiter", "(", BadGroupDelimiterError); - - const expression = this.parseExpression(bindingPowers.lowest); - - this.advanceOrThrow("group delimiter", ")", BadGroupDelimiterError); - - // range change due to group delimiters - const offset = { begin: { row: 0, col: -1 }, end: { row: 0, col: 1 } }; - const range = copyRange(expression.range.begin, expression.range.end, offset); - - return { ...expression, range }; - } - - private advanceOrThrow(type: string, value: string, ErrorClass: typeof ParserError): void { - const token = this.reader.read(); - this.reader.advance(); - - if (token.type !== type || token.value !== value) { - throw new ErrorClass(token.value, value, token.range); - } - } - - private isPrefixOperator(operator: string): operator is PrefixOperator { - return Parser.PREFIX_OPERATORS.some(prefix => prefix === operator); - } - - private isInfixOperator(operator: string): operator is InfixOperator { - return Parser.INFIX_OPERATORS.some(infix => infix === operator); - } -}; - -export type * from "./syntax-node";