From b2b1533ee61660d5936303432fc83a5078680527 Mon Sep 17 00:00:00 2001 From: SimonFJ20 Date: Thu, 27 Apr 2023 02:32:06 +0200 Subject: [PATCH] init --- .gitignore | 1 + checked.ts | 99 +++++++ checker.ts | 55 ++++ deno.jsonc | 10 + lexer.ts | 218 +++++++++++++++ main.ts | 10 + parsed.ts | 126 +++++++++ parser.ts | 758 +++++++++++++++++++++++++++++++++++++++++++++++++++++ token.ts | 76 ++++++ 9 files changed, 1353 insertions(+) create mode 100644 .gitignore create mode 100644 checked.ts create mode 100644 checker.ts create mode 100644 deno.jsonc create mode 100644 lexer.ts create mode 100644 main.ts create mode 100644 parsed.ts create mode 100644 parser.ts create mode 100644 token.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..882c025 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +session.vim diff --git a/checked.ts b/checked.ts new file mode 100644 index 0000000..6904c7c --- /dev/null +++ b/checked.ts @@ -0,0 +1,99 @@ +import { AssignType, BinaryType, UnaryType } from "./parsed.ts"; +import { Position } from "./token.ts"; + +export type CheckedExpr = + & { + pos: Position; + } + & ({ exprType: "error"; message: string } | { + exprType: "unit"; + } | { + exprType: "id"; + value: string; + } | { + exprType: "int"; + value: number; + } | { + exprType: "if"; + condition: CheckedExpr; + truthy: CheckedExpr; + falsy: CheckedExpr | null; + } | { + exprType: "block"; + body: CheckedExpr[]; + } | { + exprType: "call"; + subject: CheckedExpr; + args: CheckedExpr[]; + } | { + exprType: "index"; + subject: CheckedExpr; + value: CheckedExpr; + } | { + exprType: "increment" | "decrement"; + subject: CheckedExpr; + } | { + exprType: "unary"; + unaryType: UnaryType; + subject: CheckedExpr; + } | { + exprType: "binary"; + binaryType: BinaryType; + left: CheckedExpr; + right: CheckedExpr; + } | { + exprType: "assign"; + assignType: AssignType; + subject: CheckedExpr; + value: CheckedExpr; + } | { + exprType: "let"; + param: CheckedParameter; + value: CheckedExpr; + } | { + exprType: "fn"; + subject: string; + params: CheckedParameter[]; + body: CheckedExpr; + } | { + exprType: "return"; + value: CheckedExpr | null; + } | { + exprType: "loop"; + body: CheckedExpr; + } | { + exprType: "while"; + condition: CheckedExpr; + body: CheckedExpr; + } | { + exprType: "break"; + } | { + exprType: "continue"; + }); + +export type CheckedParameter = + & { + pos: Position; + } + & ({ paramType: "error"; message: string } | { + paramType: "value"; + subject: string; + valueType: CheckedType | null; + mutable: boolean; + }); + +export type CheckedType = + & { pos: Position } + & ({ typeType: "error"; message: string } | { + typeType: "unit"; + } | { + typeType: "u16"; + } | { + typeType: "pointer"; + subject: CheckedType; + mutable: boolean; + } | { + typeType: "function"; + params: CheckedParameter[]; + returnType: CheckedType; + }); diff --git a/checker.ts b/checker.ts new file mode 100644 index 0000000..8c1c402 --- /dev/null +++ b/checker.ts @@ -0,0 +1,55 @@ +import { CheckedExpr, CheckedType } from "./checked.ts"; +import { ParsedExpr } from "./parsed.ts"; +import { CompileError } from "./token.ts"; + +export type SymbolValue = { + id: number; + subject: string; + valueType: CheckedType; +}; + +export class SymbolTable { + private idCounter = 0; + private symbols: { [key: string]: SymbolValue } = {}; + + public define(subject: string, valueType: CheckedType) { + if (subject in this.symbols) { + throw new Error("redefinition not implemented"); + } + const id = this.idCounter; + this.idCounter++; + this.symbols[subject] = { id, subject, valueType }; + } +} + +export class Checker { + private symbolTable = new SymbolTable(); + public errors: CompileError[] = []; + + public check(statements: ParsedExpr[]): CheckedExpr[] { + return statements as CheckedExpr[]; + } + + private searchTopLevelStatements(statements: ParsedExpr[]) { + for (const statement of statements) { + switch (statement.exprType) { + case "fn": + this.searchTopLevelFn(statement); + break; + case "let": + break; + default: + this.errors.push({ + pos: statement.pos, + message: + `statement '${statement.exprType}' not supported in top level`, + }); + break; + } + } + } + + private searchTopLevelFn(statement: ParsedExpr & { exprType: "fn" }) { + } +} + diff --git a/deno.jsonc b/deno.jsonc new file mode 100644 index 0000000..ae1f70d --- /dev/null +++ b/deno.jsonc @@ -0,0 +1,10 @@ +{ + "fmt": { + "options": { + "useTabs":false, + "lineWidth": 80, + "indentWidth": 4, + "singleQuote": false + } + } +} diff --git a/lexer.ts b/lexer.ts new file mode 100644 index 0000000..c424b2e --- /dev/null +++ b/lexer.ts @@ -0,0 +1,218 @@ +import { Position, StaticTokenType, Token, TokenIter } from "./token.ts"; + +export class Lexer implements TokenIter { + private index = 0; + private line = 1; + private col = 1; + + public constructor(private text: string) {} + + public next(): Token { + if (this.done()) { + return { pos: this.position(), tokenType: "eof" }; + } else if (" \t\r\n".includes(this.current())) { + this.step(); + while (" \t\r\n".includes(this.current())) this.step(); + return this.next(); + } else if (Lexer.idStartChars.includes(this.current())) { + const pos = this.position(); + let valueString = this.current(); + this.step(); + while (Lexer.idChars.includes(this.current())) { + valueString += this.current(); + this.step(); + } + if (valueString in Lexer.keywordTokenTypes) { + return { pos, tokenType: Lexer.keywordTokenTypes[valueString] }; + } else { + return { + pos, + tokenType: "id", + value: valueString, + }; + } + } else if (Lexer.intStartChars.includes(this.current())) { + const pos = this.position(); + let valueString = this.current(); + this.step(); + while (Lexer.intChars.includes(this.current())) { + valueString += this.current(); + this.step(); + } + return { + pos, + tokenType: "int", + value: parseInt(valueString), + }; + } else if (this.current() in Lexer.singleStaticTokenTypes) { + const pos = this.position(); + const tokenType = Lexer.singleStaticTokenTypes[this.current()]; + this.step(); + return { pos, tokenType }; + } else if (this.currentIs("+")) { + const pos = this.position(); + this.step(); + if (this.currentIs("+")) { + this.step(); + return { pos, tokenType: "plusplus" }; + } else if (this.currentIs("=")) { + this.step(); + return { pos, tokenType: "plusequal" }; + } else { + return { pos, tokenType: "plus" }; + } + } else if (this.currentIs("-")) { + const pos = this.position(); + this.step(); + if (this.currentIs("-")) { + this.step(); + return { pos, tokenType: "minusminus" }; + } else if (this.currentIs(">")) { + this.step(); + return { pos, tokenType: "minusgt" }; + } else if (this.currentIs("=")) { + this.step(); + return { pos, tokenType: "minusequal" }; + } else { + return { pos, tokenType: "minus" }; + } + } else if (this.currentIs("&")) { + const pos = this.position(); + this.step(); + if (this.currentIs("=")) { + this.step(); + return { pos, tokenType: "ampersandequal" }; + } else { + return { pos, tokenType: "ampersand" }; + } + } else if (this.currentIs("|")) { + const pos = this.position(); + this.step(); + if (this.currentIs("=")) { + this.step(); + return { pos, tokenType: "pipeequal" }; + } else { + return { pos, tokenType: "pipe" }; + } + } else if (this.currentIs("^")) { + const pos = this.position(); + this.step(); + if (this.currentIs("=")) { + this.step(); + return { pos, tokenType: "hatequal" }; + } else { + return { pos, tokenType: "hat" }; + } + } else if (this.currentIs("=")) { + const pos = this.position(); + this.step(); + if (this.currentIs("=")) { + this.step(); + return { pos, tokenType: "equalequal" }; + } else { + return { pos, tokenType: "equal" }; + } + } else if (this.currentIs("!")) { + const pos = this.position(); + this.step(); + if (this.currentIs("=")) { + this.step(); + return { pos, tokenType: "exclamationequal" }; + } else { + return { pos, tokenType: "exclamation" }; + } + } else if (this.currentIs("<")) { + const pos = this.position(); + this.step(); + if (this.currentIs("=")) { + this.step(); + return { pos, tokenType: "ltequal" }; + } else { + return { pos, tokenType: "lt" }; + } + } else if (this.currentIs(">")) { + const pos = this.position(); + this.step(); + if (this.currentIs("=")) { + this.step(); + return { pos, tokenType: "gtequal" }; + } else { + return { pos, tokenType: "gt" }; + } + } else { + const pos = this.position(); + this.step(); + return { pos, tokenType: "invalid" }; + } + } + + private static intStartChars = "123456789" as const; + private static intChars = "1234567890" as const; + private static idStartChars = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_" as const; + private static idChars = Lexer.idStartChars + Lexer.intChars; + + private static singleStaticTokenTypes: { [key: string]: StaticTokenType } = + { + "(": "lparen", + ")": "rparen", + "{": "lbrace", + "}": "rbrace", + "[": "lbracket", + "]": "rbracket", + ".": "dot", + ",": "comma", + ":": "colon", + ";": "semicolon", + "~": "tilde", + "*": "asterisk", + } as const; + + private static keywordTokenTypes: { [key: string]: StaticTokenType } = { + "let": "let", + "mut": "mut", + "not": "not", + "and": "and", + "or": "or", + "fn": "fn", + "return": "return", + "if": "fn", + "else": "else", + "loop": "loop", + "while": "while", + "break": "break", + "continue": "continue", + } as const; + + private step() { + this.index++; + if (!this.done()) { + if (this.current() == "\n") { + this.line++; + this.col = 1; + } else { + this.col++; + } + } + } + + private position(): Position { + return { + index: this.index, + line: this.line, + col: this.col, + }; + } + + private currentIs(char: string): boolean { + return !this.done() && this.current() == char; + } + + private current(): string { + return this.text[this.index]; + } + + private done(): boolean { + return this.index >= this.text.length; + } +} diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..91ed071 --- /dev/null +++ b/main.ts @@ -0,0 +1,10 @@ +async function readFromStdin() { + const buffer = new Uint8Array(8192); + const n = await Deno.stdin.read(buffer); + return new TextDecoder().decode(buffer.subarray(0, n!)); +} + +async function main() { +} + +main(); diff --git a/parsed.ts b/parsed.ts new file mode 100644 index 0000000..de3c711 --- /dev/null +++ b/parsed.ts @@ -0,0 +1,126 @@ +import { Position } from "./token.ts"; + +export type UnaryType = + | "negate" + | "log_not" + | "bit_not" + | "addressof" + | "dereference"; + +export type BinaryType = + | "add" + | "subtract" + | "log_and" + | "log_or" + | "bit_and" + | "bit_or" + | "bit_xor" + | "equal" + | "inequal" + | "lt" + | "gt" + | "ltequal" + | "gtequal"; + +export type AssignType = + | "equal" + | "add" + | "subtract" + | "and" + | "or" + | "xor"; + +export type ParsedExpr = + & { + pos: Position; + } + & ({ exprType: "error"; message: string } | { + exprType: "unit"; + } | { + exprType: "id"; + value: string; + } | { + exprType: "int"; + value: number; + } | { + exprType: "if"; + condition: ParsedExpr; + truthy: ParsedExpr; + falsy: ParsedExpr | null; + } | { + exprType: "block"; + body: ParsedExpr[]; + } | { + exprType: "call"; + subject: ParsedExpr; + args: ParsedExpr[]; + } | { + exprType: "index"; + subject: ParsedExpr; + value: ParsedExpr; + } | { + exprType: "increment" | "decrement"; + subject: ParsedExpr; + } | { + exprType: "unary"; + unaryType: UnaryType; + subject: ParsedExpr; + } | { + exprType: "binary"; + binaryType: BinaryType; + left: ParsedExpr; + right: ParsedExpr; + } | { + exprType: "assign"; + assignType: AssignType; + subject: ParsedExpr; + value: ParsedExpr; + } | { + exprType: "let"; + param: ParsedParameter; + value: ParsedExpr; + } | { + exprType: "fn"; + subject: string; + params: ParsedParameter[]; + returnType: ParsedType | null; + body: ParsedExpr; + } | { + exprType: "return"; + value: ParsedExpr | null; + } | { + exprType: "loop"; + body: ParsedExpr; + } | { + exprType: "while"; + condition: ParsedExpr; + body: ParsedExpr; + } | { + exprType: "break"; + } | { + exprType: "continue"; + }); + +export type ParsedParameter = + & { + pos: Position; + } + & ({ paramType: "error"; message: string } | { + paramType: "value"; + subject: string; + valueType: ParsedType | null; + mutable: boolean; + }); + +export type ParsedType = + & { pos: Position } + & ({ typeType: "error"; message: string } | { + typeType: "unit"; + } | { + typeType: "id"; + value: string; + } | { + typeType: "pointer"; + subject: ParsedType; + mutable: boolean; + }); diff --git a/parser.ts b/parser.ts new file mode 100644 index 0000000..67001c6 --- /dev/null +++ b/parser.ts @@ -0,0 +1,758 @@ +import { + BinaryType, + ParsedExpr, + ParsedParameter, + ParsedType, +} from "./parsed.ts"; +import { + CompileError, + Position, + Token, + TokenIter, + TokenType, +} from "./token.ts"; + +export class Parser { + private current: Token; + public errors: CompileError[] = []; + + public constructor(private lexer: TokenIter) { + this.current = lexer.next(); + } + + public parse(): ParsedExpr[] { + while (this.currentIs("semicolon")) this.step(); + const statements: ParsedExpr[] = []; + while (!this.done()) { + while (this.currentIs("semicolon")) this.step(); + } + return statements; + } + + private checkAndSkipStatementLinebreak() { + if (!this.currentIs("semicolon")) { + this.errors.push({ + pos: this.position(), + message: + `expected ';' after statement, got '${this.current.tokenType}'`, + }); + } else { + this.step(); + } + } + + public parseStatement(): ParsedExpr { + if (this.currentIs("fn")) { + return this.parseFn(); + } else if (this.currentIs("if")) { + return this.parseIf(); + } else if (this.currentIs("loop")) { + return this.parseLoop(); + } else if (this.currentIs("while")) { + return this.parseWhile(); + } else if (this.currentIs("return")) { + const statement = this.parseReturn(); + this.checkAndSkipStatementLinebreak(); + return statement; + } else if (this.currentIs("break")) { + const statement = this.parseBreak(); + this.checkAndSkipStatementLinebreak(); + return statement; + } else if (this.currentIs("continue")) { + const statement = this.parseContinue(); + this.checkAndSkipStatementLinebreak(); + return statement; + } else if (this.currentIs("let")) { + const statement = this.parseLet(); + this.checkAndSkipStatementLinebreak(); + return statement; + } else { + const statement = this.parseAssign(); + this.checkAndSkipStatementLinebreak(); + return statement; + } + } + + private parseFn(): ParsedExpr { + const pos = this.position(); + this.step(); + if (this.done() || this.current.tokenType != "id") { + return this.errorExpr( + `expected 'Id' after 'fn' as function name, got '${this.current.tokenType}'`, + ); + } + const subject = this.current.value; + this.step(); + if (!this.currentIs("lparen")) { + return this.errorExpr( + `expected '(' before function-parameters, got '${this.current.tokenType}'`, + ); + } + this.step(); + const params: ParsedParameter[] = []; + params.push(this.parseParam()); + while (this.currentIs("comma")) { + this.step(); + if (this.done() || this.currentIs("rparen")) break; + params.push(this.parseParam()); + } + if (!this.currentIs("rparen")) { + return this.errorExpr( + `expected ')' after function-parameters, got '${this.current.tokenType}'`, + ); + } + this.step(); + let returnType: ParsedType | null = null; + if (this.currentIs("minusgt")) { + this.step(); + returnType = this.parseType(); + } + if (!this.currentIs("lbrace")) { + return this.errorExpr( + `expected '{' after 'loop', got '${this.current.tokenType}'`, + ); + } + const body = this.parseBlock(); + return { + pos, + exprType: "fn", + subject, + params, + returnType, + body, + }; + } + + private parseLoop(): ParsedExpr { + const pos = this.position(); + this.step(); + if (!this.currentIs("lbrace")) { + return this.errorExpr( + `expected '{' after 'loop', got '${this.current.tokenType}'`, + ); + } + const body = this.parseBlock(); + return { + pos, + exprType: "loop", + body, + }; + } + + private parseWhile(): ParsedExpr { + const pos = this.position(); + this.step(); + const condition = this.parseExpr(); + if (!this.currentIs("lbrace")) { + return this.errorExpr( + `expected '{' after while-condition, got '${this.current.tokenType}'`, + ); + } + const body = this.parseBlock(); + return { + pos, + exprType: "while", + condition, + body, + }; + } + + private parseReturn(): ParsedExpr { + const pos = this.position(); + this.step(); + let value: ParsedExpr | null = null; + if (!this.done() && this.current.tokenType != "semicolon") { + value = this.parseExpr(); + } + return { pos, exprType: "return", value }; + } + + private parseBreak(): ParsedExpr { + const pos = this.position(); + this.step(); + return { pos, exprType: "break" }; + } + + private parseContinue(): ParsedExpr { + const pos = this.position(); + this.step(); + return { pos, exprType: "continue" }; + } + + private parseLet(): ParsedExpr { + const pos = this.position(); + this.step(); + const param = this.parseParam(); + if (!this.currentIs("equal")) { + return this.errorExpr( + `expected '=' after let-parameter, got '${this.current.tokenType}'`, + ); + } + this.step(); + const value = this.parseExpr(); + return { + pos, + exprType: "let", + param, + value, + }; + } + + private parseParam(): ParsedParameter { + const pos = this.position(); + const mutable = this.currentIs("mut"); + if (mutable) this.step(); + if (this.done() || this.current.tokenType != "id") { + return this.errorParam( + `expected 'id' in parameter, got '${this.current.tokenType}'`, + ); + } + const subject = this.current.value; + this.step(); + let valueType: ParsedType | null = null; + if (this.currentIs("colon")) { + this.step(); + valueType = this.parseType(); + } + return { + pos, + paramType: "value", + subject, + valueType, + mutable, + }; + } + + private parseAssign(): ParsedExpr { + const pos = this.position(); + const subject = this.parseExpr(); + if (this.currentIs("equal")) { + this.step(); + const value = this.parseExpr(); + return { + pos, + exprType: "assign", + assignType: "equal", + subject, + value, + }; + } else if (this.currentIs("plusequal")) { + this.step(); + const value = this.parseExpr(); + return { + pos, + exprType: "assign", + assignType: "add", + subject, + value, + }; + } else if (this.currentIs("minusequal")) { + this.step(); + const value = this.parseExpr(); + return { + pos, + exprType: "assign", + assignType: "subtract", + subject, + value, + }; + } else if (this.currentIs("ampersandequal")) { + this.step(); + const value = this.parseExpr(); + return { + pos, + exprType: "assign", + assignType: "and", + subject, + value, + }; + } else if (this.currentIs("pipeequal")) { + this.step(); + const value = this.parseExpr(); + return { + pos, + exprType: "assign", + assignType: "or", + subject, + value, + }; + } else if (this.currentIs("hatequal")) { + this.step(); + const value = this.parseExpr(); + return { + pos, + exprType: "assign", + assignType: "xor", + subject, + value, + }; + } else { + return subject; + } + } + + public parseExpr(): ParsedExpr { + return this.parseLogOr(); + } + + private binaryExpr( + binaryType: BinaryType, + left: ParsedExpr, + right: ParsedExpr, + pos: Position, + ): ParsedExpr { + return { + pos, + exprType: "binary", + binaryType, + left, + right, + }; + } + + private parseLogOr(): ParsedExpr { + const pos = this.position(); + let left = this.parseLogAnd(); + while (!this.done()) { + if (this.currentIs("or")) { + this.step(); + const right = this.parseLogAnd(); + left = this.binaryExpr("log_or", left, right, pos); + } else { + break; + } + } + return left; + } + + private parseLogAnd(): ParsedExpr { + const pos = this.position(); + let left = this.parseBitOr(); + while (!this.done()) { + if (this.currentIs("and")) { + this.step(); + const right = this.parseBitOr(); + left = this.binaryExpr("log_and", left, right, pos); + } else { + break; + } + } + return left; + } + + private parseBitOr(): ParsedExpr { + const pos = this.position(); + let left = this.parseBitXor(); + while (!this.done()) { + if (this.currentIs("pipe")) { + this.step(); + const right = this.parseBitXor(); + left = this.binaryExpr("bit_or", left, right, pos); + } else { + break; + } + } + return left; + } + + private parseBitXor(): ParsedExpr { + const pos = this.position(); + let left = this.parseBitAnd(); + while (!this.done()) { + if (this.currentIs("hat")) { + this.step(); + const right = this.parseBitAnd(); + left = this.binaryExpr("bit_xor", left, right, pos); + } else { + break; + } + } + return left; + } + + private parseBitAnd(): ParsedExpr { + const pos = this.position(); + let left = this.parseEquality(); + while (!this.done()) { + if (this.currentIs("ampersand")) { + this.step(); + const right = this.parseEquality(); + left = this.binaryExpr("bit_and", left, right, pos); + } else { + break; + } + } + return left; + } + + private parseEquality(): ParsedExpr { + const pos = this.position(); + let left = this.parseComparison(); + while (!this.done()) { + if (this.currentIs("equalequal")) { + this.step(); + const right = this.parseComparison(); + left = this.binaryExpr("equal", left, right, pos); + } else if (this.currentIs("exclamationequal")) { + this.step(); + const right = this.parseComparison(); + left = this.binaryExpr("inequal", left, right, pos); + } else { + break; + } + } + return left; + } + + private parseComparison(): ParsedExpr { + const pos = this.position(); + const left = this.parseAddSubtract(); + if (this.currentIs("lt")) { + this.step(); + const right = this.parseAddSubtract(); + return this.binaryExpr("lt", left, right, pos); + } else if (this.currentIs("gt")) { + this.step(); + const right = this.parseAddSubtract(); + return this.binaryExpr("gt", left, right, pos); + } else if (this.currentIs("ltequal")) { + this.step(); + const right = this.parseAddSubtract(); + return this.binaryExpr("ltequal", left, right, pos); + } else if (this.currentIs("gtequal")) { + this.step(); + const right = this.parseAddSubtract(); + return this.binaryExpr("gtequal", left, right, pos); + } else { + return left; + } + } + + private parseAddSubtract(): ParsedExpr { + const pos = this.position(); + let left = this.parseUnary(); + while (!this.done()) { + if (this.currentIs("plus")) { + this.step(); + const right = this.parseUnary(); + left = this.binaryExpr("add", left, right, pos); + } else if (this.currentIs("minus")) { + this.step(); + const right = this.parseUnary(); + left = this.binaryExpr("subtract", left, right, pos); + } else { + break; + } + } + return left; + } + + private parseUnary(): ParsedExpr { + if (this.currentIs("minus")) { + const pos = this.position(); + this.step(); + return { + pos, + exprType: "unary", + unaryType: "negate", + subject: this.parseUnary(), + }; + } else if (this.currentIs("not")) { + const pos = this.position(); + this.step(); + return { + pos, + exprType: "unary", + unaryType: "log_not", + subject: this.parseUnary(), + }; + } else if (this.currentIs("tilde")) { + const pos = this.position(); + this.step(); + return { + pos, + exprType: "unary", + unaryType: "bit_not", + subject: this.parseUnary(), + }; + } else if (this.currentIs("ampersand")) { + const pos = this.position(); + this.step(); + return { + pos, + exprType: "unary", + unaryType: "addressof", + subject: this.parseUnary(), + }; + } else if (this.currentIs("asterisk")) { + const pos = this.position(); + this.step(); + return { + pos, + exprType: "unary", + unaryType: "dereference", + subject: this.parseUnary(), + }; + } else { + return this.parseCallIndex(); + } + } + + private parseCallIndex(): ParsedExpr { + const pos = this.position(); + let subject = this.parseOperand(); + while (!this.done()) { + if (this.currentIs("lparen")) { + this.step(); + const args: ParsedExpr[] = []; + if (!this.done() && this.current.tokenType != "rparen") { + args.push(this.parseExpr()); + while (this.currentIs("comma")) { + this.step(); + if (this.done() || this.currentIs("rparen")) { + break; + } + args.push(this.parseExpr()); + } + } + if (!this.currentIs("rparen")) { + return this.errorExpr( + `expected ')', got '${this.current.tokenType}'`, + ); + } + this.step(); + subject = { + pos, + exprType: "call", + subject, + args, + }; + } else if (this.currentIs("lbracket")) { + this.step(); + const value = this.parseExpr(); + if (!this.currentIs("rbracket")) { + return this.errorExpr( + `expected ']', got '${this.current.tokenType}'`, + ); + } + this.step(); + subject = { + pos, + exprType: "index", + subject, + value, + }; + } else { + break; + } + } + return subject; + } + + private parseOperand(): ParsedExpr { + if (this.current.tokenType == "id") { + const pos = this.position(); + const value = this.current.value; + this.step(); + return { + pos, + exprType: "id", + value, + }; + } else if (this.current.tokenType == "int") { + const pos = this.position(); + const value = this.current.value; + this.step(); + return { + pos, + exprType: "int", + value, + }; + } else if (this.currentIs("lparen")) { + const pos = this.position(); + this.step(); + if (this.currentIs("rparen")) { + this.step(); + return { pos, exprType: "unit" }; + } + const subject = this.parseExpr(); + if (!this.currentIs("rparen")) { + return this.errorExpr( + `expected ')', got '${this.current.tokenType}'`, + ); + } + this.step(); + subject.pos = pos; + return subject; + } else if (this.currentIs("if")) { + return this.parseIf(); + } else if (this.currentIs("lbrace")) { + return this.parseBlock(); + } else { + return this.errorExpr( + `expected operand, got '${this.current.tokenType}'`, + ); + } + } + + private parseIf(): ParsedExpr { + const pos = this.position(); + this.step(); + const condition = this.parseExpr(); + if (!this.currentIs("lbrace")) { + return this.errorExpr( + `expected '{' after if-condition, got '${this.current.tokenType}'`, + ); + } + const truthy = this.parseBlock(); + let falsy: ParsedExpr | null = null; + if (this.currentIs("else")) { + this.step(); + if (!this.currentIs("lbrace")) { + return this.errorExpr( + `expected '{' after 'else', got '${this.current.tokenType}'`, + ); + } + falsy = this.parseBlock(); + } + return { + pos, + exprType: "if", + condition, + truthy, + falsy, + }; + } + + private parseBlock(): ParsedExpr { + const pos = this.position(); + this.step(); + while (this.currentIs("semicolon")) this.step(); + const body: ParsedExpr[] = []; + while (!this.done() && this.current.tokenType != "rbrace") { + body.push(this.parseStatement()); + while (this.currentIs("semicolon")) this.step(); + } + if (!this.currentIs("rbrace")) { + return this.errorExpr( + `expected '}' after block, got '${this.current.tokenType}'`, + ); + } + this.step(); + return { + pos, + exprType: "block", + body, + }; + } + + private parseType(): ParsedType { + return this.parsePointerType(); + } + + private parsePointerType(): ParsedType { + if (this.currentIs("asterisk")) { + const pos = this.position(); + this.step(); + let mutable = false; + if (this.currentIs("mut")) { + mutable = true; + this.step(); + } + const subject = this.parseTypeOperand(); + return { + pos, + typeType: "pointer", + subject, + mutable, + }; + } else { + return this.parseTypeOperand(); + } + } + + private parseTypeOperand(): ParsedType { + if (this.current.tokenType == "id") { + const pos = this.position(); + const value = this.current.value; + this.step(); + return { + pos, + typeType: "id", + value, + }; + } else if (this.currentIs("lparen")) { + const pos = this.position(); + this.step(); + if (this.currentIs("rparen")) { + this.step(); + return { pos, typeType: "unit" }; + } + const subject = this.parseType(); + if (!this.currentIs("rparen")) { + return this.errorType( + `expected ')', got '${this.current.tokenType}'`, + ); + } + this.step(); + subject.pos = pos; + return subject; + } else { + return this.errorType( + `expected type, got '${this.current.tokenType}'`, + ); + } + } + + private errorExpr( + message: string, + pos: Position = this.position(), + ): ParsedExpr { + this.errors.push({ pos, message }); + return { + pos, + exprType: "error", + message, + }; + } + + private errorParam( + message: string, + pos: Position = this.position(), + ): ParsedParameter { + this.errors.push({ pos, message }); + return { + pos, + paramType: "error", + message, + }; + } + + private errorType( + message: string, + pos: Position = this.position(), + ): ParsedType { + this.errors.push({ pos, message }); + return { + pos, + typeType: "error", + message, + }; + } + + private position() { + return this.current.pos; + } + + private step() { + this.current = this.lexer.next(); + } + + private currentIs(tokenType: TokenType): boolean { + return this.current.tokenType == tokenType; + } + + private done(): boolean { + return this.current.tokenType == "eof"; + } +} diff --git a/token.ts b/token.ts new file mode 100644 index 0000000..d33e0ca --- /dev/null +++ b/token.ts @@ -0,0 +1,76 @@ +export type Position = { index: number; line: number; col: number }; + +export type CompileError = { pos: Position; message: string }; + +export type DynamicTokenType = "id" | "int"; + +export type StaticTokenType = + | "eof" + | "invalid" + | "lparen" + | "rparen" + | "lbrace" + | "rbrace" + | "lbracket" + | "rbracket" + | "dot" + | "comma" + | "colon" + | "semicolon" + | "plus" + | "plusplus" + | "plusequal" + | "minus" + | "minusminus" + | "minusequal" + | "minusgt" + | "asterisk" + | "tilde" + | "ampersand" + | "ampersandequal" + | "pipe" + | "pipeequal" + | "hat" + | "hatequal" + | "equal" + | "equalequal" + | "exclamation" + | "exclamationequal" + | "lt" + | "gt" + | "ltequal" + | "gtequal" + | "not" + | "and" + | "or" + | "let" + | "mut" + | "fn" + | "return" + | "if" + | "else" + | "loop" + | "while" + | "break" + | "continue"; + +export type TokenType = DynamicTokenType | StaticTokenType; + +export type Token = + & { + pos: Position; + } + & ( + | { tokenType: "id"; value: string } + | { + tokenType: "int"; + value: number; + } + | { + tokenType: StaticTokenType; + } + ); + +export interface TokenIter { + next(): Token; +}