2090 lines
82 KiB
Zig
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);
|
|
}
|