Files
sx/src/comptime.zig
2026-02-11 01:05:21 +02:00

2090 lines
82 KiB
Zig

const std = @import("std");
const types = @import("types.zig");
const Type = types.Type;
/// 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 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, "<fn {s}>", .{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,
// 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 = "<expr>",
};
}
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 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;
}
/// Process escape sequences in a raw string literal.
fn unescapeString(allocator: std.mem.Allocator, raw: []const u8) ![]u8 {
var result = try allocator.alloc(u8, raw.len);
var i: usize = 0;
var j: usize = 0;
while (i < raw.len) {
if (raw[i] == '\\' and i + 1 < raw.len) {
i += 1;
switch (raw[i]) {
'n' => result[j] = '\n',
't' => result[j] = '\t',
'r' => result[j] = '\r',
'\\' => result[j] = '\\',
'"' => result[j] = '"',
'0' => result[j] = 0,
else => result[j] = raw[i],
}
j += 1;
i += 1;
} else {
result[j] = raw[i];
j += 1;
i += 1;
}
}
return result[0..j];
}
/// 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 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.instructions.items[jump_idx] = .{
.jump_if_false = @intCast(@as(i64, @intCast(self.instructions.items.len)) - @as(i64, @intCast(jump_idx)) - 1),
};
} 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.instructions.items[jump_idx] = .{
.jump_if_true = @intCast(@as(i64, @intCast(self.instructions.items.len)) - @as(i64, @intCast(jump_idx)) - 1),
};
} 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,
.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.instructions.items[jump_idx] = .{
.jump_if_false = @intCast(@as(i64, @intCast(self.instructions.items.len)) - @as(i64, @intCast(jump_idx)) - 1),
};
}
},
.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
// Patch jump_if_false to here
self.instructions.items[jump_false_idx] = .{ .jump_if_false = @intCast(self.instructions.items.len - jump_false_idx - 1) };
try self.compileNode(eb);
// Patch jump to end
self.instructions.items[jump_end_idx] = .{ .jump = @intCast(self.instructions.items.len - jump_end_idx - 1) };
} else {
// Patch jump_if_false to here
self.instructions.items[jump_false_idx] = .{ .jump_if_false = @intCast(self.instructions.items.len - jump_false_idx - 1) };
}
},
.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.mem.eql(u8, base, "print")) {
try self.emit(.{ .call_builtin = .{ .id = .print, .arg_count = @intCast(call_node.args.len) } });
} else if (std.mem.eql(u8, base, "write")) {
try self.emit(.{ .call_builtin = .{ .id = .write, .arg_count = @intCast(call_node.args.len) } });
} else if (std.mem.eql(u8, base, "sqrt")) {
try self.emit(.{ .call_builtin = .{ .id = .sqrt, .arg_count = @intCast(call_node.args.len) } });
} else if (std.mem.eql(u8, base, "size_of")) {
try self.emit(.{ .call_builtin = .{ .id = .size_of, .arg_count = @intCast(call_node.args.len) } });
} else if (std.mem.eql(u8, base, "cast")) {
try self.emit(.{ .call_builtin = .{ .id = .cast, .arg_count = @intCast(call_node.args.len) } });
} else if (std.mem.eql(u8, base, "alloc")) {
try self.emit(.{ .call_builtin = .{ .id = .alloc, .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
// Patch jump_if_false
self.instructions.items[jump_next_idx] = .{ .jump_if_false = @intCast(self.instructions.items.len - jump_next_idx - 1) };
} 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.instructions.items[idx] = .{ .jump = @intCast(self.instructions.items.len - idx - 1) };
}
},
.struct_literal => |sl| {
for (sl.field_inits) |fi| {
try self.compileNode(fi.value);
}
const name = sl.struct_name orelse "<anon>";
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
const after_loop = self.instructions.items.len;
self.instructions.items[jump_false_idx] = .{ .jump_if_false = @intCast(after_loop - jump_false_idx - 1) };
// Patch all break jumps to after the loop
for (self.break_patches.items) |patch_idx| {
self.instructions.items[patch_idx] = .{ .jump = @as(i32, @intCast(after_loop)) - @as(i32, @intCast(patch_idx)) - 1 };
}
// 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 = @intCast(idx_val.asInt() orelse return error.TypeError);
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 => {
const b = try self.pop();
const a = try self.pop();
try self.push(try self.arith(a, b, .add_op));
},
.sub => {
const b = try self.pop();
const a = try self.pop();
try self.push(try self.arith(a, b, .sub_op));
},
.mul => {
const b = try self.pop();
const a = try self.pop();
try self.push(try self.arith(a, b, .mul_op));
},
.div => {
const b = try self.pop();
const a = try self.pop();
try self.push(try self.arith(a, b, .div_op));
},
.mod => {
const b = try self.pop();
const a = try self.pop();
try self.push(try self.arith(a, b, .mod_op));
},
.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 => {
const b = try self.pop();
const a = try self.pop();
try self.push(.{ .bool_val = self.valEqual(a, b) });
},
.neq => {
const b = try self.pop();
const a = try self.pop();
try self.push(.{ .bool_val = !self.valEqual(a, b) });
},
.lt => {
const b = try self.pop();
const a = try self.pop();
try self.push(.{ .bool_val = self.valLess(a, b) });
},
.lte => {
const b = try self.pop();
const a = try self.pop();
try self.push(.{ .bool_val = self.valLess(a, b) or self.valEqual(a, b) });
},
.gt => {
const b = try self.pop();
const a = try self.pop();
try self.push(.{ .bool_val = self.valLess(b, a) });
},
.gte => {
const b = try self.pop();
const a = try self.pop();
try self.push(.{ .bool_val = self.valLess(b, a) or self.valEqual(a, b) });
},
.not => {
const v = try self.pop();
const b = switch (v) {
.bool_val => |bv| bv,
.int_val => |iv| iv != 0,
else => true,
};
try self.push(.{ .bool_val = !b });
},
// Control flow
.jump => |offset| {
frame.ip = @intCast(@as(i64, frame.ip) + offset);
},
.jump_if_false => |offset| {
const v = try self.pop();
const is_true = switch (v) {
.bool_val => |bv| bv,
.int_val => |iv| iv != 0,
else => true,
};
if (!is_true) {
frame.ip = @intCast(@as(i64, frame.ip) + offset);
}
},
.jump_if_true => |offset| {
const v = try self.pop();
const is_true = switch (v) {
.bool_val => |bv| bv,
.int_val => |iv| iv != 0,
else => true,
};
if (is_true) {
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 = @intCast(idx_val.asInt() orelse return error.TypeError);
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 = @intCast(idx_val.asInt() orelse return error.TypeError);
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 = @intCast(idx_val.asInt() orelse return error.TypeError);
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 = @intCast(idx_val.asInt() orelse return error.TypeError);
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 = @intCast(idx_val.asInt() orelse return error.TypeError);
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 = @intCast(idx_val.asInt() orelse return error.TypeError);
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
.cast => {
// TODO: implement type casting
},
}
}
}
const ArithOp = enum { add_op, sub_op, mul_op, div_op, mod_op };
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)) {
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);
}
}
},
.namespace_decl => |ns| {
for (ns.decls) |d| {
if (d.data == .fn_decl) {
if (std.mem.eql(u8, d.data.fn_decl.name, name)) {
var compiler = Compiler.init(self.allocator, self.sema_result, self.root_decls, self.codegen);
const chunk = try compiler.compileFunction(d.data.fn_decl);
try self.functions.put(name, chunk);
if (self.functions.getPtr(name)) |ptr| {
return self.invokeChunk(ptr, 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 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)) {
var compiler = Compiler.init(self.allocator, self.sema_result, self.root_decls, self.codegen);
const chunk = compiler.compile(cd.value) catch return error.CompileError;
const result = self.evalInFreshVM(&chunk) catch return error.CompileError;
try self.globals.put(name, result);
return result;
}
},
.var_decl => |vd| {
if (std.mem.eql(u8, vd.name, name)) {
if (vd.value) |val_expr| {
var compiler = Compiler.init(self.allocator, self.sema_result, self.root_decls, self.codegen);
const chunk = compiler.compile(val_expr) catch return error.CompileError;
const result = self.evalInFreshVM(&chunk) catch return error.CompileError;
try self.globals.put(name, result);
return result;
}
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)) {
var compiler = Compiler.init(self.allocator, self.sema_result, self.root_decls, self.codegen);
const chunk = compiler.compile(d.data.const_decl.value) catch return error.CompileError;
const result = self.evalInFreshVM(&chunk) catch return error.CompileError;
try self.globals.put(name, result);
return result;
}
}
},
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)) {
const val = Value{ .type_val = .{ .struct_type = name } };
try self.globals.put(name, val);
return val;
}
},
.enum_decl => |ed| {
if (std.mem.eql(u8, ed.name, name)) {
const val = Value{ .type_val = .{ .enum_type = name } };
try self.globals.put(name, val);
return val;
}
},
.union_decl => |ud| {
if (std.mem.eql(u8, ud.name, name)) {
const val = Value{ .type_val = .{ .union_type = name } };
try self.globals.put(name, val);
return val;
}
},
else => {},
}
}
// Check if it's a primitive type name (s32, f64, bool, etc.)
if (Type.fromName(name)) |ty| {
const val = Value{ .type_val = ty };
try self.globals.put(name, val);
return val;
}
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 = "<test>",
};
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 = "<test>",
};
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 = "<test>",
};
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 = "<test>",
};
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 = "<test>",
};
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 = "<test>",
};
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 = "<test>",
};
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 = "<test>",
};
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 = "<test>",
};
var vm = VM.init(alloc, null, &.{}, null);
const result = try vm.execute(&chunk);
try std.testing.expectEqual(@as(i64, 30), result.int_val);
}