diff --git a/bun.lock b/bun.lock index f0b5dde..80170b4 100644 --- a/bun.lock +++ b/bun.lock @@ -1,12 +1,13 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "dependencies": { "cmake-js": "^7.3.1", }, "devDependencies": { - "@types/bun": "^1.2.19", + "@types/bun": "^1.3.13", }, }, }, @@ -14,12 +15,10 @@ "cmake-js": "7.3.1", }, "packages": { - "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], + "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], - "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -32,7 +31,7 @@ "axios": ["axios@1.11.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA=="], - "bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], + "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -52,8 +51,6 @@ "console-control-strings": ["console-control-strings@1.1.0", "", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="], - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], diff --git a/index.ts b/index.ts index fb9369d..853cff2 100644 --- a/index.ts +++ b/index.ts @@ -2,7 +2,9 @@ import { rmSync } from "fs" import { Lexer } from "./src/lexer" import { Parser } from "./src/parser" import { SemanticAnalyzer } from "./src/semantic/analysis" -import { TypeChecker } from "./src/semantic/typing" +import { TypeChecker } from "./src/semantic/checker" +import { Codegen } from "./src/gen" +import { TypeInferrer } from "./src/semantic/inferrer" rmSync("./out", { recursive: true, force: true }) // error driven development right here @@ -39,8 +41,12 @@ rmSync("./out", { recursive: true, force: true }) const program = ` fn main() { - let x := true - let y := 3 + x + return 0 +} + +fn hello() { + let b: int= 3 + return 2 } `.trim() @@ -51,7 +57,7 @@ const t = l.lex() const lexerTime = performance.now() await Bun.write("out/tok.json", JSON.stringify(t, null, 2)) -console.log(`lexed ${t.length} tokens in ${(lexerTime - start).toFixed(3)}ms`) +console.log(`lexed ${program.length} characters in ${(lexerTime - start).toFixed(3)}ms into ${t.length} tokens`) const p = new Parser(t) const ast = p.parse() @@ -63,12 +69,40 @@ console.log(`parsed ${t.length} tokens in ${(astTime - lexerTime).toFixed(3)}ms, const sa = new SemanticAnalyzer(ast) const analyzed = sa.analyze() -const tc = new TypeChecker(ast) -tc.check() +const ti = new TypeInferrer(analyzed) +const inferred = ti.infer() + +const tc = new TypeChecker(inferred) +const checked = tc.check() -await Bun.write("out/semantic.json", JSON.stringify(analyzed, null, 2)) +await Bun.write("out/semantic.json", JSON.stringify(checked, null, 2)) const semanticTime = performance.now() console.log(`semantic analysis done in ${(semanticTime - astTime).toFixed(3)}ms`) -console.log(`total time: ${(semanticTime - start).toFixed(3)}ms`) \ No newline at end of file +const cg = new Codegen("my_module", ast) +const llvmIr = cg.generate() +await Bun.write("out/module.ll", llvmIr) + +console.log("") + +Bun.write("out/module.ll", llvmIr) +const clang = Bun.spawn({ + cmd: ["clang", "-o", "out/program.exe", "out/module.ll"], +}) +await clang.exited + +const cgTime = performance.now() + + +console.log(`codegen done in ${(cgTime - semanticTime).toFixed(3)}ms`) +console.log(`total time: ${(cgTime - start).toFixed(3)}ms`) +console.log("") + +const exe = Bun.spawn({ + cmd: ["out/program.exe"], +}) + +await exe.exited + +console.log(`program exited with code ${exe.exitCode}`) \ No newline at end of file diff --git a/package.json b/package.json index 3c55639..9fda987 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "devDependencies": { - "@types/bun": "^1.2.19" + "@types/bun": "^1.3.13" }, "dependencies": { "cmake-js": "^7.3.1" diff --git a/src/bindings/ffi.ts b/src/bindings/ffi.ts index 5e97e3a..07d2336 100644 --- a/src/bindings/ffi.ts +++ b/src/bindings/ffi.ts @@ -4,8 +4,8 @@ import { platform } from "os"; const location = platform() === "win32" ? "LLVM-C.dll" : "libLLVM-C.so"; const lib = dlopen(location, { - LLVMConstStringInContext: { args: ["ptr", "cstring", "uint32_t", "bool"], returns: "ptr" }, - LLVMArrayType: { args: ["ptr", "uint32_t"], returns: "ptr" }, + LLVMConstStringInContext2: { args: ["ptr", "cstring", "uint64_t", "bool"], returns: "ptr" }, + LLVMArrayType2: { args: ["ptr", "uint64_t"], returns: "ptr" }, LLVMAddGlobal: { args: ["ptr", "ptr", "cstring"], returns: "ptr" }, LLVMSetInitializer: { args: ["ptr", "ptr"], returns: "void" }, LLVMSetGlobalConstant: { args: ["ptr", "bool"], returns: "void" }, @@ -31,7 +31,7 @@ const lib = dlopen(location, { LLVMDoubleTypeInContext: { args: ["ptr"], returns: "ptr" }, LLVMVoidTypeInContext: { args: ["ptr"], returns: "ptr" }, - LLVMPointerType: { args: ["ptr", "uint32_t"], returns: "ptr" }, + LLVMPointerTypeInContext: { args: ["ptr", "uint32_t"], returns: "ptr" }, // functions & blocks LLVMFunctionType: { args: ["ptr", "ptr", "uint32_t", "bool"], returns: "ptr" }, @@ -59,7 +59,7 @@ const lib = dlopen(location, { // memory LLVMBuildAlloca: { args: ["ptr", "ptr", "cstring"], returns: "ptr" }, LLVMBuildStore: { args: ["ptr", "ptr", "ptr"], returns: "ptr" }, - LLVMBuildLoad: { args: ["ptr", "ptr", "cstring"], returns: "ptr" }, + LLVMBuildLoad2: { args: ["ptr", "ptr", "ptr", "cstring"], returns: "ptr" }, // constants LLVMConstInt: { args: ["ptr", "uint64_t", "bool"], returns: "ptr" }, @@ -90,7 +90,7 @@ export const { LLVMFloatTypeInContext, LLVMDoubleTypeInContext, LLVMVoidTypeInContext, - LLVMPointerType, + LLVMPointerTypeInContext, LLVMFunctionType, LLVMAddFunction, @@ -123,13 +123,13 @@ export const { LLVMGetIntTypeWidth, LLVMBuildAlloca, LLVMBuildStore, - LLVMBuildLoad, + LLVMBuildLoad2, LLVMGetNamedFunction, LLVMBuildCall2, LLVMBuildICmp, LLVMGetInsertBlock, - LLVMConstStringInContext, - LLVMArrayType, + LLVMConstStringInContext2, + LLVMArrayType2, LLVMAddGlobal, LLVMSetInitializer, LLVMSetGlobalConstant, diff --git a/src/bindings/index.ts b/src/bindings/index.ts index 2327659..a57e459 100644 --- a/src/bindings/index.ts +++ b/src/bindings/index.ts @@ -2,7 +2,7 @@ import { LLVMAddFunction, LLVMAddGlobal, LLVMAppendBasicBlockInContext, - LLVMArrayType, + LLVMArrayType2, LLVMBuildAdd, LLVMBuildAlloca, LLVMBuildBitCast, @@ -14,7 +14,7 @@ import { LLVMBuildFMul, LLVMBuildFSub, LLVMBuildICmp, - LLVMBuildLoad, + LLVMBuildLoad2, LLVMBuildMul, LLVMBuildRet, LLVMBuildSDiv, @@ -23,7 +23,7 @@ import { LLVMBuildUDiv, LLVMConstInt, LLVMConstReal, - LLVMConstStringInContext, + LLVMConstStringInContext2, LLVMContextCreate, LLVMCreateBuilderInContext, LLVMDeleteBasicBlock, @@ -40,7 +40,7 @@ import { LLVMInt64TypeInContext, LLVMInt8TypeInContext, LLVMModuleCreateWithNameInContext, - LLVMPointerType, + LLVMPointerTypeInContext, LLVMPositionBuilderAtEnd, LLVMPrintModuleToString, LLVMSetGlobalConstant, @@ -96,12 +96,12 @@ private _funcs: Map = new Map(); const ctx = this.getContext(); const i8 = Type.int8(ctx); const strBytes = Buffer.from(value + "\0"); - const strConst = LLVMConstStringInContext(ctx.handle, strBytes, strBytes.length, 1); - const arrType = LLVMArrayType(i8.handle, strBytes.length); + const strConst = LLVMConstStringInContext2(ctx.handle, strBytes, BigInt(strBytes.length), 1); + const arrType = LLVMArrayType2(i8.handle, BigInt(strBytes.length)); const global = LLVMAddGlobal(this.ptr, arrType, Buffer.from(name + "\0")); LLVMSetInitializer(global, strConst); LLVMSetGlobalConstant(global, true); - return new Value(global, Type.pointer(i8)); + return new Value(global, Type.pointer(ctx)); } /** @@ -437,12 +437,13 @@ export class IRBuilder { /** load a value from memory + @param type the type of the value to load @param ptr the pointer to load from @param name variable name @returns the loaded value */ - load(ptr: Value, name = "load"): Value { - return new Value(LLVMBuildLoad(this.ptr, ptr.handle, Buffer.from(name + "\0"))); + load(type: Type, ptr: Value, name = "load"): Value { + return new Value(LLVMBuildLoad2(this.ptr, type.handle, ptr.handle, Buffer.from(name + "\0")), type); } /** @@ -691,12 +692,12 @@ export class Type { } /** - get pointer type - @param elementType type to point to + get pointer type (opaque pointer in LLVM 21) + @param context LLVM context @param addressSpace address space */ - static pointer(elementType: Type, addressSpace = 0): Type { - return new Type(LLVMPointerType(elementType.handle, addressSpace), "pointer", undefined, elementType); + static pointer(context: Context, addressSpace = 0): Type { + return new Type(LLVMPointerTypeInContext(context.handle, addressSpace), "pointer"); } /** diff --git a/src/gen/helper.ts b/src/gen/helper.ts new file mode 100644 index 0000000..1178619 --- /dev/null +++ b/src/gen/helper.ts @@ -0,0 +1,77 @@ +import VM, { Context, Func, FunctionType, IRBuilder, Linkage, Module, Type, Value } from "../bindings"; + +export class LLVMHelper { + public name: string; + + public ctx: Context; + public mod: Module; + public builder: IRBuilder; + + // currentFunction: Func | null = null; + + constructor(name: string) { + this.name = name + + this.ctx = new VM.Context(); + this.mod = new VM.Module(name, this.ctx); + this.builder = new VM.IRBuilder(this.ctx); + } + + fn(name: string, fnType: FunctionType, opts?: { + linkage?: Linkage; + extern?: boolean; + }): Func { + let func = this.mod.getFunction(name); + if (func) return func; + func = this.mod.createFunction(name, fnType, opts ?? { linkage: Linkage.External, extern: false }); + if (!opts?.extern) this.builder.insertInto(func.addBlock("entry")); + + // this.currentFunction = func; + return func; + } + + ret(value?: Value) { + // if (!this.currentFunction) { + // throw new Error("No current function to return from"); + // } + + this.builder.ret(value); + } + + public add(left: Value, right: Value): Value { + return this.builder.add(left, right); + } + + public sub(left: Value, right: Value): Value { + return this.builder.sub(left, right); + } + + public mul(left: Value, right: Value): Value { + return this.builder.mul(left, right); + } + + public div(left: Value, right: Value): Value { + if (left.getType().isFloat() || right.getType().isFloat()) { + return this.builder.fdiv(left, right); + } + + return this.builder.sdiv(left, right); + } + + public alloca(type: Type, name?: string): Value { + return this.builder.alloca(type, name); + } + + public store(value: Value, ptr: Value): void { + this.builder.store(value, ptr); + } + + public load(type: Type, ptr: Value, name?: string): Value { + return this.builder.load(type, ptr, name); + } + + toString() { + this.mod.verify() + return this.mod.toString(); + } +} \ No newline at end of file diff --git a/src/gen/index.ts b/src/gen/index.ts new file mode 100644 index 0000000..db2613e --- /dev/null +++ b/src/gen/index.ts @@ -0,0 +1,130 @@ +import { Func, FunctionType, Module, Type, Value } from "../bindings"; +import { BinaryExpression, Expression, ExpressionType, FunctionDeclaration, Identifier, NumberLiteral, ProgramExpression, ReturnExpression, VariableDeclaration } from "../parser/ast"; +import { LLVMHelper } from "./helper"; + +export class Codegen { + private h: LLVMHelper; + private ast: ProgramExpression; + + private table: { [key in ExpressionType]?: (e: any) => Func | Value | void } = { + FunctionDeclaration: this.genFunctionDeclaration.bind(this), + ReturnExpression: this.genReturnExpression.bind(this), + BinaryExpression: this.genBinaryExpression.bind(this), + VariableDeclaration: this.genVariableDeclaration.bind(this) + }; + + private variables: { [name: string]: { ptr: Value; type: Type; value?: Value } } = {}; + + constructor(moduleName: string, ast: ProgramExpression) { + this.h = new LLVMHelper(moduleName); + this.ast = ast; + } + + public generate(): string { + for (const expr of this.ast.body) { + this.table[expr.type]?.(expr); + } + + return this.h.toString(); + } + + private genFunctionDeclaration(f: FunctionDeclaration): Func { + const fn = this.h.fn( + f.name.value, + new FunctionType( + f.params.map(_ => Type.int32(this.h.ctx)), + Type.int32(this.h.ctx) + ) + ); + + for (const expr of f.body) { + this.table[expr.type]?.(expr); + } + + return fn; + } + + private genReturnExpression(r: ReturnExpression): void { + const v = this.getValueFromExpression(r.value); + if (!v) { + throw new Error(`Unsupported return expression type: ${r.value.type}`); + } + + this.h.ret( + v + ) + } + + private genBinaryExpression(b: BinaryExpression): Value | void { + const left = this.getValueFromExpression(b.left); + const right = this.getValueFromExpression(b.right); + + if (!left || !right) { + throw new Error(`Unsupported binary expression operands: ${b.left.type}, ${b.right.type}`); + } + + let val: Value = (() => { + switch (b.operator) { + case "+": + return this.h.add(left, right); + case "-": + return this.h.sub(left, right); + case "*": + return this.h.mul(left, right); + case "/": + return this.h.div(left, right); + default: + throw new Error(`Unsupported binary operator: ${b.operator}`); + } + })(); + + return val; + } + + private genVariableDeclaration(v: VariableDeclaration): void { + const val = this.getValueFromExpression(v.value); + if (!val) { + throw new Error(`Unsupported variable declaration value type: ${v.value.type}`); + } + + const ptr = this.h.alloca(val.getType(), v.name.value) + this.h.store(val, ptr); + this.variables[v.name.value] = { ptr, type: val.getType(), value: val }; + } + + private getTypeFromExpression(e: Expression): Type | null { + switch (e.type) { + case "NumberLiteral": + return Type.int32(this.h.ctx); + } + + return null; + } + + private getValueFromExpression(expr: Expression): Value | null { + switch (expr.type) { + case "NumberLiteral": { + const e = expr as NumberLiteral; + return Value.constInt(Type.int32(this.h.ctx), e.value); + } + + case "BinaryExpression": { + const e = expr as BinaryExpression; + return this.genBinaryExpression(e) as Value; + } + + case "Identifier": { + const e = expr as Identifier; + const obj = this.variables[e.value]; + if (!obj) { + throw new Error(`Undefined variable: ${e.value}`); + } + // if cached + if (obj.value) return obj.value; + return this.h.load(obj.type, obj.ptr, e.value); + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/parser/ast.ts b/src/parser/ast.ts index fdaa463..622d0ab 100644 --- a/src/parser/ast.ts +++ b/src/parser/ast.ts @@ -1,4 +1,6 @@ +import { Type } from "../semantic/types" // oops import { Modifier } from "../lexer/token" +import { PlaceholderType } from "../semantic/types" //import { CheckerPlaceholder, CheckerType } from "../typing/types" export type Expression = { @@ -66,7 +68,7 @@ export type FunctionDeclaration = Expression & { name: Identifier params: FunctionParam[] returnType?: Identifier - // resolvedReturnType?: CheckerType | CheckerPlaceholder + resolvedReturnType?: Type | PlaceholderType body: Expression[] modifiers: Modifier[] } @@ -74,20 +76,20 @@ export type FunctionDeclaration = Expression & { export type FunctionCall = Expression & { type: "FunctionCall" callee: Identifier// | MemberAccess - // inferredCallee?: CheckerType | CheckerPlaceholder + inferredCallee?: Type | PlaceholderType args: Expression[] } export type FunctionParam = { type: "FunctionParam" name: Identifier - paramType: Identifier + paramType: Identifier // declared type annotation } export type IfExpression = Expression & { type: "IfExpression" condition: Expression - // inferredCondition?: CheckerType | CheckerPlaceholder + inferredCondition?: Type | PlaceholderType body: Expression[] alternate?: IfExpression | ElseExpression } @@ -100,21 +102,21 @@ export type ElseExpression = Expression & { export type WhileExpression = Expression & { type: "WhileExpression" condition: Expression - // inferredCondition?: CheckerType | CheckerPlaceholder + inferredCondition?: Type | PlaceholderType body: Expression[] } export type SwitchExpression = Expression & { type: "SwitchExpression" value: Expression - // inferredValue?: CheckerType | CheckerPlaceholder + inferredValue?: Type | PlaceholderType cases: CaseExpression[] } export type CaseExpression = Expression & { type: "CaseExpression" value: Expression | "default" - // inferredValue?: CheckerType | CheckerPlaceholder + inferredValue?: Type | PlaceholderType body: Expression[] } @@ -128,7 +130,7 @@ export type VariableDeclaration = Expression & { name: Identifier value: Expression typeAnnotation?: Identifier - // resolvedType?: CheckerType | CheckerPlaceholder + resolvedType?: Type | PlaceholderType mutable: boolean } diff --git a/src/semantic/analysis.ts b/src/semantic/analysis.ts index 2df675d..639264e 100644 --- a/src/semantic/analysis.ts +++ b/src/semantic/analysis.ts @@ -47,6 +47,15 @@ export class SemanticAnalyzer { if (this.symbols.has(name)) { throw new Error(`function ${name} is already declared`) } + + for (const param of f.params) { + const name = param.name.value + if (this.symbols.has(name)) { + throw new Error(`parameter ${name} is already declared in function scope ${f.name.value}`) + } + + this.symbols.set(name, { type: "variable", name: param.name, value: param }) + } this.symbols.set(name, symbol) for (const expr of f.body) { diff --git a/src/semantic/checker.ts b/src/semantic/checker.ts new file mode 100644 index 0000000..389a273 --- /dev/null +++ b/src/semantic/checker.ts @@ -0,0 +1,170 @@ +import { BinaryExpression, Expression, ExpressionType, FunctionDeclaration, Identifier, NumberLiteral, ProgramExpression, ReturnExpression, VariableDeclaration } from "../parser/ast" +import { newPlaceholderType, similarType, Type } from "./types" + +export class TypeEnvironment { + private types: Map = new Map() + private parent?: TypeEnvironment + + constructor(parent?: TypeEnvironment) { + this.parent = parent + + if (!parent) { + this.types.set("int", { kind: "int" }) + this.types.set("float", { kind: "float" }) + this.types.set("bool", { kind: "bool" }) + this.types.set("string", { kind: "string" }) + } + } + + define(name: string, type: Type) { + const existing = this.types.get(name) + if (existing) { + if (existing.kind === "placeholder") { + this.types.set(name, type) + return + } + + if (!similarType(existing, type)) { + throw new Error(`type mismatch for ${name}: ${existing.kind} vs ${type.kind}`) + } + + throw new Error(`name ${name} with type ${existing.kind} already defined`) + } + + this.types.set(name, type) + } + + get(name: string): Type | undefined { + return this.types.get(name) ?? this.parent?.get(name) + } + + createChild(): TypeEnvironment { + return new TypeEnvironment(this) + } + + restoreParent(): TypeEnvironment { + if (!this.parent) throw new Error("no parent environment to restore to"); + return this.parent + } +} + +export class TypeChecker { + private env: TypeEnvironment + private ast: ProgramExpression + + private table: { [key in ExpressionType]?: (expr: Expression) => Type | undefined } = { + "FunctionDeclaration": (expr) => this.checkFunctionDeclaration(expr as FunctionDeclaration), + + "Identifier": (expr) => this.checkIdentifier(expr as Identifier), + + "BinaryExpression": (expr) => this.checkBinaryExpression(expr as BinaryExpression), + "VariableDeclaration": (expr) => this.checkVariableDeclaration(expr as VariableDeclaration), + "ReturnExpression": (expr) => this.checkReturnExpression(expr as ReturnExpression), + + "NumberLiteral": (expr) => { return Number.isInteger((expr as NumberLiteral).value) ? { kind: "int" } : { kind: "float" } }, + "BooleanLiteral": (expr) => { return { kind: "bool" } }, + } + + constructor(ast: ProgramExpression, env?: TypeEnvironment) { + this.ast = ast + this.env = env ?? new TypeEnvironment() + } + + check() { + for (const expr of this.ast.body) { + this.checkExpression(expr) + } + + return this.ast + } + + private checkExpression(expr: Expression): Type | undefined { + const fn = this.table[expr.type] + if (!fn) throw new Error(`no type rule for ${expr.type}`) + return fn(expr) + } + + private checkFunctionDeclaration(f: FunctionDeclaration) { + this.env = this.env.createChild(); + + for (const param of f.params) { + + if (!param.paramType) this.env.define(param.name.value, newPlaceholderType()) + else { + const paramType = this.env.get(param.paramType.value) + + if (!paramType) this.env.define(param.name.value, newPlaceholderType()) + else this.env.define(param.name.value, paramType) + } + } + + for (const expr of f.body) { + this.checkExpression(expr) + } + + this.env = this.env.restoreParent(); + return undefined; + } + + private checkIdentifier(id: Identifier): Type { + const t = this.env.get(id.value) + if (!t) throw new Error(`undefined symbol: ${id.value}`) + return t + } + + private checkBinaryExpression(b: BinaryExpression): Type { + const leftType = this.checkExpression(b.left) + const rightType = this.checkExpression(b.right) + + if (!leftType) { + throw new Error("Left side of binary expression has undefined type") + } + if (!rightType) { + throw new Error("Right side of binary expression has undefined type") + } + + if (!similarType(leftType, rightType)) { + throw new Error( + `type mismatch in binary expression: ${leftType.kind} vs ${rightType.kind}` + ) + } + + return leftType + } + + private checkVariableDeclaration(v: VariableDeclaration): Type | undefined { + const name = v.name.value; + if (this.env.get(name)) throw new Error(`variable ${name} already declared`); + + let finalType: Type | undefined; + + if (v.typeAnnotation) { + const annotatedType = this.env.get(v.typeAnnotation.value); + if (!annotatedType) throw new Error(`unknown type annotation: ${v.typeAnnotation.value}`); + + const rhsType = this.checkExpression(v.value); + if (!rhsType) throw new Error(`undefined type for variable: ${name}`); + + if (!similarType(annotatedType, rhsType)) { + throw new Error( + `type mismatch for '${name}': annotated ${annotatedType.kind} vs inferred ${rhsType.kind}` + ); + } + + finalType = annotatedType; + } else { + finalType = this.checkExpression(v.value); + if (!finalType) throw new Error(`undefined type for variable: ${name}`); + } + + this.env.define(name, finalType); + return finalType; + } + + private checkReturnExpression(r: ReturnExpression): Type | undefined { + if (r.value) { + return this.checkExpression(r.value) + } + return undefined + } +} \ No newline at end of file diff --git a/src/semantic/inferrer.ts b/src/semantic/inferrer.ts new file mode 100644 index 0000000..275db91 --- /dev/null +++ b/src/semantic/inferrer.ts @@ -0,0 +1,85 @@ +import { ProgramExpression, Expression, VariableDeclaration, NumberLiteral, BooleanLiteral, BinaryExpression, Identifier, FunctionDeclaration, ReturnExpression } from "../parser/ast"; +import { Type, TypeKind } from "./types"; + +export class TypeInferrer { + private ast: ProgramExpression; + private env: Map = new Map(); + + constructor(ast: ProgramExpression) { + this.ast = ast; + } + + public infer(): ProgramExpression { + for (const node of this.ast.body) { + this.inferNode(node); + } + return this.ast; + } + + private inferNode(node: Expression): Type | undefined { + switch (node.type) { + case "NumberLiteral": return this.visitNumberLiteral(node as NumberLiteral); + case "BooleanLiteral": return this.visitBooleanLiteral(node as BooleanLiteral); + case "BinaryExpression": return this.visitBinaryExpression(node as BinaryExpression); + case "VariableDeclaration": return this.visitVariableDeclaration(node as VariableDeclaration); + case "Identifier": return this.visitIdentifier(node as Identifier); + case "FunctionDeclaration": return this.visitFunctionDeclaration(node as FunctionDeclaration); + case "ReturnExpression": return this.visitReturnExpression(node as ReturnExpression); + default: return undefined; + } + } + + private visitNumberLiteral(n: NumberLiteral): Type { + return Number.isInteger(n.value) ? { kind: "int" } : { kind: "float" }; + } + + private visitBooleanLiteral(b: BooleanLiteral): Type { + return { kind: "bool" }; + } + + private visitBinaryExpression(b: BinaryExpression): Type | undefined { + const L = this.inferNode(b.left); + const R = this.inferNode(b.right); + if (!L || !R) return undefined; + // TODO: more complex promotion + if (L.kind === "float" || R.kind === "float") return { kind: "float" }; + return { kind: "int" }; + } + + private visitIdentifier(id: Identifier): Type | undefined { + return this.env.get(id.value); + } + + private visitVariableDeclaration(v: VariableDeclaration): Type | undefined { + const t = this.inferNode(v.value); + if (t) { + v.resolvedType = t; + this.env.set(v.name.value, t); + } + return t; + } + + private visitFunctionDeclaration(f: FunctionDeclaration): Type | undefined { + const oldEnv = new Map(this.env); + for (const p of f.params) { + if (p.paramType) { + this.env.set(p.name.value, { kind: (p.paramType.value as TypeKind) }); // simplistic: expects 'int'|'float' etc + } + } + for (const stmt of f.body) this.inferNode(stmt); + + for (const stmt of f.body) { + if (stmt.type === "ReturnExpression") { + const ret = this.inferNode(stmt as ReturnExpression); + if (ret) f.resolvedReturnType = ret; + break; + } + } + this.env = oldEnv; + return f.resolvedReturnType; + } + + private visitReturnExpression(r: ReturnExpression): Type | undefined { + return this.inferNode(r.value); + } +} \ No newline at end of file diff --git a/src/semantic/typing/types.ts b/src/semantic/types.ts similarity index 76% rename from src/semantic/typing/types.ts rename to src/semantic/types.ts index b9aa893..8ecef9e 100644 --- a/src/semantic/typing/types.ts +++ b/src/semantic/types.ts @@ -3,6 +3,7 @@ export type TypeKind = | "float" | "bool" | "string" + | "placeholder" export type Type = { kind: TypeKind @@ -25,6 +26,16 @@ export type StringType = Type & { value: string } +export type PlaceholderType = Type & { + kind: "placeholder" + id: string +} + +let pid = 0 +export const newPlaceholderType = (): PlaceholderType => { + return { kind: "placeholder", id: `${pid++}` } +} + export const sameType = (a?: Type, b?: Type): boolean => { if (!a || !b) return false; @@ -37,6 +48,8 @@ export const similarType = (a?: Type, b?: Type): boolean => { const groups: TypeKind[][] = [ ["int", "float"], + ["string"], + ["bool"] ]; for (const group of groups) { diff --git a/src/semantic/typing/index.ts b/src/semantic/typing/index.ts deleted file mode 100644 index bc35c6c..0000000 --- a/src/semantic/typing/index.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { BinaryExpression, Expression, ExpressionType, FunctionDeclaration, Identifier, NumberLiteral, ProgramExpression, VariableDeclaration } from "../../parser/ast" -import { similarType, Type } from "./types" - -export class TypeEnvironment { - private types: Map = new Map() - private parent?: TypeEnvironment - - constructor(parent?: TypeEnvironment) { - this.parent = parent - - if (!parent) { - this.types.set("int", { kind: "int" }) - this.types.set("float", { kind: "float" }) - this.types.set("bool", { kind: "bool" }) - this.types.set("string", { kind: "string" }) - } - } - - define(name: string, type: Type) { - if (this.types.has(name)) { - throw new Error(`type ${name} already defined`) - } - this.types.set(name, type) - } - - get(name: string): Type | undefined { - return this.types.get(name) ?? this.parent?.get(name) - } - - createChild(): TypeEnvironment { - return new TypeEnvironment(this) - } - - restoreParent(): TypeEnvironment { - if (!this.parent) throw new Error("no parent environment to restore to"); - return this.parent - } -} - -export class TypeChecker { - private env: TypeEnvironment - private ast: ProgramExpression - - private table: { [key in ExpressionType]?: (expr: Expression) => Type | undefined } = { - "FunctionDeclaration": (expr) => this.visitFunctionDeclaration(expr as FunctionDeclaration), - - "Identifier": (expr) => this.visitIdentifier(expr as Identifier), - - "BinaryExpression": (expr) => this.visitBinaryExpression(expr as BinaryExpression), - "VariableDeclaration": (expr) => this.visitVariableDeclaration(expr as VariableDeclaration), - - "NumberLiteral": (expr) => { return Number.isInteger((expr as NumberLiteral).value) ? { kind: "int" } : { kind: "float" } }, - "BooleanLiteral": (expr) => { return { kind: "bool" } }, - } - - constructor(ast: ProgramExpression, env?: TypeEnvironment) { - this.ast = ast - this.env = env ?? new TypeEnvironment() - } - - check() { - for (const expr of this.ast.body) { - this.checkExpression(expr) - } - - return this.ast - } - - private checkExpression(expr: Expression): Type | undefined { - const fn = this.table[expr.type] - if (!fn) throw new Error(`no type rule for ${expr.type}`) - return fn(expr) - } - - private visitFunctionDeclaration(f: FunctionDeclaration) { - this.env = this.env.createChild(); - - for (const expr of f.body) { - this.checkExpression(expr) - } - - const restored = this.env.restoreParent(); - return undefined; - } - - private visitIdentifier(id: Identifier): Type { - const t = this.env.get(id.value) - if (!t) throw new Error(`undefined symbol: ${id.value}`) - return t - } - - private visitBinaryExpression(b: BinaryExpression): Type { - const leftType = this.checkExpression(b.left) - const rightType = this.checkExpression(b.right) - - if (!leftType) { - throw new Error("Left side of binary expression has undefined type") - } - if (!rightType) { - throw new Error("Right side of binary expression has undefined type") - } - - if (!similarType(leftType, rightType)) { - throw new Error( - `type mismatch in binary expression: ${leftType.kind} vs ${rightType.kind}` - ) - } - - return leftType - } - - private visitVariableDeclaration(v: VariableDeclaration): Type | undefined { - const name = v.name.value - const type = this.checkExpression(v.value) - if (!type) throw new Error(`undefined type for variable: ${name}`) - this.env.define(name, type) - return type - } -} \ No newline at end of file diff --git a/tests/__snapshots__/syntax.test.ts.snap b/tests/__snapshots__/syntax.test.ts.snap new file mode 100644 index 0000000..60f7161 --- /dev/null +++ b/tests/__snapshots__/syntax.test.ts.snap @@ -0,0 +1,1395 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots + +exports[`tokens and AST for: 01.yt 1`] = ` +{ + "ast": { + "body": [ + { + "mutable": true, + "name": { + "type": "Identifier", + "value": "a", + }, + "type": "VariableDeclaration", + "value": { + "type": "NumberLiteral", + "value": 5, + }, + }, + { + "mutable": true, + "name": { + "type": "Identifier", + "value": "x", + }, + "type": "VariableDeclaration", + "value": { + "left": { + "type": "Identifier", + "value": "a", + }, + "operator": "*", + "right": { + "type": "NumberLiteral", + "value": 2, + }, + "type": "BinaryExpression", + }, + }, + { + "mutable": true, + "name": { + "type": "Identifier", + "value": "y", + }, + "type": "VariableDeclaration", + "value": { + "left": { + "type": "Identifier", + "value": "x", + }, + "operator": "+", + "right": { + "type": "Identifier", + "value": "a", + }, + "type": "BinaryExpression", + }, + }, + ], + "type": "Program", + }, + "tokens": [ + { + "literal": "let", + "type": "Keyword", + }, + { + "literal": "a", + "type": "Identifier", + }, + { + "literal": ":=", + "type": "Delimiter", + }, + { + "literal": "5", + "type": "Number", + }, + { + "literal": "let", + "type": "Keyword", + }, + { + "literal": "x", + "type": "Identifier", + }, + { + "literal": ":=", + "type": "Delimiter", + }, + { + "literal": "a", + "type": "Identifier", + }, + { + "literal": "*", + "type": "Operator", + }, + { + "literal": "2", + "type": "Number", + }, + { + "literal": "let", + "type": "Keyword", + }, + { + "literal": "y", + "type": "Identifier", + }, + { + "literal": ":=", + "type": "Delimiter", + }, + { + "literal": "x", + "type": "Identifier", + }, + { + "literal": "+", + "type": "Operator", + }, + { + "literal": "a", + "type": "Identifier", + }, + { + "literal": "", + "type": "EOF", + }, + ], +} +`; + +exports[`tokens and AST for: 02.yt 1`] = ` +{ + "ast": { + "body": [ + { + "type": "CommentExpression", + "value": +"yttria + a blazingly fast*, universal* and easy-to-use* programming language + for anything you can imagine* + * = not true (yet) + + this file is a complete (albeit meaningless) program + that demonstrates the syntax of yttria + it is not meant to be run, but rather to be read + it is a work in progress, so expect changes in the future" +, + }, + { + "body": [ + { + "mutable": false, + "name": { + "type": "Identifier", + "value": "a", + }, + "type": "VariableDeclaration", + "value": { + "type": "NumberLiteral", + "value": 1, + }, + }, + { + "mutable": false, + "name": { + "type": "Identifier", + "value": "b", + }, + "type": "VariableDeclaration", + "typeAnnotation": { + "type": "Identifier", + "value": "int", + }, + "value": { + "type": "NumberLiteral", + "value": 2, + }, + }, + { + "mutable": true, + "name": { + "type": "Identifier", + "value": "c", + }, + "type": "VariableDeclaration", + "value": { + "type": "NumberLiteral", + "value": 3, + }, + }, + { + "mutable": true, + "name": { + "type": "Identifier", + "value": "d", + }, + "type": "VariableDeclaration", + "typeAnnotation": { + "type": "Identifier", + "value": "float", + }, + "value": { + "type": "NumberLiteral", + "value": 4.5, + }, + }, + { + "type": "Identifier", + "value": "try", + }, + { + "args": [ + { + "type": "Identifier", + "value": "x", + }, + ], + "callee": { + "type": "Identifier", + "value": "foo", + }, + "type": "FunctionCall", + }, + { + "args": [ + { + "type": "Identifier", + "value": "e", + }, + ], + "callee": { + "type": "Identifier", + "value": "catch", + }, + "type": "FunctionCall", + }, + { + "args": [ + { + "type": "Identifier", + "value": "w", + }, + ], + "callee": { + "type": "Identifier", + "value": "bar", + }, + "type": "FunctionCall", + }, + { + "type": "Identifier", + "value": "finally", + }, + { + "args": [ + { + "type": "Identifier", + "value": "c", + }, + ], + "callee": { + "type": "Identifier", + "value": "quux", + }, + "type": "FunctionCall", + }, + { + "args": [ + { + "type": "Identifier", + "value": "MsgSuccess", + }, + ], + "callee": { + "object": { + "type": "Identifier", + "value": "io", + }, + "property": { + "type": "Identifier", + "value": "println", + }, + "type": "MemberAccess", + }, + "type": "FunctionCall", + }, + { + "mutable": false, + "name": { + "type": "Identifier", + "value": "numbers", + }, + "type": "VariableDeclaration", + "typeAnnotation": { + "type": "Identifier", + "value": "int", + }, + "value": { + "type": "NumberLiteral", + "value": 34, + }, + }, + { + "alternate": { + "alternate": { + "body": [ + { + "args": [ + { + "type": "NumberLiteral", + "value": 3, + }, + ], + "callee": { + "object": { + "type": "Identifier", + "value": "io", + }, + "property": { + "type": "Identifier", + "value": "println", + }, + "type": "MemberAccess", + }, + "type": "FunctionCall", + }, + ], + "type": "ElseExpression", + }, + "body": [ + { + "args": [ + { + "type": "NumberLiteral", + "value": 2, + }, + ], + "callee": { + "object": { + "type": "Identifier", + "value": "io", + }, + "property": { + "type": "Identifier", + "value": "println", + }, + "type": "MemberAccess", + }, + "type": "FunctionCall", + }, + ], + "condition": { + "left": { + "type": "Identifier", + "value": "a", + }, + "operator": ">", + "right": { + "type": "Identifier", + "value": "b", + }, + "type": "BinaryExpression", + }, + "type": "IfExpression", + }, + "body": [ + { + "args": [ + { + "type": "NumberLiteral", + "value": 1, + }, + ], + "callee": { + "object": { + "type": "Identifier", + "value": "io", + }, + "property": { + "type": "Identifier", + "value": "println", + }, + "type": "MemberAccess", + }, + "type": "FunctionCall", + }, + ], + "condition": { + "left": { + "type": "Identifier", + "value": "a", + }, + "operator": "<", + "right": { + "type": "Identifier", + "value": "b", + }, + "type": "BinaryExpression", + }, + "type": "IfExpression", + }, + { + "cases": [ + { + "body": [ + { + "args": [ + { + "object": { + "object": { + "type": "Identifier", + "value": "m", + }, + "property": { + "type": "Identifier", + "value": "x", + }, + "type": "MemberAccess", + }, + "property": { + "type": "Identifier", + "value": "y", + }, + "type": "MemberAccess", + }, + ], + "callee": { + "object": { + "type": "Identifier", + "value": "io", + }, + "property": { + "type": "Identifier", + "value": "println", + }, + "type": "MemberAccess", + }, + "type": "FunctionCall", + }, + ], + "type": "CaseExpression", + "value": { + "type": "NumberLiteral", + "value": 1, + }, + }, + { + "body": [ + { + "args": [ + { + "object": { + "object": { + "type": "Identifier", + "value": "c", + }, + "property": { + "type": "Identifier", + "value": "t", + }, + "type": "MemberAccess", + }, + "property": { + "type": "Identifier", + "value": "p", + }, + "type": "MemberAccess", + }, + ], + "callee": { + "object": { + "type": "Identifier", + "value": "io", + }, + "property": { + "type": "Identifier", + "value": "println", + }, + "type": "MemberAccess", + }, + "type": "FunctionCall", + }, + ], + "type": "CaseExpression", + "value": { + "type": "NumberLiteral", + "value": 2, + }, + }, + { + "body": [ + { + "args": [ + { + "object": { + "object": { + "type": "Identifier", + "value": "f", + }, + "property": { + "type": "Identifier", + "value": "l", + }, + "type": "MemberAccess", + }, + "property": { + "type": "Identifier", + "value": "e", + }, + "type": "MemberAccess", + }, + ], + "callee": { + "object": { + "type": "Identifier", + "value": "io", + }, + "property": { + "type": "Identifier", + "value": "println", + }, + "type": "MemberAccess", + }, + "type": "FunctionCall", + }, + ], + "type": "CaseExpression", + "value": "default", + }, + ], + "type": "SwitchExpression", + "value": { + "type": "Identifier", + "value": "c", + }, + }, + ], + "modifiers": [], + "name": { + "type": "Identifier", + "value": "main", + }, + "params": [], + "returnType": { + "type": "Identifier", + "value": "void", + }, + "type": "FunctionDeclaration", + }, + { + "body": [ + { + "alternate": undefined, + "body": [ + { + "type": "ReturnExpression", + "value": { + "type": "Identifier", + "value": "n", + }, + }, + ], + "condition": { + "left": { + "type": "Identifier", + "value": "n", + }, + "operator": "<=", + "right": { + "type": "NumberLiteral", + "value": 1, + }, + "type": "BinaryExpression", + }, + "type": "IfExpression", + }, + { + "type": "ReturnExpression", + "value": { + "left": { + "args": [ + { + "left": { + "type": "Identifier", + "value": "n", + }, + "operator": "-", + "right": { + "type": "NumberLiteral", + "value": 1, + }, + "type": "BinaryExpression", + }, + ], + "callee": { + "type": "Identifier", + "value": "fib", + }, + "type": "FunctionCall", + }, + "operator": "+", + "right": { + "args": [ + { + "left": { + "type": "Identifier", + "value": "n", + }, + "operator": "-", + "right": { + "type": "NumberLiteral", + "value": 2, + }, + "type": "BinaryExpression", + }, + ], + "callee": { + "type": "Identifier", + "value": "fib", + }, + "type": "FunctionCall", + }, + "type": "BinaryExpression", + }, + }, + ], + "modifiers": [], + "name": { + "type": "Identifier", + "value": "fib", + }, + "params": [ + { + "name": { + "type": "Identifier", + "value": "n", + }, + "paramType": { + "type": "Identifier", + "value": "int", + }, + "type": "FunctionParam", + }, + ], + "returnType": { + "type": "Identifier", + "value": "int", + }, + "type": "FunctionDeclaration", + }, + ], + "type": "Program", + }, + "tokens": [ + { + "literal": +"yttria + a blazingly fast*, universal* and easy-to-use* programming language + for anything you can imagine* + * = not true (yet) + + this file is a complete (albeit meaningless) program + that demonstrates the syntax of yttria + it is not meant to be run, but rather to be read + it is a work in progress, so expect changes in the future" +, + "type": "Comment", + }, + { + "literal": "fn", + "type": "Keyword", + }, + { + "literal": "main", + "type": "Identifier", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "->", + "type": "Operator", + }, + { + "literal": "void", + "type": "Identifier", + }, + { + "literal": "{", + "type": "Delimiter", + }, + { + "literal": "const", + "type": "Keyword", + }, + { + "literal": "a", + "type": "Identifier", + }, + { + "literal": ":=", + "type": "Delimiter", + }, + { + "literal": "1", + "type": "Number", + }, + { + "literal": "const", + "type": "Keyword", + }, + { + "literal": "b", + "type": "Identifier", + }, + { + "literal": ":", + "type": "Delimiter", + }, + { + "literal": "int", + "type": "Identifier", + }, + { + "literal": "=", + "type": "Operator", + }, + { + "literal": "2", + "type": "Number", + }, + { + "literal": "let", + "type": "Keyword", + }, + { + "literal": "c", + "type": "Identifier", + }, + { + "literal": ":=", + "type": "Delimiter", + }, + { + "literal": "3", + "type": "Number", + }, + { + "literal": "let", + "type": "Keyword", + }, + { + "literal": "d", + "type": "Identifier", + }, + { + "literal": ":", + "type": "Delimiter", + }, + { + "literal": "float", + "type": "Identifier", + }, + { + "literal": "=", + "type": "Operator", + }, + { + "literal": "4.5", + "type": "Number", + }, + { + "literal": "try", + "type": "Identifier", + }, + { + "literal": "foo", + "type": "Identifier", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": "x", + "type": "Identifier", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "catch", + "type": "Identifier", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": "e", + "type": "Identifier", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "bar", + "type": "Identifier", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": "w", + "type": "Identifier", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "finally", + "type": "Identifier", + }, + { + "literal": "quux", + "type": "Identifier", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": "c", + "type": "Identifier", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "io", + "type": "Identifier", + }, + { + "literal": ".", + "type": "Delimiter", + }, + { + "literal": "println", + "type": "Identifier", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": "MsgSuccess", + "type": "Identifier", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "const", + "type": "Keyword", + }, + { + "literal": "numbers", + "type": "Identifier", + }, + { + "literal": ":", + "type": "Delimiter", + }, + { + "literal": "int", + "type": "Identifier", + }, + { + "literal": "=", + "type": "Operator", + }, + { + "literal": "34", + "type": "Number", + }, + { + "literal": "if", + "type": "Keyword", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": "a", + "type": "Identifier", + }, + { + "literal": "<", + "type": "Operator", + }, + { + "literal": "b", + "type": "Identifier", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "{", + "type": "Delimiter", + }, + { + "literal": "io", + "type": "Identifier", + }, + { + "literal": ".", + "type": "Delimiter", + }, + { + "literal": "println", + "type": "Identifier", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": "1", + "type": "Number", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "}", + "type": "Delimiter", + }, + { + "literal": "else", + "type": "Keyword", + }, + { + "literal": "if", + "type": "Keyword", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": "a", + "type": "Identifier", + }, + { + "literal": ">", + "type": "Operator", + }, + { + "literal": "b", + "type": "Identifier", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "{", + "type": "Delimiter", + }, + { + "literal": "io", + "type": "Identifier", + }, + { + "literal": ".", + "type": "Delimiter", + }, + { + "literal": "println", + "type": "Identifier", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": "2", + "type": "Number", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "}", + "type": "Delimiter", + }, + { + "literal": "else", + "type": "Keyword", + }, + { + "literal": "{", + "type": "Delimiter", + }, + { + "literal": "io", + "type": "Identifier", + }, + { + "literal": ".", + "type": "Delimiter", + }, + { + "literal": "println", + "type": "Identifier", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": "3", + "type": "Number", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "}", + "type": "Delimiter", + }, + { + "literal": "switch", + "type": "Keyword", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": "c", + "type": "Identifier", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "{", + "type": "Delimiter", + }, + { + "literal": "1", + "type": "Number", + }, + { + "literal": "->", + "type": "Operator", + }, + { + "literal": "{", + "type": "Delimiter", + }, + { + "literal": "io", + "type": "Identifier", + }, + { + "literal": ".", + "type": "Delimiter", + }, + { + "literal": "println", + "type": "Identifier", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": "m", + "type": "Identifier", + }, + { + "literal": ".", + "type": "Delimiter", + }, + { + "literal": "x", + "type": "Identifier", + }, + { + "literal": ".", + "type": "Delimiter", + }, + { + "literal": "y", + "type": "Identifier", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "}", + "type": "Delimiter", + }, + { + "literal": "2", + "type": "Number", + }, + { + "literal": "->", + "type": "Operator", + }, + { + "literal": "{", + "type": "Delimiter", + }, + { + "literal": "io", + "type": "Identifier", + }, + { + "literal": ".", + "type": "Delimiter", + }, + { + "literal": "println", + "type": "Identifier", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": "c", + "type": "Identifier", + }, + { + "literal": ".", + "type": "Delimiter", + }, + { + "literal": "t", + "type": "Identifier", + }, + { + "literal": ".", + "type": "Delimiter", + }, + { + "literal": "p", + "type": "Identifier", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "}", + "type": "Delimiter", + }, + { + "literal": "default", + "type": "Keyword", + }, + { + "literal": "->", + "type": "Operator", + }, + { + "literal": "{", + "type": "Delimiter", + }, + { + "literal": "io", + "type": "Identifier", + }, + { + "literal": ".", + "type": "Delimiter", + }, + { + "literal": "println", + "type": "Identifier", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": "f", + "type": "Identifier", + }, + { + "literal": ".", + "type": "Delimiter", + }, + { + "literal": "l", + "type": "Identifier", + }, + { + "literal": ".", + "type": "Delimiter", + }, + { + "literal": "e", + "type": "Identifier", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "}", + "type": "Delimiter", + }, + { + "literal": "}", + "type": "Delimiter", + }, + { + "literal": "}", + "type": "Delimiter", + }, + { + "literal": "fn", + "type": "Keyword", + }, + { + "literal": "fib", + "type": "Identifier", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": "n", + "type": "Identifier", + }, + { + "literal": ":", + "type": "Delimiter", + }, + { + "literal": "int", + "type": "Identifier", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "->", + "type": "Operator", + }, + { + "literal": "int", + "type": "Identifier", + }, + { + "literal": "{", + "type": "Delimiter", + }, + { + "literal": "if", + "type": "Keyword", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": "n", + "type": "Identifier", + }, + { + "literal": "<=", + "type": "Operator", + }, + { + "literal": "1", + "type": "Number", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "{", + "type": "Delimiter", + }, + { + "literal": "return", + "type": "Keyword", + }, + { + "literal": "n", + "type": "Identifier", + }, + { + "literal": "}", + "type": "Delimiter", + }, + { + "literal": "return", + "type": "Keyword", + }, + { + "literal": "fib", + "type": "Identifier", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": "n", + "type": "Identifier", + }, + { + "literal": "-", + "type": "Operator", + }, + { + "literal": "1", + "type": "Number", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "+", + "type": "Operator", + }, + { + "literal": "fib", + "type": "Identifier", + }, + { + "literal": "(", + "type": "Delimiter", + }, + { + "literal": "n", + "type": "Identifier", + }, + { + "literal": "-", + "type": "Operator", + }, + { + "literal": "2", + "type": "Number", + }, + { + "literal": ")", + "type": "Delimiter", + }, + { + "literal": "}", + "type": "Delimiter", + }, + { + "literal": "", + "type": "EOF", + }, + ], +} +`; diff --git a/tests/bindings/index.test.ts b/tests/bindings/index.test.ts index 4023cc2..03497ed 100644 --- a/tests/bindings/index.test.ts +++ b/tests/bindings/index.test.ts @@ -11,11 +11,11 @@ describe('llvm-bun', () => { const builder = new IRBuilder(ctx); builder.insertInto(entry); const i8 = Type.int8(ctx); - const ptrType = Type.pointer(i8); + const ptrType = Type.pointer(ctx); const ptr = builder.alloca(i8, 'ptr'); // Bitcast to another pointer type (e.g., i32*) const i32 = Type.int32(ctx); - const ptrI32 = Type.pointer(i32); + const ptrI32 = Type.pointer(ctx); const casted = builder.bitcast(ptr, ptrI32, 'casted'); expect(casted.handle).toBeTruthy(); expect(casted.getType().isPointer()).toBe(true); @@ -66,7 +66,7 @@ describe('llvm-bun', () => { it('creates pointer types', () => { const ctx = new Context() const i8 = Type.int8(ctx) - const ptr = Type.pointer(i8) + const ptr = Type.pointer(ctx) expect(ptr.isPointer()).toBe(true) }) @@ -115,7 +115,7 @@ describe('llvm-bun', () => { const ptr = builder.alloca(i32, 'x') const val = Value.constInt(i32, 123) builder.store(val, ptr) - const loaded = builder.load(ptr) + const loaded = builder.load(i32, ptr) expect(loaded.handle).toBeTruthy() }) it('builds integer and float arithmetic', () => { diff --git a/tests/syntax.test.ts b/tests/syntax.test.ts new file mode 100644 index 0000000..8154a77 --- /dev/null +++ b/tests/syntax.test.ts @@ -0,0 +1,24 @@ +import { Lexer } from "../src/lexer" +import { Parser } from "../src/parser" +import { join } from "node:path" +import { readdir, readFile } from "node:fs/promises" +import { expect, test } from "bun:test" + +const TEST_FILES_DIR = "testfiles" + +const testFiles = await readdir(TEST_FILES_DIR) + +for (const testFile of testFiles) { + test(`tokens and AST for: ${testFile}`, async () => { + const testFilePath = join(TEST_FILES_DIR, testFile) + const mod = await readFile(testFilePath, "utf8") + + const tokens = new Lexer(mod).lex() + const ast = new Parser(tokens).parse() + + expect({ + tokens: tokens, + ast: ast + }).toMatchSnapshot() + }) +} diff --git a/tests/testfiles/01.yt b/tests/testfiles/01.yt new file mode 100644 index 0000000..15b7369 --- /dev/null +++ b/tests/testfiles/01.yt @@ -0,0 +1,3 @@ +let a := 5 +let x := a * 2 +let y := x + a diff --git a/tests/testfiles/02.yt b/tests/testfiles/02.yt new file mode 100644 index 0000000..04db029 --- /dev/null +++ b/tests/testfiles/02.yt @@ -0,0 +1,48 @@ +[| + yttria + a blazingly fast*, universal* and easy-to-use* programming language + for anything you can imagine* + * = not true (yet) + + this file is a complete (albeit meaningless) program + that demonstrates the syntax of yttria + it is not meant to be run, but rather to be read + it is a work in progress, so expect changes in the future +|] + +fn main() -> void { + const a := 1 + const b: int = 2 + let c := 3 + let d: float = 4.5 + + try foo(x) + catch (e) bar(w) + finally quux(c) + + io.println(MsgSuccess) + + const numbers: int[] = 34 + + if (a < b) { + io.println(1) + } else if (a > b) { + io.println(2) + } else { + io.println(3) + } + + switch (c) { + 1 -> { io.println(m.x.y) } + 2 -> { io.println(c.t.p) } + default -> { io.println(f.l.e) } + } +} + +fn fib(n: int) -> int { + if (n <= 1) { + return n + } + + return fib(n - 1) + fib(n - 2) +} diff --git a/tsconfig.json b/tsconfig.json index fe042f9..88c9544 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,8 +2,10 @@ "compilerOptions": { "target": "ESNext", "module": "ESNext", - "moduleResolution": "Node", "strict": true, "esModuleInterop": true, + "types": [ + "@types/bun", + ] } } \ No newline at end of file