const std = @import("std"); const types = @import("types.zig"); const Type = types.Type; const unescape = @import("unescape.zig"); /// Runtime value for comptime evaluation. /// Replaces codegen's JitResult with richer type support. pub const Value = union(enum) { int_val: i64, float_val: f64, float32_val: f32, bool_val: bool, string_val: []const u8, void_val: void, struct_val: StructValue, array_val: ArrayValue, type_val: Type, function_val: FunctionVal, pointer_val: PointerValue, null_val: void, pub const PointerValue = struct { target: [*]Value, }; pub const StructValue = struct { type_name: []const u8, field_names: []const []const u8, fields: []Value, }; pub const ArrayValue = struct { elements: []Value, }; pub const FunctionVal = struct { name: []const u8, param_count: u8, }; pub fn isInt(self: Value) bool { return self == .int_val; } pub fn isFloat(self: Value) bool { return switch (self) { .float_val, .float32_val => true, else => false, }; } pub fn asInt(self: Value) ?i64 { return switch (self) { .int_val => |v| v, .bool_val => |v| if (v) @as(i64, 1) else 0, else => null, }; } pub fn asIndex(self: Value) !usize { return @intCast(self.asInt() orelse return error.TypeError); } pub fn isTruthy(self: Value) bool { return switch (self) { .bool_val => |bv| bv, .int_val => |iv| iv != 0, else => true, }; } pub fn asFloat(self: Value) ?f64 { return switch (self) { .float_val => |v| v, .float32_val => |v| @floatCast(v), .int_val => |v| @floatFromInt(v), else => null, }; } pub fn format(self: Value, allocator: std.mem.Allocator) ![]const u8 { return switch (self) { .int_val => |v| std.fmt.allocPrint(allocator, "{d}", .{v}), .float_val => |v| std.fmt.allocPrint(allocator, "{d}", .{v}), .float32_val => |v| std.fmt.allocPrint(allocator, "{d}", .{v}), .bool_val => |v| if (v) allocator.dupe(u8, "true") else allocator.dupe(u8, "false"), .string_val => |v| allocator.dupe(u8, v), .void_val => allocator.dupe(u8, "void"), .type_val => |v| v.displayName(allocator), .function_val => |v| std.fmt.allocPrint(allocator, "", .{v.name}), .struct_val => |v| { var buf: std.ArrayList(u8) = .empty; try buf.appendSlice(allocator, v.type_name); try buf.append(allocator, '{'); for (v.fields, 0..) |fv, i| { if (i > 0) try buf.appendSlice(allocator, ", "); if (i < v.field_names.len) { try buf.appendSlice(allocator, v.field_names[i]); try buf.appendSlice(allocator, ": "); } const fs = try fv.format(allocator); try buf.appendSlice(allocator, fs); } try buf.append(allocator, '}'); return buf.items; }, .array_val => |v| { var buf: std.ArrayList(u8) = .empty; try buf.append(allocator, '['); for (v.elements, 0..) |elem, i| { if (i > 0) try buf.appendSlice(allocator, ", "); const es = try elem.format(allocator); try buf.appendSlice(allocator, es); } try buf.append(allocator, ']'); return buf.items; }, .pointer_val => |pv| { const inner = try pv.target[0].format(allocator); return std.fmt.allocPrint(allocator, "@{s}", .{inner}); }, .null_val => allocator.dupe(u8, "null"), }; } }; /// Bytecode instruction for the comptime VM. pub const Instruction = union(enum) { // Constants push_int: i64, push_float: f64, push_f32: f32, push_true, push_false, push_string: u32, // index into Chunk.strings push_void, push_type: Type, push_function: FnRef, // Local variables get_local: u16, // slot index in current frame set_local: u16, // Global variables (resolved lazily from root_decls) get_global: u32, // index into Chunk.strings for the global name // Arithmetic (type-dispatched at runtime via Value tag) add, sub, mul, div, mod, negate, // Comparison eq, neq, lt, lte, gt, gte, // Bitwise bit_and, bit_or, // Logic not, // Type conversion cast: CastInfo, // Control flow jump: i32, // relative offset jump_if_false: i32, jump_if_true: i32, pop, dup, // Functions call: CallInfo, call_builtin: BuiltinCall, ret, ret_void, // Structs make_struct: StructMake, get_field: u16, set_field: u16, // Pointers address_of_local: u16, // push pointer to local slot address_of_index, // pop idx, pop array, push pointer to element deref, // pop pointer, push dereferenced value deref_set, // pop value, pop pointer, store through pointer push_null, // push null pointer // Arrays make_array: u32, // element count on stack get_index, set_index, // Strings concat, format_to_string, // convert top-of-stack value to string representation pub const CastInfo = struct { to: ValueKind }; pub const CallInfo = struct { func_name: []const u8, arg_count: u8 }; pub const BuiltinCall = struct { id: BuiltinId, arg_count: u8 }; pub const StructMake = struct { type_name: []const u8, field_count: u16, field_names: []const []const u8 }; pub const FnRef = struct { name: []const u8, param_count: u8 }; }; pub const ValueKind = enum { int, float, f32_k, bool_k, string }; pub const BuiltinId = enum { print, write, sqrt, size_of, cast, alloc }; /// A compiled function or expression — a flat sequence of instructions. pub const Chunk = struct { code: []const Instruction, strings: []const []const u8, // string constant pool local_count: u16, // number of local variable slots name: []const u8, // function name (for debugging) }; const ast = @import("ast.zig"); const Node = ast.Node; const sema = @import("sema.zig"); const codegen_mod = @import("codegen.zig"); const llvm = @import("llvm_api.zig"); /// Compute byte size of a Type. Uses LLVM data layout via codegen if available, /// otherwise falls back to known sizes for primitive types. fn sizeOfType(ty: Type, cg: ?*codegen_mod.CodeGen) u64 { if (cg) |gen| { if (std.meta.eql(ty, Type.void_type)) return 0; const llvm_ty = gen.typeToLLVM(ty); const data_layout = llvm.c.LLVMGetModuleDataLayout(gen.module); return llvm.c.LLVMStoreSizeOfType(data_layout, llvm_ty); } // Fallback without codegen return switch (ty) { .signed, .unsigned => |w| (w + 7) / 8, .f32 => 4, .f64 => 8, .boolean => 1, .string_type => 8, .void_type => 0, .enum_type => 4, else => 0, }; } /// Compiles AST expressions into bytecode Chunks. pub const Compiler = struct { allocator: std.mem.Allocator, instructions: std.ArrayList(Instruction), strings: std.ArrayList([]const u8), locals: std.ArrayList(Local), scope_depth: u16, sema_result: ?*const sema.SemaResult, root_decls: []const *Node, codegen: ?*codegen_mod.CodeGen, // Loop context for break/continue loop_start: ?usize = null, // instruction index of condition start (for continue) break_patches: std.ArrayList(usize) = std.ArrayList(usize).empty, // indices of break jumps to patch const Local = struct { name: []const u8, depth: u16, }; pub fn init(allocator: std.mem.Allocator, sema_result: ?*const sema.SemaResult, root_decls: []const *Node, cg: ?*codegen_mod.CodeGen) Compiler { return .{ .allocator = allocator, .instructions = std.ArrayList(Instruction).empty, .strings = std.ArrayList([]const u8).empty, .locals = std.ArrayList(Local).empty, .scope_depth = 0, .sema_result = sema_result, .root_decls = root_decls, .codegen = cg, }; } pub fn compile(self: *Compiler, expr: *Node) !Chunk { try self.compileNode(expr); return .{ .code = try self.instructions.toOwnedSlice(self.allocator), .strings = try self.strings.toOwnedSlice(self.allocator), .local_count = @intCast(self.locals.items.len), .name = "", }; } pub fn compileFunction(self: *Compiler, fd: ast.FnDecl) !Chunk { // Add params as locals for (fd.params) |param| { try self.locals.append(self.allocator, .{ .name = param.name, .depth = self.scope_depth }); } try self.compileNode(fd.body); // Ensure there's a return at the end. // If the function has a return type, emit `ret` (implicit return of last value). // Otherwise emit `ret_void`. const code = self.instructions.items; if (code.len == 0 or (code[code.len - 1] != .ret and code[code.len - 1] != .ret_void)) { const has_return_type = fd.return_type != null; if (has_return_type) { try self.emit(.ret); } else { try self.emit(.ret_void); } } return .{ .code = try self.instructions.toOwnedSlice(self.allocator), .strings = try self.strings.toOwnedSlice(self.allocator), .local_count = @intCast(self.locals.items.len), .name = fd.name, }; } fn emit(self: *Compiler, instruction: Instruction) !void { try self.instructions.append(self.allocator, instruction); } fn patchJump(self: *Compiler, idx: usize) void { self.instructions.items[idx] = .{ .jump = @intCast(@as(i64, @intCast(self.instructions.items.len)) - @as(i64, @intCast(idx)) - 1) }; } fn patchJumpIfFalse(self: *Compiler, idx: usize) void { self.instructions.items[idx] = .{ .jump_if_false = @intCast(@as(i64, @intCast(self.instructions.items.len)) - @as(i64, @intCast(idx)) - 1) }; } fn patchJumpIfTrue(self: *Compiler, idx: usize) void { self.instructions.items[idx] = .{ .jump_if_true = @intCast(@as(i64, @intCast(self.instructions.items.len)) - @as(i64, @intCast(idx)) - 1) }; } fn addString(self: *Compiler, str: []const u8) !u32 { const idx: u32 = @intCast(self.strings.items.len); try self.strings.append(self.allocator, str); return idx; } /// Look up a struct field index by name, handling pointer auto-deref. fn resolveFieldIndex(self: *Compiler, object: *Node, field: []const u8) ?u16 { if (self.sema_result) |sr| { const obj_ty = sr.type_map.get(object) orelse return null; const struct_name: ?[]const u8 = if (obj_ty.isStruct()) obj_ty.struct_type else if (obj_ty.isPointer()) obj_ty.pointer_type.pointee_name else null; if (struct_name) |sn| { if (sr.struct_types.get(sn)) |info| { for (info.field_names, 0..) |fname, idx| { if (std.mem.eql(u8, fname, field)) { return @intCast(idx); } } } } } return null; } fn resolveLocal(self: *Compiler, name: []const u8) ?u16 { var i = self.locals.items.len; while (i > 0) { i -= 1; if (std.mem.eql(u8, self.locals.items[i].name, name)) { return @intCast(i); } } return null; } /// Compile a string literal with escape sequences and interpolation support. /// Handles `{expr}` patterns by parsing and compiling the inner expressions, /// then concatenating all segments together. /// /// Strategy: emit each segment in order, and after each additional segment /// (from the second one onward), emit a concat instruction to merge it with /// the accumulated result so far. fn compileStringLiteral(self: *Compiler, raw: []const u8) !void { // String literals are plain text — {} is NOT interpolated here. // String interpolation is handled by print() at the call site. const unescaped = try unescape.unescapeString(self.allocator, raw); const idx = try self.addString(unescaped); try self.emit(.{ .push_string = idx }); } fn compileNode(self: *Compiler, node: *Node) anyerror!void { switch (node.data) { .int_literal => |lit| { try self.emit(.{ .push_int = lit.value }); }, .float_literal => |lit| { try self.emit(.{ .push_float = lit.value }); }, .bool_literal => |lit| { try self.emit(if (lit.value) .push_true else .push_false); }, .string_literal => |lit| { try self.compileStringLiteral(lit.raw); }, .identifier => |ident| { if (self.resolveLocal(ident.name)) |slot| { try self.emit(.{ .get_local = slot }); } else { // Not a local — emit get_global to resolve lazily at runtime const idx = try self.addString(ident.name); try self.emit(.{ .get_global = idx }); } }, .binary_op => |binop| { if (binop.op == .and_op) { // Short-circuit AND: LHS, dup, jump_if_false +N, pop, RHS try self.compileNode(binop.lhs); try self.emit(.dup); const jump_idx = self.instructions.items.len; try self.emit(.{ .jump_if_false = 0 }); try self.emit(.pop); try self.compileNode(binop.rhs); self.patchJumpIfFalse(jump_idx); } else if (binop.op == .or_op) { // Short-circuit OR: LHS, dup, jump_if_true +N, pop, RHS try self.compileNode(binop.lhs); try self.emit(.dup); const jump_idx = self.instructions.items.len; try self.emit(.{ .jump_if_true = 0 }); try self.emit(.pop); try self.compileNode(binop.rhs); self.patchJumpIfTrue(jump_idx); } else { try self.compileNode(binop.lhs); try self.compileNode(binop.rhs); try self.emit(switch (binop.op) { .add => .add, .sub => .sub, .mul => .mul, .div => .div, .mod => .mod, .eq => .eq, .neq => .neq, .lt => .lt, .lte => .lte, .gt => .gt, .gte => .gte, .bit_and => .bit_and, .bit_or => .bit_or, .and_op, .or_op => unreachable, }); } }, .chained_comparison => |chain| { // Compile first pair try self.compileNode(chain.operands[0]); try self.compileNode(chain.operands[1]); try self.emit(switch (chain.ops[0]) { .lt => .lt, .lte => .lte, .gt => .gt, .gte => .gte, .eq => .eq, .neq => .neq, else => unreachable, }); // For each subsequent pair, short-circuit AND var i: usize = 1; while (i < chain.ops.len) : (i += 1) { try self.emit(.dup); const jump_idx = self.instructions.items.len; try self.emit(.{ .jump_if_false = 0 }); try self.emit(.pop); try self.compileNode(chain.operands[i]); try self.compileNode(chain.operands[i + 1]); try self.emit(switch (chain.ops[i]) { .lt => .lt, .lte => .lte, .gt => .gt, .gte => .gte, .eq => .eq, .neq => .neq, else => unreachable, }); self.patchJumpIfFalse(jump_idx); } }, .unary_op => |unop| { if (unop.op == .address_of) { if (unop.operand.data == .identifier) { if (self.resolveLocal(unop.operand.data.identifier.name)) |slot| { try self.emit(.{ .address_of_local = slot }); } else { return error.UnsupportedExpression; } } else if (unop.operand.data == .index_expr) { // &arr[i] — push array, push index, address_of_index try self.compileNode(unop.operand.data.index_expr.object); try self.compileNode(unop.operand.data.index_expr.index); try self.emit(.address_of_index); } else { return error.UnsupportedExpression; } } else { try self.compileNode(unop.operand); switch (unop.op) { .negate => try self.emit(.negate), .not => try self.emit(.not), .xx => {}, // cast — handle later .address_of => unreachable, // handled above } } }, .comptime_expr => |ct| { try self.compileNode(ct.expr); }, .block => |blk| { self.scope_depth += 1; const scope_start = self.locals.items.len; for (blk.stmts) |stmt| { try self.compileNode(stmt); } // Pop locals from this scope while (self.locals.items.len > scope_start) { _ = self.locals.pop(); } self.scope_depth -= 1; }, .var_decl => |vd| { if (vd.value) |val| { try self.compileNode(val); } else { try self.emit(.push_void); } const slot: u16 = @intCast(self.locals.items.len); try self.locals.append(self.allocator, .{ .name = vd.name, .depth = self.scope_depth }); try self.emit(.{ .set_local = slot }); }, .const_decl => |cd| { try self.compileNode(cd.value); const slot: u16 = @intCast(self.locals.items.len); try self.locals.append(self.allocator, .{ .name = cd.name, .depth = self.scope_depth }); try self.emit(.{ .set_local = slot }); }, .assignment => |asgn| { if (asgn.target.data == .identifier) { if (self.resolveLocal(asgn.target.data.identifier.name)) |slot| { if (asgn.op != .assign) { // Compound assignment: get current value, compile RHS, apply op, set try self.emit(.{ .get_local = slot }); try self.compileNode(asgn.value); try self.emit(switch (asgn.op) { .add_assign => .add, .sub_assign => .sub, .mul_assign => .mul, .div_assign => .div, .mod_assign => .mod, .assign => unreachable, }); } else { try self.compileNode(asgn.value); } try self.emit(.{ .set_local = slot }); } else { return error.UndefinedVariable; } } else if (asgn.target.data == .index_expr) { // arr[i] = val → push arr, push idx, push val, set_index const ie = asgn.target.data.index_expr; try self.compileNode(ie.object); try self.compileNode(ie.index); if (asgn.op != .assign) { // Compound: get current, apply op with RHS try self.emit(.dup); // dup index // We need the array and index for both get and set // Stack: arr, idx — but we need arr[idx] for the compound op // Simpler: just support simple assign for index targets return error.UnsupportedExpression; } try self.compileNode(asgn.value); try self.emit(.set_index); // set_index pushes the modified container back; store it back if it's a local if (ie.object.data == .identifier) { if (self.resolveLocal(ie.object.data.identifier.name)) |slot| { try self.emit(.{ .set_local = slot }); } } } else if (asgn.target.data == .field_access) { // obj.field = val (works with auto-deref for pointers) const fa = asgn.target.data.field_access; const field_idx = self.resolveFieldIndex(fa.object, fa.field) orelse return error.UnsupportedExpression; try self.compileNode(fa.object); if (asgn.op != .assign) return error.UnsupportedExpression; try self.compileNode(asgn.value); try self.emit(.{ .set_field = field_idx }); // Store back to local if (fa.object.data == .identifier) { if (self.resolveLocal(fa.object.data.identifier.name)) |slot| { try self.emit(.{ .set_local = slot }); } } } else if (asgn.target.data == .deref_expr) { // p.* = val try self.compileNode(asgn.target.data.deref_expr.operand); if (asgn.op != .assign) return error.UnsupportedExpression; try self.compileNode(asgn.value); try self.emit(.deref_set); } }, .return_stmt => |rs| { if (rs.value) |val| { try self.compileNode(val); try self.emit(.ret); } else { try self.emit(.ret_void); } }, .if_expr => |ie| { try self.compileNode(ie.condition); const jump_false_idx = self.instructions.items.len; try self.emit(.{ .jump_if_false = 0 }); // placeholder try self.compileNode(ie.then_branch); if (ie.else_branch) |eb| { const jump_end_idx = self.instructions.items.len; try self.emit(.{ .jump = 0 }); // placeholder self.patchJumpIfFalse(jump_false_idx); try self.compileNode(eb); self.patchJump(jump_end_idx); } else { self.patchJumpIfFalse(jump_false_idx); } }, .call => |call_node| { // Compile arguments for (call_node.args) |arg| { try self.compileNode(arg); } // Resolve callee name const callee_name = if (call_node.callee.data == .identifier) call_node.callee.data.identifier.name else if (call_node.callee.data == .field_access) blk: { const fa = call_node.callee.data.field_access; if (fa.object.data == .identifier) { break :blk fa.field; } break :blk null; } else null; if (callee_name) |name| { // Check if it's a builtin const base = if (std.mem.lastIndexOfScalar(u8, name, '.')) |idx| name[idx + 1 ..] else name; if (std.meta.stringToEnum(BuiltinId, base)) |id| { try self.emit(.{ .call_builtin = .{ .id = id, .arg_count = @intCast(call_node.args.len) } }); } else { try self.emit(.{ .call = .{ .func_name = name, .arg_count = @intCast(call_node.args.len) } }); } } else { return error.InvalidCallee; } }, .match_expr => |me| { try self.compileNode(me.subject); var end_jumps = std.ArrayList(usize).empty; for (me.arms) |arm| { if (arm.pattern) |pattern| { try self.emit(.dup); // duplicate subject for comparison try self.compileNode(pattern); try self.emit(.eq); const jump_next_idx = self.instructions.items.len; try self.emit(.{ .jump_if_false = 0 }); // placeholder try self.emit(.pop); // pop the subject copy try self.compileNode(arm.body); try end_jumps.append(self.allocator, self.instructions.items.len); try self.emit(.{ .jump = 0 }); // placeholder jump to end self.patchJumpIfFalse(jump_next_idx); } else { // else arm: unconditionally execute body try self.emit(.pop); // pop the subject copy try self.compileNode(arm.body); try end_jumps.append(self.allocator, self.instructions.items.len); try self.emit(.{ .jump = 0 }); // placeholder jump to end } } try self.emit(.pop); // pop remaining subject // Patch all end jumps for (end_jumps.items) |idx| { self.patchJump(idx); } }, .struct_literal => |sl| { for (sl.field_inits) |fi| { try self.compileNode(fi.value); } const name = sl.struct_name orelse ""; const fnames = try self.allocator.alloc([]const u8, sl.field_inits.len); for (sl.field_inits, 0..) |fi, i| { fnames[i] = fi.name orelse ""; } try self.emit(.{ .make_struct = .{ .type_name = name, .field_count = @intCast(sl.field_inits.len), .field_names = fnames } }); }, .field_access => |fa| { try self.compileNode(fa.object); // Check for string field access (.len, .ptr) if (self.sema_result) |sr| { const obj_ty = sr.type_map.get(fa.object); if (obj_ty != null and obj_ty.? == .string_type) { if (std.mem.eql(u8, fa.field, "len")) { try self.emit(.{ .get_field = 1 }); // len is field 1 in {ptr, len} return; } else if (std.mem.eql(u8, fa.field, "ptr")) { try self.emit(.{ .get_field = 0 }); // ptr is field 0 return; } } } // Look up field index from sema struct_types (handles pointer auto-deref) if (self.resolveFieldIndex(fa.object, fa.field)) |field_idx| { try self.emit(.{ .get_field = field_idx }); return; } // Fallback: use field name for well-known string fields // (sema may not have type info for nodes in imported function bodies) if (std.mem.eql(u8, fa.field, "len")) { try self.emit(.{ .get_field = 1 }); } else { try self.emit(.{ .get_field = 0 }); } }, .array_literal => |al| { for (al.elements) |elem| { try self.compileNode(elem); } try self.emit(.{ .make_array = @intCast(al.elements.len) }); }, .index_expr => |ie| { try self.compileNode(ie.object); try self.compileNode(ie.index); try self.emit(.get_index); }, .type_expr => |te| { const resolved = if (self.sema_result) |sr| sr.type_map.get(node) orelse Type.fromName(te.name) orelse .void_type else Type.fromName(te.name) orelse .void_type; try self.emit(.{ .push_type = resolved }); }, .enum_literal => |el| { const idx = try self.addString(el.name); try self.emit(.{ .push_string = idx }); }, .while_expr => |we| { // Save outer loop context const saved_loop_start = self.loop_start; const saved_break_patches = self.break_patches; self.break_patches = std.ArrayList(usize).empty; // Record condition start position const condition_start = self.instructions.items.len; self.loop_start = condition_start; // Compile condition try self.compileNode(we.condition); // Jump past body if false const jump_false_idx = self.instructions.items.len; try self.emit(.{ .jump_if_false = 0 }); // placeholder // Compile body try self.compileNode(we.body); // Jump back to condition const back_offset = @as(i32, @intCast(condition_start)) - @as(i32, @intCast(self.instructions.items.len)) - 1; try self.emit(.{ .jump = back_offset }); // Patch jump_if_false to after the loop self.patchJumpIfFalse(jump_false_idx); // Patch all break jumps to after the loop for (self.break_patches.items) |patch_idx| { self.patchJump(patch_idx); } // Restore outer loop context self.loop_start = saved_loop_start; self.break_patches = saved_break_patches; }, .break_expr => { // Emit placeholder jump, record for patching try self.break_patches.append(self.allocator, self.instructions.items.len); try self.emit(.{ .jump = 0 }); // placeholder — patched when while ends }, .continue_expr => { // Jump back to condition start const target = self.loop_start orelse return error.UnsupportedExpression; const offset = @as(i32, @intCast(target)) - @as(i32, @intCast(self.instructions.items.len)) - 1; try self.emit(.{ .jump = offset }); }, .deref_expr => |de| { try self.compileNode(de.operand); try self.emit(.deref); }, .null_literal => { try self.emit(.push_null); }, .pointer_type_expr, .many_pointer_type_expr => { try self.emit(.push_void); // type expressions not meaningful as values }, .defer_stmt => {}, // defer not meaningful in comptime .insert_expr => {}, // handled by codegen, not VM else => { return error.UnsupportedExpression; }, } } }; /// Stack-based virtual machine for comptime bytecode execution. pub const VM = struct { stack: [256]Value = undefined, sp: u16 = 0, frames: [64]CallFrame = undefined, fp: u8 = 0, functions: std.StringHashMap(Chunk), globals: std.StringHashMap(Value), allocator: std.mem.Allocator, sema_result: ?*const sema.SemaResult, root_decls: []const *Node, codegen: ?*codegen_mod.CodeGen, pub const CallFrame = struct { chunk: *const Chunk, ip: u32, base_slot: u16, }; pub fn init(allocator: std.mem.Allocator, sema_result: ?*const sema.SemaResult, root_decls: []const *Node, cg: ?*codegen_mod.CodeGen) VM { return .{ .functions = std.StringHashMap(Chunk).init(allocator), .globals = std.StringHashMap(Value).init(allocator), .allocator = allocator, .sema_result = sema_result, .root_decls = root_decls, .codegen = cg, }; } fn push(self: *VM, value: Value) !void { if (self.sp >= 256) return error.StackOverflow; self.stack[self.sp] = value; self.sp += 1; } fn pop(self: *VM) !Value { if (self.sp == 0) return error.StackUnderflow; self.sp -= 1; return self.stack[self.sp]; } fn peek(self: *VM) !Value { if (self.sp == 0) return error.StackUnderflow; return self.stack[self.sp - 1]; } pub fn execute(self: *VM, chunk: *const Chunk) !Value { // Set up initial frame self.frames[0] = .{ .chunk = chunk, .ip = 0, .base_slot = 0 }; self.fp = 1; return self.run(); } fn run(self: *VM) !Value { while (true) { const frame = &self.frames[self.fp - 1]; if (frame.ip >= frame.chunk.code.len) { // End of chunk — return top of stack or void if (self.sp > frame.base_slot) { return self.pop(); } return .{ .void_val = {} }; } const instruction = frame.chunk.code[frame.ip]; frame.ip += 1; switch (instruction) { // Constants .push_int => |v| try self.push(.{ .int_val = v }), .push_float => |v| try self.push(.{ .float_val = v }), .push_f32 => |v| try self.push(.{ .float32_val = v }), .push_true => try self.push(.{ .bool_val = true }), .push_false => try self.push(.{ .bool_val = false }), .push_string => |idx| { if (idx < frame.chunk.strings.len) { try self.push(.{ .string_val = frame.chunk.strings[idx] }); } else { try self.push(.{ .string_val = "" }); } }, .push_void => try self.push(.{ .void_val = {} }), .push_type => |t| try self.push(.{ .type_val = t }), .push_function => |fr| try self.push(.{ .function_val = .{ .name = fr.name, .param_count = fr.param_count } }), // Stack ops .pop => _ = try self.pop(), .dup => { const v = try self.peek(); try self.push(v); }, // Local variables .get_local => |slot| { const abs_slot = frame.base_slot + slot; if (abs_slot < self.sp) { try self.push(self.stack[abs_slot]); } else { try self.push(.{ .void_val = {} }); } }, .set_local => |slot| { const abs_slot = frame.base_slot + slot; const val = try self.pop(); // Grow stack if needed while (self.sp <= abs_slot) { self.stack[self.sp] = .{ .void_val = {} }; self.sp += 1; } self.stack[abs_slot] = val; }, // Pointers .address_of_local => |slot| { const abs_slot = frame.base_slot + slot; // Grow stack if needed so the target slot exists while (self.sp <= abs_slot) { self.stack[self.sp] = .{ .void_val = {} }; self.sp += 1; } const ptr: [*]Value = @ptrCast(&self.stack[abs_slot]); try self.push(.{ .pointer_val = .{ .target = ptr } }); }, .address_of_index => { const idx_val = try self.pop(); const arr = try self.pop(); const idx: usize = try idx_val.asIndex(); if (arr == .array_val) { if (idx >= arr.array_val.elements.len) return error.IndexOutOfBounds; try self.push(.{ .pointer_val = .{ .target = arr.array_val.elements.ptr + idx } }); } else { return error.TypeError; } }, .deref => { const v = try self.pop(); if (v == .pointer_val) { try self.push(try self.cloneValue(v.pointer_val.target[0])); } else if (v == .null_val) { return error.NullDereference; } else { return error.TypeError; } }, .deref_set => { const val = try self.pop(); const ptr_v = try self.pop(); if (ptr_v == .pointer_val) { ptr_v.pointer_val.target[0] = val; } else if (ptr_v == .null_val) { return error.NullDereference; } else { return error.TypeError; } }, .push_null => try self.push(.{ .null_val = {} }), // Global variables (lazily resolved from root_decls) .get_global => |name_idx| { const name = if (name_idx < frame.chunk.strings.len) frame.chunk.strings[name_idx] else return error.InvalidGlobal; try self.push(try self.resolveGlobal(name)); }, // Arithmetic .add => try self.execArith(.add_op), .sub => try self.execArith(.sub_op), .mul => try self.execArith(.mul_op), .div => try self.execArith(.div_op), .mod => try self.execArith(.mod_op), .bit_and => { const b = try self.pop(); const a = try self.pop(); if (a == .int_val and b == .int_val) { try self.push(.{ .int_val = a.int_val & b.int_val }); } else return error.TypeError; }, .bit_or => { const b = try self.pop(); const a = try self.pop(); if (a == .int_val and b == .int_val) { try self.push(.{ .int_val = a.int_val | b.int_val }); } else return error.TypeError; }, .negate => { const v = try self.pop(); try self.push(switch (v) { .int_val => |i| Value{ .int_val = -i }, .float_val => |f| Value{ .float_val = -f }, .float32_val => |f| Value{ .float32_val = -f }, else => return error.TypeError, }); }, // Comparison .eq => try self.execComparison(.eq), .neq => try self.execComparison(.neq), .lt => try self.execComparison(.lt), .lte => try self.execComparison(.lte), .gt => try self.execComparison(.gt), .gte => try self.execComparison(.gte), .not => { const v = try self.pop(); try self.push(.{ .bool_val = !v.isTruthy() }); }, // Control flow .jump => |offset| { frame.ip = @intCast(@as(i64, frame.ip) + offset); }, .jump_if_false => |offset| { const v = try self.pop(); if (!v.isTruthy()) { frame.ip = @intCast(@as(i64, frame.ip) + offset); } }, .jump_if_true => |offset| { const v = try self.pop(); if (v.isTruthy()) { frame.ip = @intCast(@as(i64, frame.ip) + offset); } }, // Functions .call => |ci| { try self.callFunction(ci.func_name, ci.arg_count); }, .call_builtin => |bi| { try self.callBuiltin(bi.id, bi.arg_count); }, .ret => { const result = try self.pop(); if (self.fp <= 1) return result; // Pop frame self.fp -= 1; self.sp = frame.base_slot; try self.push(result); }, .ret_void => { if (self.fp <= 1) return .{ .void_val = {} }; self.fp -= 1; self.sp = frame.base_slot; try self.push(.{ .void_val = {} }); }, // Structs .make_struct => |sm| { const fields = try self.allocator.alloc(Value, sm.field_count); var i: u16 = sm.field_count; while (i > 0) { i -= 1; fields[i] = try self.pop(); } try self.push(.{ .struct_val = .{ .type_name = sm.type_name, .field_names = sm.field_names, .fields = fields } }); }, .get_field => |idx| { const raw_obj = try self.pop(); // Auto-deref pointer const obj = if (raw_obj == .pointer_val) raw_obj.pointer_val.target[0] else raw_obj; if (obj == .struct_val) { if (idx < obj.struct_val.fields.len) { try self.push(obj.struct_val.fields[idx]); } else { try self.push(.{ .void_val = {} }); } } else if (obj == .string_val) { // String slice: field 0 = ptr (return string itself), field 1 = len if (idx == 1) { try self.push(.{ .int_val = @intCast(obj.string_val.len) }); } else { try self.push(obj); // ptr → return string itself } } else { return error.TypeError; } }, .set_field => |idx| { const val = try self.pop(); const raw_obj = try self.pop(); if (raw_obj == .pointer_val) { // Auto-deref: mutate field in-place through pointer const target = raw_obj.pointer_val.target; if (target[0] == .struct_val) { const sv = target[0].struct_val; if (idx < sv.fields.len) { sv.fields[idx] = val; } } try self.push(raw_obj); // push pointer back } else if (raw_obj == .struct_val) { if (idx < raw_obj.struct_val.fields.len) { raw_obj.struct_val.fields[idx] = val; } try self.push(raw_obj); } else { return error.TypeError; } }, // Arrays .make_array => |count| { const elements = try self.allocator.alloc(Value, count); var i: u32 = count; while (i > 0) { i -= 1; elements[i] = try self.pop(); } try self.push(.{ .array_val = .{ .elements = elements } }); }, .get_index => { const idx_val = try self.pop(); const arr = try self.pop(); if (arr == .array_val) { const idx: usize = try idx_val.asIndex(); if (idx < arr.array_val.elements.len) { try self.push(arr.array_val.elements[idx]); } else { return error.IndexOutOfBounds; } } else if (arr == .string_val) { // String indexing: return byte as int const idx: usize = try idx_val.asIndex(); if (idx < arr.string_val.len) { try self.push(.{ .int_val = @intCast(arr.string_val[idx]) }); } else { return error.IndexOutOfBounds; } } else if (arr == .pointer_val) { // Many-pointer indexing: ptr[i] const idx: usize = try idx_val.asIndex(); try self.push(arr.pointer_val.target[idx]); } else { return error.TypeError; } }, .set_index => { const val = try self.pop(); const idx_val = try self.pop(); const arr = try self.pop(); if (arr == .array_val) { const idx: usize = try idx_val.asIndex(); if (idx < arr.array_val.elements.len) { arr.array_val.elements[idx] = val; } try self.push(arr); } else if (arr == .string_val) { // String index assignment: mutate byte const idx: usize = try idx_val.asIndex(); const byte_val: u8 = @intCast(val.asInt() orelse return error.TypeError); if (idx < arr.string_val.len) { const mutable = @constCast(arr.string_val); mutable[idx] = byte_val; } try self.push(arr); } else if (arr == .pointer_val) { // Many-pointer index assignment: ptr[i] = val const idx: usize = try idx_val.asIndex(); arr.pointer_val.target[idx] = val; try self.push(arr); } else { return error.TypeError; } }, // Strings .concat => { const b = try self.pop(); const a = try self.pop(); const sa = try a.format(self.allocator); const sb = try b.format(self.allocator); const result = try std.fmt.allocPrint(self.allocator, "{s}{s}", .{ sa, sb }); try self.push(.{ .string_val = result }); }, .format_to_string => { const v = try self.pop(); const s = try v.format(self.allocator); try self.push(.{ .string_val = s }); }, .cast => {}, } } } const ArithOp = enum { add_op, sub_op, mul_op, div_op, mod_op }; fn execArith(self: *VM, op: ArithOp) !void { const b = try self.pop(); const a = try self.pop(); try self.push(try self.arith(a, b, op)); } const CmpOp = enum { eq, neq, lt, lte, gt, gte }; fn execComparison(self: *VM, comptime op: CmpOp) !void { const b = try self.pop(); const a = try self.pop(); const result = switch (op) { .eq => self.valEqual(a, b), .neq => !self.valEqual(a, b), .lt => self.valLess(a, b), .lte => self.valLess(a, b) or self.valEqual(a, b), .gt => self.valLess(b, a), .gte => self.valLess(b, a) or self.valEqual(a, b), }; try self.push(.{ .bool_val = result }); } fn arith(self: *VM, a: Value, b: Value, op: ArithOp) !Value { _ = self; // Both int if (a == .int_val and b == .int_val) { const ai = a.int_val; const bi = b.int_val; return .{ .int_val = switch (op) { .add_op => ai + bi, .sub_op => ai - bi, .mul_op => ai * bi, .div_op => if (bi != 0) @divTrunc(ai, bi) else return error.DivisionByZero, .mod_op => if (bi != 0) @rem(ai, bi) else return error.DivisionByZero, } }; } // Both f32 if (a == .float32_val and b == .float32_val) { const af = a.float32_val; const bf = b.float32_val; return .{ .float32_val = switch (op) { .add_op => af + bf, .sub_op => af - bf, .mul_op => af * bf, .div_op => af / bf, .mod_op => @rem(af, bf), } }; } // Promote to f64 const af = a.asFloat() orelse return error.TypeError; const bf = b.asFloat() orelse return error.TypeError; return .{ .float_val = switch (op) { .add_op => af + bf, .sub_op => af - bf, .mul_op => af * bf, .div_op => af / bf, .mod_op => @rem(af, bf), } }; } /// Clone a value, allocating new backing storage for composites (structs/arrays) /// so the clone is independent. Does not follow pointers. fn cloneValue(self: *VM, val: Value) !Value { return switch (val) { .struct_val => |sv| { const new_fields = try self.allocator.alloc(Value, sv.fields.len); for (sv.fields, 0..) |f, i| { new_fields[i] = try self.cloneValue(f); } return .{ .struct_val = .{ .type_name = sv.type_name, .field_names = sv.field_names, .fields = new_fields } }; }, .array_val => |av| { const new_elements = try self.allocator.alloc(Value, av.elements.len); for (av.elements, 0..) |e, i| { new_elements[i] = try self.cloneValue(e); } return .{ .array_val = .{ .elements = new_elements } }; }, else => val, }; } fn valEqual(self: *VM, a: Value, b: Value) bool { _ = self; if (a == .int_val and b == .int_val) return a.int_val == b.int_val; if (a == .bool_val and b == .bool_val) return a.bool_val == b.bool_val; if (a == .string_val and b == .string_val) return std.mem.eql(u8, a.string_val, b.string_val); // Pointer comparison if (a == .null_val and b == .null_val) return true; if (a == .null_val or b == .null_val) return false; if (a == .pointer_val and b == .pointer_val) return a.pointer_val.target == b.pointer_val.target; // Float comparison const af = a.asFloat(); const bf = b.asFloat(); if (af != null and bf != null) return af.? == bf.?; return false; } fn valLess(self: *VM, a: Value, b: Value) bool { _ = self; if (a == .int_val and b == .int_val) return a.int_val < b.int_val; const af = a.asFloat(); const bf = b.asFloat(); if (af != null and bf != null) return af.? < bf.?; return false; } fn callFunction(self: *VM, name: []const u8, arg_count: u8) !void { // Look up chunk in cache if (self.functions.getPtr(name)) |ptr| { return self.invokeChunk(ptr, arg_count); } // On-demand compilation: find function AST in root_decls for (self.root_decls) |decl| { switch (decl.data) { .fn_decl => |fd| { if (std.mem.eql(u8, fd.name, name)) return self.compileFunctionAndInvoke(name, fd, arg_count); }, .namespace_decl => |ns| { for (ns.decls) |d| { if (d.data == .fn_decl and std.mem.eql(u8, d.data.fn_decl.name, name)) return self.compileFunctionAndInvoke(name, d.data.fn_decl, arg_count); } }, else => {}, } } return error.UndefinedFunction; } fn invokeChunk(self: *VM, chunk: *const Chunk, arg_count: u8) !void { if (self.fp >= 64) return error.StackOverflow; // Args are on the stack. Set up new frame. const base = self.sp - @as(u16, arg_count); self.frames[self.fp] = .{ .chunk = chunk, .ip = 0, .base_slot = base }; self.fp += 1; } fn callBuiltin(self: *VM, id: BuiltinId, arg_count: u8) !void { switch (id) { .write => { // write(str) — raw string output if (arg_count >= 1) { const val = try self.pop(); const str = try val.format(self.allocator); std.debug.print("{s}", .{str}); } try self.push(.{ .void_val = {} }); }, .print => { // print(fmt, args...) — positional {} formatting if (arg_count == 0) { try self.push(.{ .void_val = {} }); } else if (arg_count == 1) { // Single arg: just print it const val = try self.pop(); const str = try val.format(self.allocator); std.debug.print("{s}", .{str}); try self.push(.{ .void_val = {} }); } else { // Pop args in reverse order (stack is LIFO) var vals = std.ArrayList(Value).empty; var j: u8 = 0; while (j < arg_count) : (j += 1) { try vals.append(self.allocator, try self.pop()); } // vals[0] is last arg, vals[arg_count-1] is first (format string) const fmt_val = vals.items[arg_count - 1]; const fmt_str = try fmt_val.format(self.allocator); // Process format string with {} placeholders var out = std.ArrayList(u8).empty; var arg_idx: usize = 0; var fi: usize = 0; while (fi < fmt_str.len) { if (fi + 1 < fmt_str.len and fmt_str[fi] == '{' and fmt_str[fi + 1] == '}') { if (arg_idx < arg_count - 1) { // vals are in reverse: vals[arg_count-2] is first value arg, vals[0] is last const val_idx = arg_count - 2 - arg_idx; const formatted = try vals.items[val_idx].format(self.allocator); try out.appendSlice(self.allocator, formatted); arg_idx += 1; } fi += 2; } else if (fi + 1 < fmt_str.len and fmt_str[fi] == '{' and fmt_str[fi + 1] == '{') { try out.append(self.allocator, '{'); fi += 2; } else if (fi + 1 < fmt_str.len and fmt_str[fi] == '}' and fmt_str[fi + 1] == '}') { try out.append(self.allocator, '}'); fi += 2; } else { try out.append(self.allocator, fmt_str[fi]); fi += 1; } } std.debug.print("{s}", .{out.items}); try self.push(.{ .void_val = {} }); } }, .sqrt => { if (arg_count >= 1) { const val = try self.pop(); const f = val.asFloat() orelse return error.TypeError; try self.push(.{ .float_val = @sqrt(f) }); } else { try self.push(.{ .float_val = 0.0 }); } }, .size_of => { if (arg_count >= 1) { const val = try self.pop(); if (val == .type_val) { const size = sizeOfType(val.type_val, self.codegen); try self.push(.{ .int_val = @intCast(size) }); } else { try self.push(.{ .int_val = 0 }); } } else { try self.push(.{ .int_val = 0 }); } }, .cast => { // cast(Type, val) — explicit type conversion if (arg_count >= 2) { const val = try self.pop(); // second arg (value) const type_arg = try self.pop(); // first arg (type) const target_ty: Type = if (type_arg == .type_val) type_arg.type_val else .void_type; // Convert based on target type if (target_ty.isFloat()) { // Target is float — convert from int or other float switch (val) { .int_val => |v| try self.push(.{ .float_val = @floatFromInt(v) }), .float32_val => |v| try self.push(.{ .float_val = @as(f64, v) }), .float_val => try self.push(val), else => try self.push(val), } } else if (target_ty.isInt()) { // Target is int — convert from float switch (val) { .float_val => |v| try self.push(.{ .int_val = @intFromFloat(v) }), .float32_val => |v| try self.push(.{ .int_val = @intFromFloat(v) }), .int_val => try self.push(val), else => try self.push(val), } } else { try self.push(val); // pass through } } else { try self.push(.{ .int_val = 0 }); } }, .alloc => { // alloc(size) — allocate zeroed byte buffer, return as string if (arg_count >= 1) { const val = try self.pop(); const size: usize = if (val.asInt()) |v| @intCast(@max(0, v)) else 0; const buf = try self.allocator.alloc(u8, size); @memset(buf, 0); try self.push(.{ .string_val = buf }); } else { try self.push(.{ .string_val = "" }); } }, } } /// Resolve a global variable by name. Checks the globals cache first, /// then searches root_decls for matching const_decl/var_decl and evaluates. const VMError = error{ CompileError, UndefinedVariable, UndefinedFunction, InvalidGlobal, InvalidCallee, TypeError, StackOverflow, StackUnderflow, IndexOutOfBounds, DivisionByZero, NullDereference, UnsupportedExpression, OutOfMemory, }; /// Evaluate a chunk in a fresh VM to avoid corrupting this VM's state. fn evalInFreshVM(self: *VM, chunk: *const Chunk) VMError!Value { var nested_vm = VM.init(self.allocator, self.sema_result, self.root_decls, self.codegen); // Share the globals cache so nested evaluations see already-resolved globals nested_vm.globals = self.globals; const result = nested_vm.execute(chunk) catch return error.CompileError; // Copy back any new globals that were resolved during nested evaluation self.globals = nested_vm.globals; return result; } fn cacheTypeGlobal(self: *VM, name: []const u8, ty: Type) VMError!Value { const val = Value{ .type_val = ty }; try self.globals.put(name, val); return val; } fn compileAndEvalGlobal(self: *VM, name: []const u8, expr: *Node) VMError!Value { var compiler = Compiler.init(self.allocator, self.sema_result, self.root_decls, self.codegen); const chunk = compiler.compile(expr) catch return error.CompileError; const result = self.evalInFreshVM(&chunk) catch return error.CompileError; try self.globals.put(name, result); return result; } fn compileFunctionAndInvoke(self: *VM, name: []const u8, fd: ast.FnDecl, arg_count: u8) !void { var compiler = Compiler.init(self.allocator, self.sema_result, self.root_decls, self.codegen); const chunk = try compiler.compileFunction(fd); try self.functions.put(name, chunk); if (self.functions.getPtr(name)) |ptr| { return self.invokeChunk(ptr, arg_count); } } fn resolveGlobal(self: *VM, name: []const u8) VMError!Value { // Check cache first if (self.globals.get(name)) |val| return val; // Search root_decls for matching declaration for (self.root_decls) |decl| { switch (decl.data) { .const_decl => |cd| { if (std.mem.eql(u8, cd.name, name)) { return self.compileAndEvalGlobal(name, cd.value); } }, .var_decl => |vd| { if (std.mem.eql(u8, vd.name, name)) { if (vd.value) |val_expr| { return self.compileAndEvalGlobal(name, val_expr); } return .{ .void_val = {} }; } }, .namespace_decl => |ns| { // Check inside namespace for matching declarations for (ns.decls) |d| { if (d.data == .const_decl and std.mem.eql(u8, d.data.const_decl.name, name)) { return self.compileAndEvalGlobal(name, d.data.const_decl.value); } } }, else => {}, } } // Check for struct/enum/union type declarations for (self.root_decls) |decl| { switch (decl.data) { .struct_decl => |sd| { if (std.mem.eql(u8, sd.name, name)) return self.cacheTypeGlobal(name, .{ .struct_type = name }); }, .enum_decl => |ed| { if (std.mem.eql(u8, ed.name, name)) { const ty: Type = if (ed.variant_types.len > 0) .{ .union_type = name } else .{ .enum_type = name }; return self.cacheTypeGlobal(name, ty); } }, .union_decl => |ud| { if (std.mem.eql(u8, ud.name, name)) return self.cacheTypeGlobal(name, .{ .union_type = name }); }, else => {}, } } // Check if it's a primitive type name (s32, f64, bool, etc.) if (Type.fromName(name)) |ty| return self.cacheTypeGlobal(name, ty); return error.UndefinedVariable; } }; test "Value: basic operations" { const a = Value{ .int_val = 42 }; try std.testing.expect(a.isInt()); try std.testing.expect(!a.isFloat()); try std.testing.expectEqual(@as(i64, 42), a.asInt().?); try std.testing.expectEqual(@as(f64, 42.0), a.asFloat().?); const b = Value{ .float_val = 3.14 }; try std.testing.expect(!b.isInt()); try std.testing.expect(b.isFloat()); try std.testing.expectEqual(@as(f64, 3.14), b.asFloat().?); const c = Value{ .bool_val = true }; try std.testing.expectEqual(@as(i64, 1), c.asInt().?); } const parser_mod = @import("parser.zig"); fn compileAndRun(source: [:0]const u8) !Value { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var p = parser_mod.Parser.init(alloc, source); const expr = try p.parseExpr(); var compiler = Compiler.init(alloc, null, &.{}, null); const chunk = try compiler.compile(expr); var vm = VM.init(alloc, null, &.{}, null); return vm.execute(&chunk); } test "VM: 2 + 3 = 5" { const result = try compileAndRun("2 + 3"); try std.testing.expectEqual(@as(i64, 5), result.int_val); } test "VM: arithmetic operations" { // subtraction const r1 = try compileAndRun("10 - 3"); try std.testing.expectEqual(@as(i64, 7), r1.int_val); // multiplication const r2 = try compileAndRun("6 * 7"); try std.testing.expectEqual(@as(i64, 42), r2.int_val); // division const r3 = try compileAndRun("20 / 4"); try std.testing.expectEqual(@as(i64, 5), r3.int_val); // negation const r4 = try compileAndRun("-42"); try std.testing.expectEqual(@as(i64, -42), r4.int_val); } test "VM: comparison operations" { const r1 = try compileAndRun("3 == 3"); try std.testing.expectEqual(true, r1.bool_val); const r2 = try compileAndRun("3 != 4"); try std.testing.expectEqual(true, r2.bool_val); const r3 = try compileAndRun("2 < 5"); try std.testing.expectEqual(true, r3.bool_val); const r4 = try compileAndRun("5 > 2"); try std.testing.expectEqual(true, r4.bool_val); } test "VM: boolean literals" { const r1 = try compileAndRun("true"); try std.testing.expectEqual(true, r1.bool_val); const r2 = try compileAndRun("false"); try std.testing.expectEqual(false, r2.bool_val); const r3 = try compileAndRun("!false"); try std.testing.expectEqual(true, r3.bool_val); } test "VM: float arithmetic" { const r1 = try compileAndRun("1.5 + 2.5"); try std.testing.expectEqual(@as(f64, 4.0), r1.float_val); const r2 = try compileAndRun("3.0 * 2.0"); try std.testing.expectEqual(@as(f64, 6.0), r2.float_val); } test "VM: if expression" { const r1 = try compileAndRun("if true then 1 else 2"); try std.testing.expectEqual(@as(i64, 1), r1.int_val); const r2 = try compileAndRun("if false then 1 else 2"); try std.testing.expectEqual(@as(i64, 2), r2.int_val); } test "VM: block with variables" { // Parse a block expression: { x := 5; y := x + 3; y; } var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); // Parse a block as a statement sequence var p = parser_mod.Parser.init(alloc, "{ x := 5; y := x + 3; y; }"); const expr = try p.parseExpr(); var compiler = Compiler.init(alloc, null, &.{}, null); const chunk = try compiler.compile(expr); var vm = VM.init(alloc, null, &.{}, null); const result = try vm.execute(&chunk); try std.testing.expectEqual(@as(i64, 8), result.int_val); } test "VM: nested if with variables" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var p = parser_mod.Parser.init(alloc, "{ x := 10; if x > 5 then 1 else 0; }"); const expr = try p.parseExpr(); var compiler = Compiler.init(alloc, null, &.{}, null); const chunk = try compiler.compile(expr); var vm = VM.init(alloc, null, &.{}, null); const result = try vm.execute(&chunk); try std.testing.expectEqual(@as(i64, 1), result.int_val); } /// Helper to compile and run a full program, executing a specific expression /// after all declarations are registered. fn compileAndRunProgram(source: [:0]const u8, expr_source: [:0]const u8) !Value { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); // Parse the full program to get root decls var prog_parser = parser_mod.Parser.init(alloc, source); const root = try prog_parser.parse(); const decls = root.data.root.decls; // Parse the expression to evaluate var expr_parser = parser_mod.Parser.init(alloc, expr_source); const expr = try expr_parser.parseExpr(); var compiler = Compiler.init(alloc, null, decls, null); const chunk = try compiler.compile(expr); var vm = VM.init(alloc, null, decls, null); return vm.execute(&chunk); } test "VM: function call" { const result = try compileAndRunProgram( "add :: (a: s32, b: s32) -> s32 { a + b; }", "add(2, 3)", ); try std.testing.expectEqual(@as(i64, 5), result.int_val); } test "VM: nested function calls" { const result = try compileAndRunProgram( "double :: (x: s32) -> s32 { x * 2; } quad :: (x: s32) -> s32 { double(double(x)); }", "quad(3)", ); try std.testing.expectEqual(@as(i64, 12), result.int_val); } test "VM: match expression" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); // Match on integer value var p = parser_mod.Parser.init(alloc, "{ x := 2; if x == { case 1: 10; case 2: 20; case 3: 30; } }"); const expr = try p.parseExpr(); var compiler = Compiler.init(alloc, null, &.{}, null); const chunk = try compiler.compile(expr); var vm = VM.init(alloc, null, &.{}, null); const result = try vm.execute(&chunk); try std.testing.expectEqual(@as(i64, 20), result.int_val); } test "VM: builtin sqrt" { const result = try compileAndRun("sqrt(16.0)"); try std.testing.expectEqual(@as(f64, 4.0), result.float_val); } test "VM: struct literal and field access" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); // Manually build a chunk that creates a struct and reads field 1 const code = [_]Instruction{ .{ .push_int = 10 }, // field 0: x .{ .push_int = 20 }, // field 1: y .{ .make_struct = .{ .type_name = "Point", .field_count = 2, .field_names = &.{ "x", "y" } } }, .{ .get_field = 1 }, // get y }; const chunk = Chunk{ .code = &code, .strings = &.{}, .local_count = 0, .name = "", }; var vm = VM.init(alloc, null, &.{}, null); const result = try vm.execute(&chunk); try std.testing.expectEqual(@as(i64, 20), result.int_val); } test "VM: array literal and index" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); // Manually build: make_array([10, 20, 30]), get_index(1) const code = [_]Instruction{ .{ .push_int = 10 }, .{ .push_int = 20 }, .{ .push_int = 30 }, .{ .make_array = 3 }, .{ .push_int = 1 }, // index .get_index, }; const chunk = Chunk{ .code = &code, .strings = &.{}, .local_count = 0, .name = "", }; var vm = VM.init(alloc, null, &.{}, null); const result = try vm.execute(&chunk); try std.testing.expectEqual(@as(i64, 20), result.int_val); } test "VM: string concat" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); const strings = [_][]const u8{ "hello ", "world" }; const code = [_]Instruction{ .{ .push_string = 0 }, .{ .push_string = 1 }, .concat, }; const chunk = Chunk{ .code = &code, .strings = &strings, .local_count = 0, .name = "", }; var vm = VM.init(alloc, null, &.{}, null); const result = try vm.execute(&chunk); try std.testing.expectEqualStrings("hello world", result.string_val); } test "VM: type value" { const result = try compileAndRun("f64"); try std.testing.expect(result == .type_val); } test "VM: function with return statement" { const result = try compileAndRunProgram( "compute :: (x: s32) -> s32 { return x * x; }", "compute(6)", ); try std.testing.expectEqual(@as(i64, 36), result.int_val); } test "VM: address-of and deref" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); // x := 42; ptr := @x; ptr.* const code = [_]Instruction{ .{ .push_int = 42 }, .{ .set_local = 0 }, // x = 42 .{ .address_of_local = 0 }, // &x .{ .set_local = 1 }, // ptr = &x .{ .get_local = 1 }, // ptr .deref, // ptr.* }; const chunk = Chunk{ .code = &code, .strings = &.{}, .local_count = 2, .name = "", }; var vm = VM.init(alloc, null, &.{}, null); const result = try vm.execute(&chunk); try std.testing.expectEqual(@as(i64, 42), result.int_val); } test "VM: deref_set modifies through pointer" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); // x := 10; ptr := @x; ptr.* = 99; x const code = [_]Instruction{ .{ .push_int = 10 }, .{ .set_local = 0 }, // x = 10 .{ .address_of_local = 0 }, // &x .{ .set_local = 1 }, // ptr = &x .{ .get_local = 1 }, // ptr .{ .push_int = 99 }, .deref_set, // ptr.* = 99 .{ .get_local = 0 }, // x (should be 99 now) }; const chunk = Chunk{ .code = &code, .strings = &.{}, .local_count = 2, .name = "", }; var vm = VM.init(alloc, null, &.{}, null); const result = try vm.execute(&chunk); try std.testing.expectEqual(@as(i64, 99), result.int_val); } test "VM: null pointer" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); const code = [_]Instruction{ .push_null, }; const chunk = Chunk{ .code = &code, .strings = &.{}, .local_count = 0, .name = "", }; var vm = VM.init(alloc, null, &.{}, null); const result = try vm.execute(&chunk); try std.testing.expect(result == .null_val); } test "VM: pointer to struct field access" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); // Build: struct{x: 10, y: 20}, @struct, get_field(1) — auto-deref const code = [_]Instruction{ .{ .push_int = 10 }, .{ .push_int = 20 }, .{ .make_struct = .{ .type_name = "Point", .field_count = 2, .field_names = &.{ "x", "y" } } }, .{ .set_local = 0 }, // v = Point{10, 20} .{ .address_of_local = 0 }, // &v .{ .get_field = 1 }, // auto-deref, get y }; const chunk = Chunk{ .code = &code, .strings = &.{}, .local_count = 1, .name = "", }; var vm = VM.init(alloc, null, &.{}, null); const result = try vm.execute(&chunk); try std.testing.expectEqual(@as(i64, 20), result.int_val); } test "VM: pointer to struct field mutation" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); // v = Point{10, 20}; ptr = &v; ptr.x = 99; v.x const code = [_]Instruction{ .{ .push_int = 10 }, .{ .push_int = 20 }, .{ .make_struct = .{ .type_name = "Point", .field_count = 2, .field_names = &.{ "x", "y" } } }, .{ .set_local = 0 }, // v = Point{10, 20} .{ .address_of_local = 0 }, // &v .{ .set_local = 1 }, // ptr = &v .{ .get_local = 1 }, // ptr .{ .push_int = 99 }, .{ .set_field = 0 }, // ptr.x = 99 (auto-deref) .{ .set_local = 1 }, // store ptr back .{ .get_local = 0 }, // v .{ .get_field = 0 }, // v.x }; const chunk = Chunk{ .code = &code, .strings = &.{}, .local_count = 2, .name = "", }; var vm = VM.init(alloc, null, &.{}, null); const result = try vm.execute(&chunk); try std.testing.expectEqual(@as(i64, 99), result.int_val); } test "VM: many-pointer indexing" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); // arr = [10, 20, 30]; mp = &arr[0]; mp[2] const code = [_]Instruction{ .{ .push_int = 10 }, .{ .push_int = 20 }, .{ .push_int = 30 }, .{ .make_array = 3 }, .{ .set_local = 0 }, // arr = [10, 20, 30] .{ .get_local = 0 }, // arr .{ .push_int = 0 }, .address_of_index, // &arr[0] .{ .set_local = 1 }, // mp = &arr[0] .{ .get_local = 1 }, // mp .{ .push_int = 2 }, .get_index, // mp[2] }; const chunk = Chunk{ .code = &code, .strings = &.{}, .local_count = 2, .name = "", }; var vm = VM.init(alloc, null, &.{}, null); const result = try vm.execute(&chunk); try std.testing.expectEqual(@as(i64, 30), result.int_val); }