From 33176265406b7ec87249a83f411c1f6c74a9a513 Mon Sep 17 00:00:00 2001 From: Ludwig Lehnert Date: Tue, 4 Mar 2025 20:54:46 +0100 Subject: [PATCH] initial commit --- .gitignore | 42 +++ README.md | 15 ++ antlr4-tool | 15 ++ bun.lock | 32 +++ examples/test.plsm | 70 +++++ lib/mem.c | 0 package.json | 15 ++ src/antlr/plsm.g4 | 448 +++++++++++++++++++++++++++++++ src/const/const.ts | 1 + src/const/float.ts | 72 +++++ src/const/index.ts | 3 + src/const/integer.test.ts | 129 +++++++++ src/const/integer.ts | 158 +++++++++++ src/errors.ts | 38 +++ src/frontend/ast/_index.ts | 9 + src/frontend/ast/block.ts | 13 + src/frontend/ast/decl.ts | 165 ++++++++++++ src/frontend/ast/expr.ts | 112 ++++++++ src/frontend/ast/index.ts | 3 + src/frontend/ast/module.ts | 37 +++ src/frontend/ast/node.ts | 15 ++ src/frontend/ast/stmt.ts | 63 +++++ src/frontend/ast/type-name.ts | 73 +++++ src/frontend/ast/visitor.ts | 245 +++++++++++++++++ src/frontend/semantics/symbol.ts | 0 src/frontend/semantics/type.ts | 45 ++++ src/io/files.ts | 20 ++ src/main.ts | 8 + src/parser.ts | 33 +++ tsconfig.json | 32 +++ 30 files changed, 1911 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 antlr4-tool create mode 100644 bun.lock create mode 100644 examples/test.plsm create mode 100644 lib/mem.c create mode 100644 package.json create mode 100644 src/antlr/plsm.g4 create mode 100644 src/const/const.ts create mode 100644 src/const/float.ts create mode 100644 src/const/index.ts create mode 100644 src/const/integer.test.ts create mode 100644 src/const/integer.ts create mode 100644 src/errors.ts create mode 100644 src/frontend/ast/_index.ts create mode 100644 src/frontend/ast/block.ts create mode 100644 src/frontend/ast/decl.ts create mode 100644 src/frontend/ast/expr.ts create mode 100644 src/frontend/ast/index.ts create mode 100644 src/frontend/ast/module.ts create mode 100644 src/frontend/ast/node.ts create mode 100644 src/frontend/ast/stmt.ts create mode 100644 src/frontend/ast/type-name.ts create mode 100644 src/frontend/ast/visitor.ts create mode 100644 src/frontend/semantics/symbol.ts create mode 100644 src/frontend/semantics/type.ts create mode 100644 src/io/files.ts create mode 100644 src/main.ts create mode 100644 src/parser.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f29302b --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + + +.antlr/ +src/antlr/* +!src/antlr/plsm.g4 + +/test* +/*.test diff --git a/README.md b/README.md new file mode 100644 index 0000000..90c3f67 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# plsm-typescript + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.3. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/antlr4-tool b/antlr4-tool new file mode 100755 index 0000000..822b1e6 --- /dev/null +++ b/antlr4-tool @@ -0,0 +1,15 @@ +#!/bin/env bash + + +DIR=$(dirname "$(realpath $0)") + +mkdir -p .antlr + +ANTLR_PATH="$DIR/.antlr/antlr4-complete.jar" + +if [ ! -f "$ANTLR_PATH" ]; then + wget -qO "$ANTLR_PATH" https://www.antlr.org/download/antlr-4.13.2-complete.jar +fi + +java -jar "$ANTLR_PATH" $@ + diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..a4d72ab --- /dev/null +++ b/bun.lock @@ -0,0 +1,32 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "plsm-typescript", + "dependencies": { + "antlr4": "^4.13.2", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.4", "", { "dependencies": { "bun-types": "1.2.4" } }, "sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA=="], + + "@types/node": ["@types/node@22.13.8", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ=="], + + "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], + + "antlr4": ["antlr4@4.13.2", "", {}, "sha512-QiVbZhyy4xAZ17UPEuG3YTOt8ZaoeOR1CvEAqrEsDBsOqINslaB147i9xqljZqoyf5S+EUlGStaj+t22LT9MOg=="], + + "bun-types": ["bun-types@1.2.4", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q=="], + + "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + } +} diff --git a/examples/test.plsm b/examples/test.plsm new file mode 100644 index 0000000..3c288ca --- /dev/null +++ b/examples/test.plsm @@ -0,0 +1,70 @@ +module test; + +import stdlib/io; + +fun main -> { + io.println(foo() + 10); +} + +trait Expr { + visit(visitor: Visitor): T; +} + +trait ExprVisitor { + visitString(string: String): T; +} + +trait List { + size: int; + get(index: int): T; + set(index: int, value: T): void; +} + +struct LinkedList { + head: T?; + tail: LinkedList?; + + init -> { + self.head = null; + self.tail = null; + } +} + +impl List for LinkedList { + size -> { + if self.head == null { + return 0; + } + + if self.tail == null { + return 1; + } + + return 1 + self.tail.size(); + } + + get(index) -> { + if index == 0 { + return self.head; + } + + return self.tail.get(index - 1); + } + + set(index, value) -> { + if index == 0 { + self.head = value; + return; + } + + if self.tail == null { + raise StringError( + "LinkedList.set(...): index out of bounds", + ); + } + + self.tail.set(index - 1, value,); + } +} + +fun foo -> "Hello World!"; diff --git a/lib/mem.c b/lib/mem.c new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json new file mode 100644 index 0000000..453c379 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "plsm-typescript", + "module": "src/main.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "antlr4": "^4.13.2" + } +} \ No newline at end of file diff --git a/src/antlr/plsm.g4 b/src/antlr/plsm.g4 new file mode 100644 index 0000000..89065dc --- /dev/null +++ b/src/antlr/plsm.g4 @@ -0,0 +1,448 @@ +grammar plsm; + +options { + language = TypeScript; +} + +@header { +import AST from "~/frontend/ast"; +import { Integer, Float } from "~/const"; +} + +@parser::members { + private fileName: string = ''; + public setFileName(fileName: string) { this.fileName = fileName }; + + private getSourceRange(ctx: ParserRuleContext): AST.SourceRange { + const startToken = ctx.start; + const stopToken = this.getCurrentToken(); + // const stopToken = ctx.stop ? ctx.stop : ctx.start; + + return new AST.SourceRange( + this.fileName, + [startToken.line, startToken.column], + [stopToken.line, stopToken.column + stopToken.text.length - 1], + ); + } +} + +module + returns[AST.Module ast = null as any]: + moduleStmt imports += importStmt* decls += topLevelDecl* EOF { + $ast = new AST.Module( + this.getSourceRange($ctx), + $moduleStmt.ast, + $imports.map(import_ => import_.ast), + $decls.map(decl => decl.ast), + ); +}; + +moduleStmt + returns[AST.ModuleDecl ast = null as any]: + 'module' qualifiers += ID ('/' qualifiers += ID)* ';' { + $ast = new AST.ModuleDecl( + this.getSourceRange($ctx), + $qualifiers.map(q => q.text), + ); + }; + +importStmt + returns[AST.ImportStmt ast = null as any]: + 'import' qualifiers += ID ('/' qualifiers += ID)* ( + 'as' alias = ID + )? ';' { + $ast = new AST.ImportStmt( + this.getSourceRange($ctx), + $qualifiers.map(q => q.text), + ); + }; + +topLevelDecl + returns[AST.Decl ast = null as any]: + varDecl { $ast = $varDecl.ast } + | funDecl { $ast = $funDecl.ast } + | structDecl { $ast = $structDecl.ast } + | traitDecl { $ast = $traitDecl.ast } + | implDecl { $ast = $implDecl.ast }; + +block + returns[AST.Block ast = null as any]: + '{' stmts += innerStmt* '}' { + $ast = new AST.Block( + this.getSourceRange($ctx), + $stmts.map(stmt => stmt.ast), + ); +}; + +innerStmt + returns[AST.Stmt ast = null as any]: + expr ';' { + $ast = new AST.ExprStmt( + this.getSourceRange($ctx), + $expr.ast, + ); + } + | varDecl { $ast = $varDecl.ast } + | returnStmt { $ast = $returnStmt.ast } + | raiseStmt { $ast = $raiseStmt.ast } + | ifStmt { $ast = $ifStmt.ast } + | whileStmt { $ast = $whileStmt.ast }; + +varDecl + returns[AST.VarDecl ast = null as any]: + 'let' (name = ID) (':' typeName)? '=' rval = expr ';' { + $ast = new AST.VarDecl( + this.getSourceRange($ctx), + $name.text!, + $ctx.typeName()?.ast ?? null, + $rval.ast, + ); + }; + +funDecl + returns[AST.FunDecl ast = null as any]: + 'fun' name = ID ( + '(' (params += funParam (',' params += funParam)* ','?)? ')' + )? (':' returnType = typeName)? '->' ( + exprBody = expr ';' + | blockBody = block + ) { + let block: AST.Block; + if ($ctx._blockBody?.ast) block = $blockBody.ast; + else block = new AST.Block($exprBody.ast.sourceRange, [ + new AST.ReturnStmt($exprBody.ast.sourceRange, $exprBody.ast), + ]); + + $ast = new AST.FunDecl( + this.getSourceRange($ctx), + $name.text!, + $ctx._returnType?.ast ?? null, + $params.map(p => p.ast), + block, + ); + }; + +funParam + returns[AST.FunParam ast = null as any]: + name = ID ':' typeName { + $ast = new AST.FunParam( + this.getSourceRange($ctx), + $name.text!, + $typeName.ast, + ); + }; + +structDecl + returns[AST.StructDecl ast = null as any]: + 'struct' name = ID ( + '<' typeParams += structTypeParam ( + ',' typeParams += structTypeParam + )* '>' + )? '{' fields += structField* initializers += structInitializer* '}' { + $ast = new AST.StructDecl( + this.getSourceRange($ctx), + $name.text!, + $typeParams.map(p => p.ast), + $fields.map(f => f.ast), + $initializers.map(i => i.ast), + ); + }; + +structField + returns[AST.StructField ast = null as any]: + name = ID ':' typeName ';' { + $ast = new AST.StructField( + this.getSourceRange($ctx), + $name.text!, + $typeName.ast, + ); + }; + +structInitializer + returns[AST.StructInitializer ast = null as any]: + 'init' ( + '(' (params += funParam (',' params += funParam)* ','?)? ')' + )? '->' body = block { + $ast = new AST.StructInitializer( + this.getSourceRange($ctx), + $params.map(p => p.ast), + $body.ast, + ); + }; + +structTypeParam + returns[AST.StructTypeParam ast = null as any]: + name = ID (':' bound = typeName)? { + $ast = new AST.StructTypeParam( + this.getSourceRange($ctx), + $name.text!, + $ctx._bound?.ast ?? null, + ); + }; + +traitDecl + returns[AST.TraitDecl ast = null as any]: + 'trait' name = ID ( + '<' typeParams += traitTypeParam ( + ',' typeParams += traitTypeParam + )* '>' + )? '{' attrs += traitAttr* '}' { + $ast = new AST.TraitDecl( + this.getSourceRange($ctx), + $name.text!, + $typeParams.map(tp => tp.ast), + $attrs.map(attr => attr.ast), + ); + }; + +traitAttr + returns[AST.TraitAttr ast = null as any]: + isConst = 'const'? name = ID ( + '(' (params += funParam (',' params += funParam)* ','?)? ')' + )? ':' returnType = typeName ( + '->' (exprBody = expr ';' | blockBody = block) + | ';' + ) { + let block: AST.Block | null = null; + if ($ctx._blockBody?.ast) block = $blockBody.ast; + else if ($ctx._exprBody?.ast) block = new AST.Block($exprBody.ast.sourceRange, [ + new AST.ReturnStmt($exprBody.ast.sourceRange, $exprBody.ast), + ]); + + $ast = new AST.TraitAttr( + this.getSourceRange($ctx), + !!$isConst.text, + $name.text!, + $params.map(p => p.ast), + $returnType.ast, + block, + ); + }; + +traitTypeParam + returns[AST.TraitTypeParam ast = null as any]: + name = ID (':' bound = typeName)? { + $ast = new AST.TraitTypeParam( + this.getSourceRange($ctx), + $name.text!, + $ctx._bound?.ast ?? null, + ); + }; + +implDecl + returns[AST.ImplDecl ast = null as any]: + 'impl' traitName = qualifiedName ( + '<' traitTypeArgs += typeArg ( + ',' traitTypeArgs += typeArg + )* '>' + )? 'for' structName = qualifiedName ( + '<' structTypeParams += ID (',' structTypeParams += ID)* '>' + )? '{' attrs += implAttr* '}' { + $ast = new AST.ImplDecl( + this.getSourceRange($ctx), + $traitName.text!, + $traitTypeArgs.map(arg => arg.ast), + $structName.text!, + $structTypeParams.map(p => p.text), + $attrs.map(attr => attr.ast), + ); + }; + +implAttr + returns[AST.ImplAttr ast = null as any]: + name = ID ('(' (params += ID (',' params += ID)* ','?)? ')')? '->' ( + exprBody = expr ';' + | blockBody = block + ) { + let block: AST.Block; + if ($ctx._blockBody?.ast) block = $blockBody.ast; + else block = new AST.Block($exprBody.ast.sourceRange, [ + new AST.ReturnStmt($exprBody.ast.sourceRange, $exprBody.ast), + ]); + + $ast = new AST.ImplAttr( + this.getSourceRange($ctx), + $name.text!, + $params.map(p => p.text), + block, + ); + }; + +returnStmt + returns[AST.ReturnStmt ast = null as any]: + 'return' expr? ';' { + $ast = new AST.ReturnStmt( + this.getSourceRange($ctx), + $ctx.expr()?.ast ?? null, + ); + }; + +whileStmt + returns[AST.WhileStmt ast = null as any]: + 'while' cond = expr body = block { + $ast = new AST.WhileStmt( + this.getSourceRange($ctx), + $cond.ast, + $body.ast, + ); + }; + +ifStmt + returns[AST.IfStmt ast = null as any]: + 'if' cond = expr then = block ('else' els = block)? { + $ast = new AST.IfStmt( + this.getSourceRange($ctx), + $cond.ast, + $then.ast, + $ctx._els?.ast ?? null, + ); + }; + +raiseStmt + returns[AST.RaiseStmt ast = null as any]: + 'raise' value = expr ';' { + $ast = new AST.RaiseStmt( + this.getSourceRange($ctx), + $value.ast, + ); + }; + +expr + returns[AST.Expr ast = null as any]: + name = qualifiedName { $ast = new AST.Identifier(this.getSourceRange($ctx), $name.name) } + | val = INT { $ast = new AST.IntExpr(this.getSourceRange($ctx), new Integer($val.text!)) } + | val = FLOAT { $ast = new AST.FloatExpr(this.getSourceRange($ctx), new Float($val.text!)) } + | val = STRING { $ast = new AST.StringExpr(this.getSourceRange($ctx), JSON.parse($val.text!)) } + | lhs = expr op = BINOP rhs = expr { + $ast = new AST.BinExpr(this.getSourceRange($ctx), $op.text!, $lhs.ast, $rhs.ast) ; + } + | cond = expr '?' ifTrue = expr ':' ifFalse = expr { + $ast = new AST.CondExpr(this.getSourceRange($ctx), $cond.ast, $ifTrue.ast, $ifFalse.ast); + } + | callee = expr '(' (args += expr (',' args += expr)* ','?)? ')' { + $ast = new AST.CallExpr( + this.getSourceRange($ctx), + $callee.ast, + $args.map(arg => arg.ast), + ); + } + | lval = expr '=' rval = expr { + $ast = new AST.AssignExpr( + this.getSourceRange($ctx), + $lval.ast, + $rval.ast, + ); + } + | '(' (params += funParam (',' params += funParam)* ','?)? ')' '->' ( + exprBody = expr + | blockBody = block + ) { + let block: AST.Block; + if ($ctx._blockBody?.ast) block = $blockBody.ast; + else block = new AST.Block($exprBody.ast.sourceRange, [ + new AST.ReturnStmt($exprBody.ast.sourceRange, $exprBody.ast), + ]); + + $ast = new AST.LambdaExpr( + this.getSourceRange($ctx), + $params.map(p => p.ast), + block, + ); + } + | '(' expr ')' { $ast = $expr.ast }; + +typeName + returns[AST.TypeName ast = null as any]: + name = ID { $ast = new AST.NamedTypeName(this.getSourceRange($ctx), $name.text!) } + | name = ID '<' typeArgs += typeArg (',' typeArgs += typeArg)* '>' { + $ast = new AST.GenericTypeName( + this.getSourceRange($ctx), + $name.text!, + $typeArgs.map(arg => arg.ast), + ); + } + | type_ = typeName '?' { + $ast = new AST.NullableTypeName( + this.getSourceRange($ctx), + $type_.ast, + ); + } + | type_ = typeName '[' ']' { + $ast = new AST.ArrayTypeName( + this.getSourceRange($ctx), + $type_.ast, + ); + } + | '(' typeNames += typeName (',' typeNames += typeName)+ ','? ')' { + $ast = new AST.TupleTypeName( + this.getSourceRange($ctx), + $typeNames.map(typeName => typeName.ast), + ); + } + | '(' ( + params += lambdaTypeNameParam ( + ',' params += lambdaTypeNameParam + )* ','? + )? ')' '->' returnType = typeName { + $ast = new AST.LambdaTypeName( + this.getSourceRange($ctx), + $params.map(p => p.ast), + $returnType.ast, + ); + }; + +lambdaTypeNameParam + returns[[string | null, AST.TypeName] ast = null as any]: + (name = ID ':')? type_ = typeName { + $ast = [$name.text ?? null, $type_.ast]; + }; + +typeArg + returns[AST.TypeName ast = null as any]: + typeName { $ast = $typeName.ast }; + +qualifiedName + returns[string[] name = []]: + qualifiers += ID ('.' qualifiers += ID)* { + $name = $qualifiers.map(q => q.text); + }; + +BINOP: + '+' + | '-' + | '*' + | '/' + | '%' + | '|' + | '&' + | '^' + | '&&' + | '||' + | '<<' + | '>>' + | '??' + | '**' + | '==' + | '>=' + | '<=' + | '>' + | '<'; + +FLOAT: ('+' | '-')? ([0-9]+ '.' [0-9]* | [0-9]* '.' [0-9]+); + +INT: ('+' | '-')? ( + '0x' [0-9a-fA-F]+ + | '0o' [0-7]+ + | '0b' [0-1]+ + | [0-9]+ + ); + +ID: [_a-zA-Z] [_a-zA-Z0-9]*; + +STRING: '"' (ESC | ~["\\\r\n])* '"'; +fragment ESC: '\\' (["\\/bfnrt] | UNICODE); +fragment UNICODE: 'u' HEX HEX HEX HEX; +fragment HEX: [0-9a-fA-F]; + +WHITESPACE: [ \t\r\n]+ -> skip; +COMMENT: ('/*' .*? '*/' | '//' ~[\r\n]*) -> skip; \ No newline at end of file diff --git a/src/const/const.ts b/src/const/const.ts new file mode 100644 index 0000000..1263338 --- /dev/null +++ b/src/const/const.ts @@ -0,0 +1 @@ +export abstract class Constant { } diff --git a/src/const/float.ts b/src/const/float.ts new file mode 100644 index 0000000..a8e2c1f --- /dev/null +++ b/src/const/float.ts @@ -0,0 +1,72 @@ +import { Constant } from "./const"; +import { makeBigInt, type Integer, type IntegerLike } from "./integer"; + +type FloatLike = Float | IntegerLike; + +export class Float extends Constant { + public readonly man: bigint; + public readonly exp: bigint; + + constructor(man: IntegerLike, exp: IntegerLike = 0) { + super(); + + man = makeBigInt(man); + exp = makeBigInt(exp); + + while (man % 10n === 0n) { + man /= 10n; + exp += 1n; + } + + this.man = man; + this.exp = exp; + } + + public add(other: FloatLike) { + const otherDec = makeFloat(other); + + if (otherDec.exp === this.exp) return new Float(this.man + otherDec.man, this.exp); + + } + + public equals(other: FloatLike) { + const otherDec = makeFloat(other); + return otherDec.exp === this.exp && otherDec.man === this.man; + } + + public gt(other: FloatLike) { + const otherDec = makeFloat(other); + if (otherDec.exp > this.exp) return true; + if (otherDec.exp < this.exp) return false; + return otherDec.man > this.man; + } + + public toString(): string { + let str = this.man.toString(); + + if (this.exp < 0) { + const arr = str.split(''); + arr.splice(str.length + Number(this.exp), 0, '.'); + str = arr.join(''); + } else if (this.exp > 0) { + for (let i = 0n; i < this.exp; i++) { + str += '0'; + } + } + + return str; + } +} + +function makeFloat(value: FloatLike) { + if (value instanceof Float) return value; + + if (typeof value === 'number') value = value.toString(); + + if (typeof value === 'string') { + if (!value.includes('.')) value += '.0'; + + } + + return new Float(value); +} diff --git a/src/const/index.ts b/src/const/index.ts new file mode 100644 index 0000000..5bf4c25 --- /dev/null +++ b/src/const/index.ts @@ -0,0 +1,3 @@ +export * from './const'; +export * from './float'; +export * from './integer'; \ No newline at end of file diff --git a/src/const/integer.test.ts b/src/const/integer.test.ts new file mode 100644 index 0000000..a979247 --- /dev/null +++ b/src/const/integer.test.ts @@ -0,0 +1,129 @@ +import { test, expect } from 'bun:test'; +import { Integer } from './integer'; + +const ITERATIONS = 5000 as const; + +function getRandomBool(): boolean { + return Math.random() < 0.5; +} + +function getRandomInt(factor: number = 500): number { + return Math.floor(Math.random() * factor); +} + +test('Integer.add', () => { + for (let i = ITERATIONS; i > 0; i--) { + const [aSign, bSign] = [getRandomBool(), getRandomBool()]; + const [aVal, bVal] = [getRandomInt(), getRandomInt()]; + const a = aSign ? -aVal : aVal; + const b = bSign ? -bVal : bVal; + const expected = a + b; + + const result = new Integer(BigInt(a)).add(new Integer(BigInt(b))); + + expect(result.toString(), `${a} + ${b}; expected: ${expected}; got: ${result}`).toBe(expected.toString()); + } +}); + +test('Integer.sub', () => { + for (let i = ITERATIONS; i > 0; i--) { + const [aSign, bSign] = [getRandomBool(), getRandomBool()]; + const [aVal, bVal] = [getRandomInt(), getRandomInt()]; + const a = aSign ? -aVal : aVal; + const b = bSign ? -bVal : bVal; + const expected = a - b; + + const result = new Integer(a).sub(new Integer(b)); + + expect(result.toString(), `${a} - ${b}; expected: ${expected}; got: ${result}`).toBe(expected.toString()); + } +}); + +test('Integer.mul', () => { + for (let i = ITERATIONS; i > 0; i--) { + const [aSign, bSign] = [getRandomBool(), getRandomBool()]; + const [aVal, bVal] = [getRandomInt(), getRandomInt()]; + const a = aSign ? -aVal : aVal; + const b = bSign ? -bVal : bVal; + const expected = a * b; + + const result = new Integer(a).mul(new Integer(b)); + + expect(result.toString(), `${a} * ${b}; expected: ${expected}; got: ${result}`).toBe(expected.toString()); + } +}); + +test('Integer.div', () => { + for (let i = ITERATIONS; i > 0; i--) { + const [aSign, bSign] = [getRandomBool(), getRandomBool()]; + const [aVal, bVal] = [getRandomInt(), getRandomInt()]; + if (bVal === 0) continue; + + const a = BigInt(aSign ? -aVal : aVal); + const b = BigInt(bSign ? -bVal : bVal); + const expected = a / b; + + const result = new Integer(a).div(new Integer(b)); + + expect(result.toString(), `${a} / ${b}; expected: ${expected}; got: ${result}`).toBe(expected.toString()); + } +}); + +test('Integer.mod', () => { + for (let i = ITERATIONS; i > 0; i--) { + const [aSign, bSign] = [getRandomBool(), getRandomBool()]; + const [aVal, bVal] = [getRandomInt(), getRandomInt()]; + if (bVal === 0) continue; + + const a = aSign ? -aVal : aVal; + const b = bSign ? -bVal : bVal; + const expected = a % b; + + const result = new Integer(a).mod(new Integer(b)); + + expect(result.toString(), `${a} % ${b}; expected: ${expected}; got: ${result}`).toBe(expected.toString()); + } +}); + +test('Integer.pow', () => { + for (let i = ITERATIONS; i > 0; i--) { + const aSign = getRandomBool(); + const [aVal, b] = [getRandomInt(50), getRandomInt(10)]; + const a = aSign ? -aVal : aVal; + const expected = a ** b; + + const result = new Integer(a).pow(new Integer(b)); + + expect(result.toString(), `${a} ** ${b}; expected: ${expected}; got: ${result}`).toBe(expected.toString()); + } +}); + +test('Integer.gcd', () => { + const TEST_CASES = [ + [48, 18, 6], + [0, 0, 0], + [0, 5, 5], + [7, 0, 7], + [101, 103, 1], + [54, 24, 6], + [36, 60, 12], + [81, 27, 27], + [17, 19, 1], + [100, 80, 20], + [1000000, 2, 2], + [-48, 18, 6], + [48, -18, 6], + [-48, -18, 6], + [-7, 0, 7], + [0, -5, 5], + [-101, 103, 1], + [-54, -24, 6] + ] as const; + + for (const [a, b, expected] of TEST_CASES) { + const result = new Integer(a).gcd(b); + expect(result.toString()).toBe(expected.toString()); + } +}); + + diff --git a/src/const/integer.ts b/src/const/integer.ts new file mode 100644 index 0000000..224f0a2 --- /dev/null +++ b/src/const/integer.ts @@ -0,0 +1,158 @@ +import { Constant } from "./const"; + +const BINARY_DIGITS = [ + '0', + '1', +] as const; + +const OCTAL_DIGITS = [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', +] as const; + +const HEX_DIGITS = [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', +] as const; + +export type IntegerLike = Integer | bigint | number | string; + + +export class Integer extends Constant { + public readonly value: bigint; + + constructor(value: IntegerLike) { + super(); + this.value = makeBigInt(value); + } + + private static fromSystem(value: string, methodName: string, prefix: string, digits: readonly string[]): Integer { + value = value.toLowerCase(); + if (value.startsWith(prefix)) value = value.slice(prefix.length); + if (!value.split('').every(c => digits.includes(c as any))) throw new Error(`Integer.${methodName}(...) called with an invalid value`); + return new Integer(BigInt(prefix + value)); + } + + static fromBinary(value: string): Integer { + return Integer.fromSystem(value, 'fromBinary', '0b', BINARY_DIGITS); + } + + static fromOctal(value: string): Integer { + return Integer.fromSystem(value, 'fromOctal', '0o', OCTAL_DIGITS); + } + + static fromHex(value: string): Integer { + return Integer.fromSystem(value, 'fromHex', '0x', HEX_DIGITS); + } + + public add(other: IntegerLike): Integer { + return new Integer(this.value + makeBigInt(other)); + } + + public sub(other: IntegerLike): Integer { + return new Integer(this.value - makeBigInt(other)); + } + + public mul(other: IntegerLike): Integer { + return new Integer(this.value * makeBigInt(other)); + } + + public div(other: IntegerLike): Integer { + if (makeBigInt(other) === 0n) throw new Error('Integer.div(...) called with divisor of zero'); + return new Integer(this.value / makeBigInt(other)); + } + + public mod(other: IntegerLike): Integer { + if (makeBigInt(other) === 0n) throw new Error('Integer.mod(...) called with divisor of zero'); + return new Integer(this.value % makeBigInt(other)); + } + + public pow(other: IntegerLike): Integer { + if (makeBigInt(other) < 0n) throw new Error('Integer.pow(...) called with negative exponent'); + return new Integer(this.value ** makeBigInt(other)); + } + + public eq(other: IntegerLike): boolean { + return this.value === makeBigInt(other); + } + + public gt(other: IntegerLike): boolean { + return this.value > makeBigInt(other); + } + + public ge(other: IntegerLike): boolean { + return this.value >= makeBigInt(other); + } + + public lt(other: IntegerLike): boolean { + return this.value < makeBigInt(other); + } + + public le(other: IntegerLike): boolean { + return this.value <= makeBigInt(other); + } + + public gcd(other: IntegerLike): Integer { + let a = this.value; + let b = makeBigInt(other); + + if (a < 0n) a = -a; + if (b < 0n) b = -b; + + if (a === 0n) return new Integer(b); + if (b === 0n) return new Integer(a); + + let shift = 0n; + while (((a | b) & 1n) == 0n) { + shift++; + a >>= 1n; + b >>= 1n; + } + + while (b != 0n) { + while ((b & 1n) == 0n) b >>= 1n; + if (a > b) [a, b] = [b, a]; + b -= a; + } + + let result = a << shift; + if (result < 0n) result = -result; + + return new Integer(result); + } + + + public toString(): string { + return this.value.toString(); + } +} + +export function makeBigInt(value: IntegerLike) { + if (value instanceof Integer) return value.value; + return BigInt(value); +} + +// function makeInt(value: IntegerLike): Integer { +// if (value instanceof Integer) return value; +// return new Integer(value); +// } diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..71d33da --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,38 @@ +import type { SourceRange } from "./frontend/ast/node"; +import { getFile } from "./io/files"; + +const errors: [SourceRange, string][] = [] + +export function raiseError(sourceRange: SourceRange, message: string) { + errors.push([sourceRange, message]); +} + +export function hasErrors() { + return errors.length > 0; +} + +export function getErrors() { + return [...errors]; +} + +function getPreviousLines(content: string, line: number, count: number = 6) { + let lines = content.split("\n"); + lines = lines.slice(Math.max(0, line - count), line); + while (!lines[0].trim().length) lines.shift(); + while (!lines[lines.length - 1].trim().length) lines.pop(); + return lines.join('\n'); +} + +export function printErrors() { + for (let i = 0; i < errors.length; i++) { + const [sourceRange, message] = errors[i]; + + const { path, content } = getFile(sourceRange.file); + const ctx = getPreviousLines(content, sourceRange.start[0]); + process.stderr.write(`in ${path}:${sourceRange.start[0]}:${sourceRange.start[1] + 1}\n`); + process.stderr.write('\x1b[90m' + ctx + '\x1b[0m\n'); + process.stderr.write('\x1b[91m' + message + '\x1b[0m\n'); + + if (i < errors.length - 1) process.stderr.write('\n'); + } +} diff --git a/src/frontend/ast/_index.ts b/src/frontend/ast/_index.ts new file mode 100644 index 0000000..14c5a77 --- /dev/null +++ b/src/frontend/ast/_index.ts @@ -0,0 +1,9 @@ +export * from './block'; +export * from './decl'; +export * from './expr'; +export * from './module'; +export * from './node'; +export * from './stmt'; +export * from './type-name'; +export * from './visitor'; + diff --git a/src/frontend/ast/block.ts b/src/frontend/ast/block.ts new file mode 100644 index 0000000..b1dec15 --- /dev/null +++ b/src/frontend/ast/block.ts @@ -0,0 +1,13 @@ +import { Node, SourceRange } from "./node"; +import type { Stmt } from "./stmt"; +import type { Visitor } from "./visitor"; + +export class Block extends Node { + constructor(sourceRange: SourceRange, public stmts: Stmt[]) { + super(sourceRange); + } + + accept(visitor: Visitor): ResultT { + return visitor.visitBlock(this); + } +} \ No newline at end of file diff --git a/src/frontend/ast/decl.ts b/src/frontend/ast/decl.ts new file mode 100644 index 0000000..8ff0450 --- /dev/null +++ b/src/frontend/ast/decl.ts @@ -0,0 +1,165 @@ +import type { Block } from "./block"; +import type { Expr } from "./expr"; +import { Node, SourceRange } from "./node"; +import { Stmt } from "./stmt"; +import type { TypeName } from "./type-name"; +import type { Visitor } from "./visitor"; + +export abstract class Decl extends Stmt { } + +export class VarDecl extends Decl { + constructor( + sourceRange: SourceRange, + public name: string, + public typeName: TypeName | null, + public value: Expr, + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitVarDecl(this); + } +} + +export class FunDecl extends Decl { + constructor( + sourceRange: SourceRange, + public name: string, + public returnTypeName: TypeName | null, + public params: FunParam[], + public body: Block, + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitFunDecl(this); + } +} + +export class FunParam extends Node { + constructor( + sourceRange: SourceRange, + public name: string, + public typeName: TypeName, + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitFunParam(this); + } +} + +export class TraitDecl extends Decl { + constructor( + sourceRange: SourceRange, + public name: string, + public typeParams: TraitTypeParam[], + public attrs: TraitAttr[], + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitTraitDecl(this); + } +} + +export class TraitAttr extends Node { + constructor( + sourceRange: SourceRange, + public isConst: boolean, + public name: string, + public params: FunParam[], + public returnTypeName: TypeName, + public defaultBody: Block | null, + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitTraitAttr(this); + } +} + +export class TraitTypeParam extends Node { + constructor( + sourceRange: SourceRange, + public name: string, + public bound: TypeName | null, + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitTraitTypeParam(this); + } +} + +export class ImplDecl extends Decl { + constructor( + sourceRange: SourceRange, + public traitName: string, + public traitTypeArgs: TypeName[], + public structName: string, + public structTypeParams: string[], + public attrs: ImplAttr[], + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitImplDecl(this); + } +} + +export class ImplAttr extends Decl { + constructor( + sourceRange: SourceRange, + public name: string, + public paramNames: string[], + public body: Block, + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitImplAttr(this); + } +} + +export class StructDecl extends Decl { + constructor( + sourceRange: SourceRange, + public name: string, + public typeParams: StructTypeParam[], + public fields: StructField[], + public initializers: StructInitializer[], + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitStructDecl(this); + } +} + +export class StructField extends Node { + constructor( + sourceRange: SourceRange, + public name: string, + public typeName: TypeName, + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitStructField(this); + } +} + +export class StructInitializer extends Node { + constructor( + sourceRange: SourceRange, + public params: FunParam[], + public body: Block, + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitStructInitializer(this); + } +} + +export class StructTypeParam extends Node { + constructor( + sourceRange: SourceRange, + public name: string, + public bound: TypeName | null, + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitStructTypeParam(this); + } +} diff --git a/src/frontend/ast/expr.ts b/src/frontend/ast/expr.ts new file mode 100644 index 0000000..ba72718 --- /dev/null +++ b/src/frontend/ast/expr.ts @@ -0,0 +1,112 @@ +import { Node, SourceRange } from "./node"; +import type { Visitor } from "./visitor"; +import type { FunParam } from "./decl"; +import type { Block } from "./block"; +import type { Float, Integer } from "~/const"; + +export abstract class Expr extends Node { } + +export class Identifier extends Expr { + constructor(sourceRange: SourceRange, public paths: string[]) { + super(sourceRange); + } + + accept(visitor: Visitor): ResultT { + return visitor.visitIdentifier(this); + } +} + +export class IntExpr extends Expr { + constructor(sourceRange: SourceRange, public value: Integer) { + super(sourceRange); + } + + accept(visitor: Visitor): ResultT { + return visitor.visitIntExpr(this); + } +} + +export class FloatExpr extends Expr { + constructor(sourceRange: SourceRange, public value: Float) { + super(sourceRange); + } + + accept(visitor: Visitor): ResultT { + return visitor.visitFloatExpr(this); + } +} + +export class StringExpr extends Expr { + constructor(sourceRange: SourceRange, public value: string) { + super(sourceRange); + } + + accept(visitor: Visitor): ResultT { + return visitor.visitStringExpr(this); + } +} + + +export class BinExpr extends Expr { + constructor( + sourceRange: SourceRange, + public op: string, + public lhs: Expr, + public rhs: Expr + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitBinExpr(this); + } +} + +export class CallExpr extends Expr { + constructor( + sourceRange: SourceRange, + public callee: Expr, + public args: Expr[], + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitCallExpr(this); + } +} + +export class CondExpr extends Expr { + constructor( + sourceRange: SourceRange, + public cond: Expr, + public then: Expr, + public els: Expr, + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitCondExpr(this); + } +} + +export class LambdaExpr extends Expr { + constructor( + sourceRange: SourceRange, + public params: FunParam[], + public body: Block, + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitLambdaExpr(this); + } +} + +export class AssignExpr extends Expr { + constructor( + sourceRange: SourceRange, + public lval: Expr, + public rval: Expr, + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitAssignExpr(this); + } +} + + diff --git a/src/frontend/ast/index.ts b/src/frontend/ast/index.ts new file mode 100644 index 0000000..4995537 --- /dev/null +++ b/src/frontend/ast/index.ts @@ -0,0 +1,3 @@ +import * as AST from './_index'; + +export default AST; \ No newline at end of file diff --git a/src/frontend/ast/module.ts b/src/frontend/ast/module.ts new file mode 100644 index 0000000..f1ad36d --- /dev/null +++ b/src/frontend/ast/module.ts @@ -0,0 +1,37 @@ +import type { Decl } from "./decl"; +import { Node, SourceRange } from "./node"; +import { Stmt } from "./stmt"; +import type { Visitor } from "./visitor"; + +export class Module extends Node { + constructor( + sourceRange: SourceRange, + public moduleDecl: ModuleDecl, + public imports: ImportStmt[], + public decls: Decl[], + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitModule(this); + } +} + +export class ModuleDecl extends Stmt { + constructor(sourceRange: SourceRange, public path: string[]) { + super(sourceRange); + } + + accept(visitor: Visitor): ResultT { + return visitor.visitModuleDecl(this); + } +} + +export class ImportStmt extends Stmt { + constructor(sourceRange: SourceRange, public path: string[]) { + super(sourceRange); + } + + accept(visitor: Visitor): ResultT { + return visitor.visitImportStmt(this); + } +} diff --git a/src/frontend/ast/node.ts b/src/frontend/ast/node.ts new file mode 100644 index 0000000..2a30e25 --- /dev/null +++ b/src/frontend/ast/node.ts @@ -0,0 +1,15 @@ +import type { Visitor } from "./visitor"; + +export abstract class Node { + constructor(public sourceRange: SourceRange) { } + + abstract accept(visitor: Visitor): ResultT; +} + +export class SourceRange { + constructor( + public file: string, + public start: [number, number], + public end: [number, number], + ) { } +} diff --git a/src/frontend/ast/stmt.ts b/src/frontend/ast/stmt.ts new file mode 100644 index 0000000..28e19dc --- /dev/null +++ b/src/frontend/ast/stmt.ts @@ -0,0 +1,63 @@ +import type { Block } from "./block"; +import type { Expr } from "./expr"; +import { Node, SourceRange } from "./node"; +import type { Visitor } from "./visitor"; + +export abstract class Stmt extends Node { } + +export class ExprStmt extends Stmt { + constructor(sourceRange: SourceRange, public expr: Expr) { + super(sourceRange); + } + + accept(visitor: Visitor): ResultT { + return visitor.visitExprStmt(this); + } +} + +export class ReturnStmt extends Stmt { + constructor(sourceRange: SourceRange, public value: Expr | null) { + super(sourceRange); + } + + accept(visitor: Visitor): ResultT { + return visitor.visitReturnStmt(this); + } +} + +export class IfStmt extends Stmt { + constructor( + sourceRange: SourceRange, + public cond: Expr, + public then: Block, + public els: Block | null + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitIfStmt(this); + } +} + +export class WhileStmt extends Stmt { + constructor( + sourceRange: SourceRange, + public cond: Expr, + public body: Block + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitWhileStmt(this); + } +} + +export class RaiseStmt extends Stmt { + constructor( + sourceRange: SourceRange, + public value: Expr + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitRaiseStmt(this); + } +} + diff --git a/src/frontend/ast/type-name.ts b/src/frontend/ast/type-name.ts new file mode 100644 index 0000000..a86abeb --- /dev/null +++ b/src/frontend/ast/type-name.ts @@ -0,0 +1,73 @@ +import type { Expr } from "./expr"; +import { Node, SourceRange } from "./node"; +import type { Visitor } from "./visitor"; + +export abstract class TypeName extends Node { } + +export class NamedTypeName extends TypeName { + constructor( + sourceRange: SourceRange, + public name: string, + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitNamedTypeName(this); + } +} + +export class GenericTypeName extends TypeName { + constructor( + sourceRange: SourceRange, + public name: string, + public args: TypeName[], + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitGenericTypeName(this); + } +} + +export class NullableTypeName extends TypeName { + constructor( + sourceRange: SourceRange, + public typeName: TypeName, + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitNullableTypeName(this); + } +} + +export class ArrayTypeName extends TypeName { + constructor( + sourceRange: SourceRange, + public typeName: TypeName, + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitArrayTypeName(this); + } +} + +export class TupleTypeName extends TypeName { + constructor( + sourceRange: SourceRange, + public typeNames: TypeName[], + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitTupleTypeName(this); + } +} + +export class LambdaTypeName extends TypeName { + constructor( + sourceRange: SourceRange, + public params: [string | null, TypeName][], + public returnTypeName: TypeName, + ) { super(sourceRange); } + + accept(visitor: Visitor): ResultT { + return visitor.visitLambdaTypeName(this); + } +} diff --git a/src/frontend/ast/visitor.ts b/src/frontend/ast/visitor.ts new file mode 100644 index 0000000..4137ed4 --- /dev/null +++ b/src/frontend/ast/visitor.ts @@ -0,0 +1,245 @@ +import type { Block } from "./block"; +import type { FunDecl, FunParam, ImplAttr, ImplDecl, StructDecl, StructField, StructInitializer, StructTypeParam, TraitAttr, TraitDecl, TraitTypeParam, VarDecl } from "./decl"; +import type { AssignExpr, BinExpr, CallExpr, CondExpr, FloatExpr, Identifier, IntExpr, LambdaExpr, StringExpr } from "./expr"; +import type { ImportStmt, Module, ModuleDecl } from "./module"; +import { IfStmt, RaiseStmt, ReturnStmt, WhileStmt, type ExprStmt } from "./stmt"; +import type { ArrayTypeName, GenericTypeName, LambdaTypeName, NamedTypeName, NullableTypeName, TupleTypeName } from "./type-name"; + +export abstract class Visitor { + public abstract visitModule(module_: Module): ResultT; + public abstract visitModuleDecl(moduleDecl: ModuleDecl): ResultT; + public abstract visitImportStmt(importStmt: ImportStmt): ResultT; + + public abstract visitBlock(block: Block): ResultT; + public abstract visitExprStmt(exprStmt: ExprStmt): ResultT; + public abstract visitReturnStmt(returnStmt: ReturnStmt): ResultT; + public abstract visitIfStmt(ifStmt: IfStmt): ResultT; + public abstract visitWhileStmt(whileStmt: WhileStmt): ResultT; + public abstract visitRaiseStmt(raiseStmt: RaiseStmt): ResultT; + + public abstract visitVarDecl(varDecl: VarDecl): ResultT; + public abstract visitFunDecl(funDecl: FunDecl): ResultT; + public abstract visitFunParam(funParam: FunParam): ResultT; + public abstract visitTraitDecl(traitDecl: TraitDecl): ResultT; + public abstract visitTraitAttr(traitAttr: TraitAttr): ResultT; + public abstract visitTraitTypeParam(traitTypeParam: TraitTypeParam): ResultT; + public abstract visitImplDecl(implDecl: ImplDecl): ResultT; + public abstract visitImplAttr(implAttr: ImplAttr): ResultT; + public abstract visitStructDecl(structDecl: StructDecl): ResultT; + public abstract visitStructField(structField: StructField): ResultT; + public abstract visitStructTypeParam(structTypeParam: StructTypeParam): ResultT; + public abstract visitStructInitializer(structInitializer: StructInitializer): ResultT; + + public abstract visitIdentifier(identifier: Identifier): ResultT; + public abstract visitIntExpr(intExpr: IntExpr): ResultT; + public abstract visitFloatExpr(floatExpr: FloatExpr): ResultT; + public abstract visitStringExpr(stringExpr: StringExpr): ResultT; + public abstract visitBinExpr(binExpr: BinExpr): ResultT; + public abstract visitCallExpr(callExpr: CallExpr): ResultT; + public abstract visitCondExpr(condExpr: CondExpr): ResultT; + public abstract visitLambdaExpr(lambdaExpr: LambdaExpr): ResultT; + public abstract visitAssignExpr(assignExpr: AssignExpr): ResultT; + + public abstract visitNamedTypeName(namedTypeName: NamedTypeName): ResultT; + public abstract visitGenericTypeName(genericTypeName: GenericTypeName): ResultT; + public abstract visitNullableTypeName(nullableTypeName: NullableTypeName): ResultT; + public abstract visitArrayTypeName(arrayTypeName: ArrayTypeName): ResultT; + public abstract visitTupleTypeName(tupleTypeName: TupleTypeName): ResultT; + public abstract visitLambdaTypeName(lambdaTypeName: LambdaTypeName): ResultT; +} + +export abstract class BaseVisitor implements Visitor { + public visitModule(module_: Module): ResultT { + module_.imports.forEach((import_) => import_.accept(this)); + module_.decls.forEach((decl) => decl.accept(this)); + return null as any; + } + + public visitModuleDecl(moduleDecl: ModuleDecl): ResultT { + return null as any; + } + + public visitImportStmt(importStmt: ImportStmt): ResultT { + return null as any; + } + + public visitBlock(block: Block): ResultT { + block.stmts.forEach((stmt) => stmt.accept(this)); + return null as any; + } + + public visitExprStmt(exprStmt: ExprStmt): ResultT { + exprStmt.expr.accept(this); + return null as any; + } + + public visitReturnStmt(returnStmt: ReturnStmt): ResultT { + returnStmt.value?.accept(this); + return null as any; + } + + public visitIfStmt(ifStmt: IfStmt): ResultT { + ifStmt.cond.accept(this); + ifStmt.then.accept(this); + ifStmt.els?.accept(this); + return null as any; + } + + public visitWhileStmt(whileStmt: WhileStmt): ResultT { + whileStmt.cond.accept(this); + whileStmt.body.accept(this); + return null as any; + } + + public visitRaiseStmt(raiseStmt: RaiseStmt): ResultT { + raiseStmt.value.accept(this); + return null as any; + } + + public visitVarDecl(varDecl: VarDecl): ResultT { + varDecl.typeName?.accept(this); + varDecl.value.accept(this); + return null as any; + } + + public visitFunDecl(funDecl: FunDecl): ResultT { + funDecl.returnTypeName?.accept(this); + funDecl.params.forEach((param) => param.accept(this)); + funDecl.body.accept(this); + return null as any; + } + + public visitFunParam(funParam: FunParam): ResultT { + funParam.typeName.accept(this); + return null as any; + } + + public visitTraitDecl(traitDecl: TraitDecl): ResultT { + traitDecl.typeParams.forEach((typeParam) => typeParam.accept(this)); + traitDecl.attrs.forEach((attr) => attr.accept(this)); + return null as any; + } + + public visitTraitAttr(traitAttr: TraitAttr): ResultT { + traitAttr.params.forEach((param) => param.accept(this)); + traitAttr.returnTypeName.accept(this); + traitAttr.defaultBody?.accept(this); + return null as any; + } + + public visitTraitTypeParam(traitTypeParam: TraitTypeParam): ResultT { + traitTypeParam.bound?.accept(this); + return null as any; + } + + public visitImplDecl(implDecl: ImplDecl): ResultT { + implDecl.traitTypeArgs.forEach((traitTypeArg) => traitTypeArg.accept(this)); + implDecl.attrs.forEach((attr) => attr.accept(this)); + return null as any; + } + + public visitImplAttr(implAttr: ImplAttr): ResultT { + implAttr.body.accept(this); + return null as any; + } + + public visitStructDecl(structDecl: StructDecl): ResultT { + structDecl.typeParams.forEach((typeParam) => typeParam.accept(this)); + structDecl.fields.forEach((field) => field.accept(this)); + return null as any; + } + + public visitStructTypeParam(structTypeParam: StructTypeParam): ResultT { + structTypeParam.bound?.accept(this); + return null as any; + } + + public visitStructField(structField: StructField): ResultT { + structField.typeName.accept(this); + return null as any; + } + + public visitStructInitializer(structInitializer: StructInitializer): ResultT { + structInitializer.params.forEach((param) => param.accept(this)); + structInitializer.body.accept(this); + return null as any; + } + + public visitIdentifier(identifier: Identifier): ResultT { + return null as any; + } + + public visitIntExpr(intExpr: IntExpr): ResultT { + return null as any; + } + + public visitFloatExpr(floatExpr: FloatExpr): ResultT { + return null as any; + } + + public visitStringExpr(stringExpr: StringExpr): ResultT { + return null as any; + } + + public visitBinExpr(binExpr: BinExpr): ResultT { + binExpr.lhs.accept(this); + binExpr.rhs.accept(this); + return null as any; + } + + public visitCallExpr(callExpr: CallExpr): ResultT { + callExpr.callee.accept(this); + callExpr.args.forEach((arg) => arg.accept(this)); + return null as any; + } + + public visitCondExpr(condExpr: CondExpr): ResultT { + condExpr.cond.accept(this); + condExpr.then.accept(this); + condExpr.els.accept(this); + return null as any; + } + + public visitLambdaExpr(lambdaExpr: LambdaExpr): ResultT { + lambdaExpr.params.forEach((param) => param.accept(this)); + lambdaExpr.body.accept(this); + return null as any; + } + + public visitAssignExpr(assignExpr: AssignExpr): ResultT { + assignExpr.lval.accept(this); + assignExpr.rval.accept(this); + return null as any; + } + + public visitNamedTypeName(namedTypeName: NamedTypeName): ResultT { + return null as any; + } + + public visitGenericTypeName(genericTypeName: GenericTypeName): ResultT { + genericTypeName.args.forEach((arg) => arg.accept(this)); + return null as any; + } + + public visitNullableTypeName(nullableTypeName: NullableTypeName): ResultT { + nullableTypeName.typeName.accept(this); + return null as any; + } + + public visitArrayTypeName(arrayTypeName: ArrayTypeName): ResultT { + arrayTypeName.typeName.accept(this); + return null as any; + } + + public visitTupleTypeName(tupleTypeName: TupleTypeName): ResultT { + tupleTypeName.typeNames.forEach((typeName) => typeName.accept(this)); + return null as any; + } + + public visitLambdaTypeName(lambdaTypeName: LambdaTypeName): ResultT { + lambdaTypeName.params.forEach((param) => param[1].accept(this)); + lambdaTypeName.returnTypeName.accept(this); + return null as any; + } +} + diff --git a/src/frontend/semantics/symbol.ts b/src/frontend/semantics/symbol.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/frontend/semantics/type.ts b/src/frontend/semantics/type.ts new file mode 100644 index 0000000..3c83ded --- /dev/null +++ b/src/frontend/semantics/type.ts @@ -0,0 +1,45 @@ +export abstract class Type { + +} + +export class OpaqueType extends Type { + constructor() { super(); } +} + +export class StructType extends Type { + constructor( + public name: string, + public fields: [string, Type][], + ) { super(); } + + public hasField(field: string) { + for (const [name] of this.fields) { + if (name === field) return true; + } + + return false; + } + + public getFieldType(field: string) { + for (const [name, type] of this.fields) { + if (name === field) return type; + } + + throw new Error(`StructType.getFieldType(...): field '${field}' not found'`); + } + + public getFieldOffset(field: string) { + for (let i = 0; i < this.fields.length; i++) { + if (this.fields[i][0] === field) return i; + } + + throw new Error(`StructType.getFieldOffset(...): field '${field}' not found'`); + } +} + +export class TraitType extends Type { + constructor( + public name: string, + public args: Type[], + ) { super(); } +} diff --git a/src/io/files.ts b/src/io/files.ts new file mode 100644 index 0000000..f7c4d61 --- /dev/null +++ b/src/io/files.ts @@ -0,0 +1,20 @@ +import fs from 'fs'; +import path from 'path'; + +const files: Record = {}; + +type File = { + path: string; + content: string; +}; + +export function getFile(filePath: string): File { + const abspath = path.normalize(path.resolve(filePath)); + + if (files[abspath]) return { path: abspath, content: files[abspath] }; + + const content = fs.readFileSync(abspath, 'utf8'); + files[abspath] = content; + + return { path: abspath, content }; +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..3ada3b0 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,8 @@ +import { inspect } from "bun"; +import { parseModule } from "./parser"; +import { printErrors } from "./errors"; + +const module = parseModule('examples/test.plsm'); +console.log(inspect(module, { depth: 1000, colors: false })); + +printErrors(); \ No newline at end of file diff --git a/src/parser.ts b/src/parser.ts new file mode 100644 index 0000000..7917bcc --- /dev/null +++ b/src/parser.ts @@ -0,0 +1,33 @@ +import plsmLexer from "~/antlr/plsmLexer"; +import plsmParser from "~/antlr/plsmParser"; +import AST from "./frontend/ast/index"; +import { CharStream, CommonToken, CommonTokenStream, ErrorListener, InputStream, RecognitionException, Recognizer, TokenStream } from 'antlr4'; +import { getFile } from "./io/files"; +import { SourceRange } from "./frontend/ast/node"; +import { raiseError } from "./errors"; + +class MyErrorListener extends ErrorListener { + constructor(private path: string) { + super(); + } + + syntaxError(recognizer: Recognizer, offendingSymbol: any, line: number, column: number, msg: string, e: RecognitionException | undefined): void { + const sourceRange = new SourceRange(this.path, [line, column], [line, column + offendingSymbol.text.length]); + raiseError(sourceRange, msg); + } +} + +export function parseModule(moduleFilePath: string): AST.Module { + const { content, path } = getFile(moduleFilePath); + + const charStream = new CharStream(content); + const lexer = new plsmLexer(charStream); + const tokenStream = new CommonTokenStream(lexer); + const parser = new plsmParser(tokenStream); + parser.setFileName(path); + + parser.removeErrorListeners(); + parser.addErrorListener(new MyErrorListener(path)); + + return parser.module_().ast; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c453b49 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + } + } +}