From ded3ae8a75c8f337f1a1f369e0b9e28ff813ea56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?gen=C3=A8ve?= <36968271+grngxd@users.noreply.github.com> Date: Wed, 21 Jan 2026 15:45:19 +0000 Subject: [PATCH 1/8] feat(codegen): upgrade to llvm 21 & start to impl gen --- bun.lock | 1 + index.ts | 22 +++++++++++++++-- src/bindings/ffi.ts | 16 ++++++------ src/bindings/index.ts | 27 ++++++++++---------- src/gen/helper.ts | 45 +++++++++++++++++++++++++++++++++ src/gen/index.ts | 48 ++++++++++++++++++++++++++++++++++++ src/semantic/typing/index.ts | 10 +++++++- tests/bindings/index.test.ts | 8 +++--- 8 files changed, 149 insertions(+), 28 deletions(-) create mode 100644 src/gen/helper.ts create mode 100644 src/gen/index.ts diff --git a/bun.lock b/bun.lock index f0b5dde..9b586cf 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "dependencies": { diff --git a/index.ts b/index.ts index fb9369d..db6765d 100644 --- a/index.ts +++ b/index.ts @@ -3,6 +3,7 @@ import { Lexer } from "./src/lexer" import { Parser } from "./src/parser" import { SemanticAnalyzer } from "./src/semantic/analysis" import { TypeChecker } from "./src/semantic/typing" +import { Codegen } from "./src/gen" rmSync("./out", { recursive: true, force: true }) // error driven development right here @@ -39,8 +40,9 @@ rmSync("./out", { recursive: true, force: true }) const program = ` fn main() { - let x := true + let x: int = 3 let y := 3 + x + return 0 } `.trim() @@ -71,4 +73,20 @@ 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) + +const cgTime = performance.now() + +console.log(`total time: ${(cgTime - start).toFixed(3)}ms`) + +Bun.write("out/module.ll", llvmIr) +const clang = Bun.spawn({ + cmd: ["clang", "-o", "out/program.exe", "out/module.ll"], +}) +await clang.exited + +const exe = Bun.spawn({ + cmd: ["out/program.exe"], +}) \ No newline at end of file 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..251a423 --- /dev/null +++ b/src/gen/helper.ts @@ -0,0 +1,45 @@ +import VM, { Context, Func, FunctionType, IRBuilder, Linkage, Module, 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); + } + + 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..a5250ee --- /dev/null +++ b/src/gen/index.ts @@ -0,0 +1,48 @@ +import { Func, FunctionType, Module, Type, Value } from "../bindings"; +import { ExpressionType, FunctionDeclaration, ProgramExpression, ReturnExpression } 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), + }; + + 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): Value | void { + this.h.ret( + Value.constInt(Type.int32(this.h.ctx), 2) + ) + } +} \ No newline at end of file diff --git a/src/semantic/typing/index.ts b/src/semantic/typing/index.ts index bc35c6c..ea146c9 100644 --- a/src/semantic/typing/index.ts +++ b/src/semantic/typing/index.ts @@ -1,4 +1,4 @@ -import { BinaryExpression, Expression, ExpressionType, FunctionDeclaration, Identifier, NumberLiteral, ProgramExpression, VariableDeclaration } from "../../parser/ast" +import { BinaryExpression, Expression, ExpressionType, FunctionDeclaration, Identifier, NumberLiteral, ProgramExpression, ReturnExpression, VariableDeclaration } from "../../parser/ast" import { similarType, Type } from "./types" export class TypeEnvironment { @@ -48,6 +48,7 @@ export class TypeChecker { "BinaryExpression": (expr) => this.visitBinaryExpression(expr as BinaryExpression), "VariableDeclaration": (expr) => this.visitVariableDeclaration(expr as VariableDeclaration), + "ReturnExpression": (expr) => this.visitReturnExpression(expr as ReturnExpression), "NumberLiteral": (expr) => { return Number.isInteger((expr as NumberLiteral).value) ? { kind: "int" } : { kind: "float" } }, "BooleanLiteral": (expr) => { return { kind: "bool" } }, @@ -116,4 +117,11 @@ export class TypeChecker { this.env.define(name, type) return type } + + private visitReturnExpression(r: ReturnExpression): Type | undefined { + if (r.value) { + return this.checkExpression(r.value) + } + return undefined + } } \ No newline at end of file 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', () => { From f6d6877a5dd3e266c0dc51e7cb850babb53bbc06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?gen=C3=A8ve?= <36968271+grngxd@users.noreply.github.com> Date: Thu, 22 Jan 2026 11:35:14 +0000 Subject: [PATCH 2/8] feat(codegen): implement binary expression handling and arithmetic operations --- index.ts | 11 ++++++--- src/gen/helper.ts | 20 +++++++++++++++ src/gen/index.ts | 63 ++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 87 insertions(+), 7 deletions(-) diff --git a/index.ts b/index.ts index db6765d..c0c1634 100644 --- a/index.ts +++ b/index.ts @@ -40,9 +40,7 @@ rmSync("./out", { recursive: true, force: true }) const program = ` fn main() { - let x: int = 3 - let y := 3 + x - return 0 + return 10 / 2 + 3 * 4 - 5 } `.trim() @@ -80,6 +78,7 @@ await Bun.write("out/module.ll", llvmIr) const cgTime = performance.now() console.log(`total time: ${(cgTime - start).toFixed(3)}ms`) +console.log("") Bun.write("out/module.ll", llvmIr) const clang = Bun.spawn({ @@ -89,4 +88,8 @@ await clang.exited const exe = Bun.spawn({ cmd: ["out/program.exe"], -}) \ No newline at end of file +}) + +await exe.exited + +console.log(`program exited with code ${exe.exitCode}`) \ No newline at end of file diff --git a/src/gen/helper.ts b/src/gen/helper.ts index 251a423..764c372 100644 --- a/src/gen/helper.ts +++ b/src/gen/helper.ts @@ -38,6 +38,26 @@ export class LLVMHelper { 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); + } + toString() { this.mod.verify() return this.mod.toString(); diff --git a/src/gen/index.ts b/src/gen/index.ts index a5250ee..39f512a 100644 --- a/src/gen/index.ts +++ b/src/gen/index.ts @@ -1,5 +1,5 @@ import { Func, FunctionType, Module, Type, Value } from "../bindings"; -import { ExpressionType, FunctionDeclaration, ProgramExpression, ReturnExpression } from "../parser/ast"; +import { BinaryExpression, Expression, ExpressionType, FunctionDeclaration, NumberLiteral, ProgramExpression, ReturnExpression } from "../parser/ast"; import { LLVMHelper } from "./helper"; export class Codegen { @@ -9,6 +9,7 @@ export class Codegen { private table: { [key in ExpressionType]?: (e: any) => Func | Value | void } = { FunctionDeclaration: this.genFunctionDeclaration.bind(this), ReturnExpression: this.genReturnExpression.bind(this), + BinaryExpression: this.getBinaryExpression.bind(this), }; constructor(moduleName: string, ast: ProgramExpression) { @@ -40,9 +41,65 @@ export class Codegen { return fn; } - private genReturnExpression(r: ReturnExpression): Value | void { + 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( - Value.constInt(Type.int32(this.h.ctx), 2) + v ) } + + private getBinaryExpression(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 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.getBinaryExpression(e) as Value; + } + } + + return null; + } } \ No newline at end of file From b422cc785366aa06fee0551b9fc1ba863fa888d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?gen=C3=A8ve?= <36968271+grngxd@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:50:08 +0000 Subject: [PATCH 3/8] feat(codegen): simple variables & binary --- index.ts | 5 ++++- src/gen/helper.ts | 14 +++++++++++++- src/gen/index.ts | 31 +++++++++++++++++++++++++++---- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/index.ts b/index.ts index c0c1634..afa032f 100644 --- a/index.ts +++ b/index.ts @@ -40,7 +40,10 @@ rmSync("./out", { recursive: true, force: true }) const program = ` fn main() { - return 10 / 2 + 3 * 4 - 5 + let a := 10 + let b := 20 + let c := a + b + return a + b * c / 2 - 5 } `.trim() diff --git a/src/gen/helper.ts b/src/gen/helper.ts index 764c372..1178619 100644 --- a/src/gen/helper.ts +++ b/src/gen/helper.ts @@ -1,4 +1,4 @@ -import VM, { Context, Func, FunctionType, IRBuilder, Linkage, Module, Value } from "../bindings"; +import VM, { Context, Func, FunctionType, IRBuilder, Linkage, Module, Type, Value } from "../bindings"; export class LLVMHelper { public name: string; @@ -58,6 +58,18 @@ export class LLVMHelper { 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(); diff --git a/src/gen/index.ts b/src/gen/index.ts index 39f512a..181047a 100644 --- a/src/gen/index.ts +++ b/src/gen/index.ts @@ -1,5 +1,5 @@ import { Func, FunctionType, Module, Type, Value } from "../bindings"; -import { BinaryExpression, Expression, ExpressionType, FunctionDeclaration, NumberLiteral, ProgramExpression, ReturnExpression } from "../parser/ast"; +import { BinaryExpression, Expression, ExpressionType, FunctionDeclaration, Identifier, NumberLiteral, ProgramExpression, ReturnExpression, VariableDeclaration } from "../parser/ast"; import { LLVMHelper } from "./helper"; export class Codegen { @@ -9,9 +9,12 @@ export class Codegen { private table: { [key in ExpressionType]?: (e: any) => Func | Value | void } = { FunctionDeclaration: this.genFunctionDeclaration.bind(this), ReturnExpression: this.genReturnExpression.bind(this), - BinaryExpression: this.getBinaryExpression.bind(this), + BinaryExpression: this.genBinaryExpression.bind(this), + VariableDeclaration: this.genVariableDeclaration.bind(this) }; + private variables: { [name: string]: { ptr: Value; type: Type } } = {}; + constructor(moduleName: string, ast: ProgramExpression) { this.h = new LLVMHelper(moduleName); this.ast = ast; @@ -52,7 +55,7 @@ export class Codegen { ) } - private getBinaryExpression(b: BinaryExpression): Value | void { + private genBinaryExpression(b: BinaryExpression): Value | void { const left = this.getValueFromExpression(b.left); const right = this.getValueFromExpression(b.right); @@ -78,6 +81,17 @@ export class Codegen { 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() }; + } + private getTypeFromExpression(e: Expression): Type | null { switch (e.type) { case "NumberLiteral": @@ -96,7 +110,16 @@ export class Codegen { case "BinaryExpression": { const e = expr as BinaryExpression; - return this.getBinaryExpression(e) as Value; + 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}`); + } + return this.h.load(obj.type, obj.ptr, e.value); } } From e9ee5223207eac9e88fe844da2b9d719b484ab77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?gen=C3=A8ve?= <36968271+grngxd@users.noreply.github.com> Date: Fri, 23 Jan 2026 09:40:06 +0000 Subject: [PATCH 4/8] feat(codegen): cache variable values --- index.ts | 2 +- src/gen/index.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/index.ts b/index.ts index afa032f..f4015a8 100644 --- a/index.ts +++ b/index.ts @@ -85,7 +85,7 @@ console.log("") Bun.write("out/module.ll", llvmIr) const clang = Bun.spawn({ - cmd: ["clang", "-o", "out/program.exe", "out/module.ll"], + cmd: ["clang", "-o", "out/program.exe", "out/module.ll", "-O2"], }) await clang.exited diff --git a/src/gen/index.ts b/src/gen/index.ts index 181047a..db2613e 100644 --- a/src/gen/index.ts +++ b/src/gen/index.ts @@ -10,10 +10,10 @@ export class Codegen { FunctionDeclaration: this.genFunctionDeclaration.bind(this), ReturnExpression: this.genReturnExpression.bind(this), BinaryExpression: this.genBinaryExpression.bind(this), - VariableDeclaration: this.genVariableDeclaration.bind(this) + VariableDeclaration: this.genVariableDeclaration.bind(this) }; - private variables: { [name: string]: { ptr: Value; type: Type } } = {}; + private variables: { [name: string]: { ptr: Value; type: Type; value?: Value } } = {}; constructor(moduleName: string, ast: ProgramExpression) { this.h = new LLVMHelper(moduleName); @@ -89,7 +89,7 @@ export class Codegen { const ptr = this.h.alloca(val.getType(), v.name.value) this.h.store(val, ptr); - this.variables[v.name.value] = { ptr, type: val.getType() }; + this.variables[v.name.value] = { ptr, type: val.getType(), value: val }; } private getTypeFromExpression(e: Expression): Type | null { @@ -119,6 +119,8 @@ export class Codegen { 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); } } From 036b9f27131c7e3dd1c997ffb81df631f95b9b26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?gen=C3=A8ve?= <36968271+grngxd@users.noreply.github.com> Date: Fri, 23 Jan 2026 11:47:03 +0000 Subject: [PATCH 5/8] impl(semantic): simple type inference --- index.ts | 14 ++-- src/parser/ast.ts | 16 ++-- src/semantic/{typing/index.ts => checker.ts} | 2 +- src/semantic/inferrer.ts | 85 ++++++++++++++++++++ src/semantic/{typing => }/types.ts | 6 ++ 5 files changed, 110 insertions(+), 13 deletions(-) rename src/semantic/{typing/index.ts => checker.ts} (98%) create mode 100644 src/semantic/inferrer.ts rename src/semantic/{typing => }/types.ts (89%) diff --git a/index.ts b/index.ts index f4015a8..4ad4b41 100644 --- a/index.ts +++ b/index.ts @@ -2,8 +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 @@ -66,10 +67,13 @@ 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() -await Bun.write("out/semantic.json", JSON.stringify(analyzed, null, 2)) +const tc = new TypeChecker(inferred) +const checked = tc.check() + +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`) @@ -85,7 +89,7 @@ console.log("") Bun.write("out/module.ll", llvmIr) const clang = Bun.spawn({ - cmd: ["clang", "-o", "out/program.exe", "out/module.ll", "-O2"], + cmd: ["clang", "-o", "out/program.exe", "out/module.ll"], }) await clang.exited diff --git a/src/parser/ast.ts b/src/parser/ast.ts index fdaa463..2c65311 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,7 +76,7 @@ export type FunctionDeclaration = Expression & { export type FunctionCall = Expression & { type: "FunctionCall" callee: Identifier// | MemberAccess - // inferredCallee?: CheckerType | CheckerPlaceholder + inferredCallee?: Type | PlaceholderType args: Expression[] } @@ -87,7 +89,7 @@ export type FunctionParam = { 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/typing/index.ts b/src/semantic/checker.ts similarity index 98% rename from src/semantic/typing/index.ts rename to src/semantic/checker.ts index ea146c9..4adcfdc 100644 --- a/src/semantic/typing/index.ts +++ b/src/semantic/checker.ts @@ -1,4 +1,4 @@ -import { BinaryExpression, Expression, ExpressionType, FunctionDeclaration, Identifier, NumberLiteral, ProgramExpression, ReturnExpression, VariableDeclaration } from "../../parser/ast" +import { BinaryExpression, Expression, ExpressionType, FunctionDeclaration, Identifier, NumberLiteral, ProgramExpression, ReturnExpression, VariableDeclaration } from "../parser/ast" import { similarType, Type } from "./types" export class TypeEnvironment { 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 89% rename from src/semantic/typing/types.ts rename to src/semantic/types.ts index b9aa893..a8eed6c 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,11 @@ export type StringType = Type & { value: string } +export type PlaceholderType = Type & { + kind: "placeholder" + id: string +} + export const sameType = (a?: Type, b?: Type): boolean => { if (!a || !b) return false; From cf62c9410e0fede3fb75ee56cd764b7ab889301c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?gen=C3=A8ve?= <36968271+grngxd@users.noreply.github.com> Date: Fri, 23 Jan 2026 12:04:41 +0000 Subject: [PATCH 6/8] fix(logging): improve logging messages for lexer and codegen timing --- index.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/index.ts b/index.ts index 4ad4b41..f66625e 100644 --- a/index.ts +++ b/index.ts @@ -55,7 +55,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() @@ -82,9 +82,6 @@ const cg = new Codegen("my_module", ast) const llvmIr = cg.generate() await Bun.write("out/module.ll", llvmIr) -const cgTime = performance.now() - -console.log(`total time: ${(cgTime - start).toFixed(3)}ms`) console.log("") Bun.write("out/module.ll", llvmIr) @@ -93,6 +90,13 @@ const clang = Bun.spawn({ }) 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"], }) From 8e1dbd082105a3ff33e32993ff8fb1f0be3fee8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?gen=C3=A8ve?= <36968271+grngxd@users.noreply.github.com> Date: Fri, 1 May 2026 10:16:51 +0100 Subject: [PATCH 7/8] feat(semantic): enhance type checking for function parameters and variable declarations Co-authored-by: Copilot --- bun.lock | 10 ++--- index.ts | 10 +++-- package.json | 2 +- src/parser/ast.ts | 2 +- src/semantic/analysis.ts | 9 +++++ src/semantic/checker.ts | 81 ++++++++++++++++++++++++++++++---------- src/semantic/types.ts | 7 ++++ tsconfig.json | 4 +- 8 files changed, 92 insertions(+), 33 deletions(-) diff --git a/bun.lock b/bun.lock index 9b586cf..80170b4 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,7 @@ "cmake-js": "^7.3.1", }, "devDependencies": { - "@types/bun": "^1.2.19", + "@types/bun": "^1.3.13", }, }, }, @@ -15,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=="], @@ -33,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=="], @@ -53,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 f66625e..853cff2 100644 --- a/index.ts +++ b/index.ts @@ -41,10 +41,12 @@ rmSync("./out", { recursive: true, force: true }) const program = ` fn main() { - let a := 10 - let b := 20 - let c := a + b - return a + b * c / 2 - 5 + return 0 +} + +fn hello() { + let b: int= 3 + return 2 } `.trim() 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/parser/ast.ts b/src/parser/ast.ts index 2c65311..622d0ab 100644 --- a/src/parser/ast.ts +++ b/src/parser/ast.ts @@ -83,7 +83,7 @@ export type FunctionCall = Expression & { export type FunctionParam = { type: "FunctionParam" name: Identifier - paramType: Identifier + paramType: Identifier // declared type annotation } export type IfExpression = Expression & { 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 index 4adcfdc..389a273 100644 --- a/src/semantic/checker.ts +++ b/src/semantic/checker.ts @@ -1,5 +1,5 @@ import { BinaryExpression, Expression, ExpressionType, FunctionDeclaration, Identifier, NumberLiteral, ProgramExpression, ReturnExpression, VariableDeclaration } from "../parser/ast" -import { similarType, Type } from "./types" +import { newPlaceholderType, similarType, Type } from "./types" export class TypeEnvironment { private types: Map = new Map() @@ -17,9 +17,20 @@ export class TypeEnvironment { } define(name: string, type: Type) { - if (this.types.has(name)) { - throw new Error(`type ${name} already defined`) + 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) } @@ -42,13 +53,13 @@ export class TypeChecker { private ast: ProgramExpression private table: { [key in ExpressionType]?: (expr: Expression) => Type | undefined } = { - "FunctionDeclaration": (expr) => this.visitFunctionDeclaration(expr as FunctionDeclaration), + "FunctionDeclaration": (expr) => this.checkFunctionDeclaration(expr as FunctionDeclaration), - "Identifier": (expr) => this.visitIdentifier(expr as Identifier), + "Identifier": (expr) => this.checkIdentifier(expr as Identifier), - "BinaryExpression": (expr) => this.visitBinaryExpression(expr as BinaryExpression), - "VariableDeclaration": (expr) => this.visitVariableDeclaration(expr as VariableDeclaration), - "ReturnExpression": (expr) => this.visitReturnExpression(expr as ReturnExpression), + "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" } }, @@ -73,24 +84,35 @@ export class TypeChecker { return fn(expr) } - private visitFunctionDeclaration(f: FunctionDeclaration) { + 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) } - const restored = this.env.restoreParent(); + this.env = this.env.restoreParent(); return undefined; } - private visitIdentifier(id: Identifier): Type { + private checkIdentifier(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 { + private checkBinaryExpression(b: BinaryExpression): Type { const leftType = this.checkExpression(b.left) const rightType = this.checkExpression(b.right) @@ -110,15 +132,36 @@ export class TypeChecker { 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 + 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 visitReturnExpression(r: ReturnExpression): Type | undefined { + private checkReturnExpression(r: ReturnExpression): Type | undefined { if (r.value) { return this.checkExpression(r.value) } diff --git a/src/semantic/types.ts b/src/semantic/types.ts index a8eed6c..8ecef9e 100644 --- a/src/semantic/types.ts +++ b/src/semantic/types.ts @@ -31,6 +31,11 @@ export type PlaceholderType = Type & { 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; @@ -43,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/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 From eaefe4adb17910fb07760f2072a2174c2fa296de Mon Sep 17 00:00:00 2001 From: Abidin Durdu Date: Sun, 12 Jul 2026 12:59:37 +0300 Subject: [PATCH 8/8] use snapshot testing for syntax analysis --- tests/__snapshots__/syntax.test.ts.snap | 1395 +++++++++++++++++++++++ tests/syntax.test.ts | 24 + tests/testfiles/01.yt | 3 + tests/testfiles/02.yt | 48 + 4 files changed, 1470 insertions(+) create mode 100644 tests/__snapshots__/syntax.test.ts.snap create mode 100644 tests/syntax.test.ts create mode 100644 tests/testfiles/01.yt create mode 100644 tests/testfiles/02.yt 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/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) +}