Skip to content

Commit ff9b7db

Browse files
committed
Add enum variant constructors to VM
Close #50
1 parent cb799eb commit ff9b7db

4 files changed

Lines changed: 271 additions & 4 deletions

File tree

src/tests/bytecode_vm_test.zig

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,3 +1172,144 @@ test "compiler: nested field access a.b.c" {
11721172
const result = try runCompiled(allocator, &.{ point_decl, line_decl, p_decl, l_decl }, .{ .field_access = fa_x });
11731173
try std.testing.expect(result.eql(.{ .int = 3 }));
11741174
}
1175+
1176+
// ===========================================================================
1177+
// Step 7: Enum variant constructors
1178+
// ===========================================================================
1179+
1180+
test "compiler: enum zero-field variant construction" {
1181+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
1182+
defer arena.deinit();
1183+
const allocator = arena.allocator();
1184+
1185+
// enum Color { Red, Green }
1186+
const variants = [_]ast.Statement.Variant{
1187+
.{ .name = ident("Red"), .fields = &.{} },
1188+
.{ .name = ident("Green"), .fields = &.{} },
1189+
};
1190+
const enum_decl = ast.Statement{ .enum_declaration = .{
1191+
.name = ident("Color"),
1192+
.is_error = false,
1193+
.variants = &variants,
1194+
} };
1195+
1196+
// Color.Red()
1197+
const fa = try allocator.create(ast.Expression.FieldAccess);
1198+
fa.* = .{ .object = .{ .variable = .{ .token = ident("Color") } }, .name = ident("Red") };
1199+
const call = try allocator.create(ast.Expression.FnCall);
1200+
call.* = .{ .callee = .{ .field_access = fa }, .args = &.{} };
1201+
1202+
const result = try runCompiled(allocator, &.{enum_decl}, .{ .fn_call = call });
1203+
try std.testing.expect(result == .struct_instance);
1204+
try std.testing.expectEqualStrings("Color.Red", result.struct_instance.type_name);
1205+
try std.testing.expectEqual(@as(usize, 0), result.struct_instance.field_values.len);
1206+
}
1207+
1208+
test "compiler: enum variant with fields" {
1209+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
1210+
defer arena.deinit();
1211+
const allocator = arena.allocator();
1212+
1213+
// enum Shape { Circle(const radius: Int) }
1214+
const circle_fields = [_]ast.Statement.FieldDeclaration{
1215+
.{ .name = ident("radius"), .type_annotation = dummyType(), .mutability = .constant, .default_value = null },
1216+
};
1217+
const variants = [_]ast.Statement.Variant{
1218+
.{ .name = ident("Circle"), .fields = &circle_fields },
1219+
};
1220+
const enum_decl = ast.Statement{ .enum_declaration = .{
1221+
.name = ident("Shape"),
1222+
.is_error = false,
1223+
.variants = &variants,
1224+
} };
1225+
1226+
// Shape.Circle(5)
1227+
const fa = try allocator.create(ast.Expression.FieldAccess);
1228+
fa.* = .{ .object = .{ .variable = .{ .token = ident("Shape") } }, .name = ident("Circle") };
1229+
const args = [_]ast.Expression{
1230+
.{ .literal = .{ .token = ident("5"), .value = .{ .int = 5 } } },
1231+
};
1232+
const call = try allocator.create(ast.Expression.FnCall);
1233+
call.* = .{ .callee = .{ .field_access = fa }, .args = &args };
1234+
1235+
const result = try runCompiled(allocator, &.{enum_decl}, .{ .fn_call = call });
1236+
try std.testing.expect(result == .struct_instance);
1237+
try std.testing.expectEqualStrings("Shape.Circle", result.struct_instance.type_name);
1238+
try std.testing.expectEqual(@as(usize, 1), result.struct_instance.field_values.len);
1239+
try std.testing.expect(result.struct_instance.field_values[0].eql(.{ .int = 5 }));
1240+
}
1241+
1242+
test "compiler: qualified enum access resolves to correct variant" {
1243+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
1244+
defer arena.deinit();
1245+
const allocator = arena.allocator();
1246+
1247+
// enum Color { Red, Green }
1248+
const variants = [_]ast.Statement.Variant{
1249+
.{ .name = ident("Red"), .fields = &.{} },
1250+
.{ .name = ident("Green"), .fields = &.{} },
1251+
};
1252+
const enum_decl = ast.Statement{ .enum_declaration = .{
1253+
.name = ident("Color"),
1254+
.is_error = false,
1255+
.variants = &variants,
1256+
} };
1257+
1258+
// Color.Green() — must resolve to Green, not Red
1259+
const fa = try allocator.create(ast.Expression.FieldAccess);
1260+
fa.* = .{ .object = .{ .variable = .{ .token = ident("Color") } }, .name = ident("Green") };
1261+
const call = try allocator.create(ast.Expression.FnCall);
1262+
call.* = .{ .callee = .{ .field_access = fa }, .args = &.{} };
1263+
1264+
const result = try runCompiled(allocator, &.{enum_decl}, .{ .fn_call = call });
1265+
try std.testing.expect(result == .struct_instance);
1266+
try std.testing.expectEqualStrings("Color.Green", result.struct_instance.type_name);
1267+
}
1268+
1269+
test "compiler: nested enum coercion" {
1270+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
1271+
defer arena.deinit();
1272+
const allocator = arena.allocator();
1273+
1274+
// enum Inner { A }
1275+
const inner_variants = [_]ast.Statement.Variant{
1276+
.{ .name = ident("A"), .fields = &.{} },
1277+
};
1278+
const inner_decl = ast.Statement{ .enum_declaration = .{
1279+
.name = ident("Inner"),
1280+
.is_error = false,
1281+
.variants = &inner_variants,
1282+
} };
1283+
1284+
// enum Outer { Inner, B }
1285+
// "Inner" has zero declared fields but matches a known enum → single-field wrapper
1286+
const outer_variants = [_]ast.Statement.Variant{
1287+
.{ .name = ident("Inner"), .fields = &.{} },
1288+
.{ .name = ident("B"), .fields = &.{} },
1289+
};
1290+
const outer_decl = ast.Statement{ .enum_declaration = .{
1291+
.name = ident("Outer"),
1292+
.is_error = false,
1293+
.variants = &outer_variants,
1294+
} };
1295+
1296+
// Inner.A()
1297+
const inner_fa = try allocator.create(ast.Expression.FieldAccess);
1298+
inner_fa.* = .{ .object = .{ .variable = .{ .token = ident("Inner") } }, .name = ident("A") };
1299+
const inner_call = try allocator.create(ast.Expression.FnCall);
1300+
inner_call.* = .{ .callee = .{ .field_access = inner_fa }, .args = &.{} };
1301+
1302+
// Outer.Inner(Inner.A())
1303+
const outer_fa = try allocator.create(ast.Expression.FieldAccess);
1304+
outer_fa.* = .{ .object = .{ .variable = .{ .token = ident("Outer") } }, .name = ident("Inner") };
1305+
const outer_args = [_]ast.Expression{.{ .fn_call = inner_call }};
1306+
const outer_call = try allocator.create(ast.Expression.FnCall);
1307+
outer_call.* = .{ .callee = .{ .field_access = outer_fa }, .args = &outer_args };
1308+
1309+
const result = try runCompiled(allocator, &.{ inner_decl, outer_decl }, .{ .fn_call = outer_call });
1310+
try std.testing.expect(result == .struct_instance);
1311+
try std.testing.expectEqualStrings("Outer.Inner", result.struct_instance.type_name);
1312+
try std.testing.expectEqual(@as(usize, 1), result.struct_instance.field_values.len);
1313+
try std.testing.expect(result.struct_instance.field_values[0] == .struct_instance);
1314+
try std.testing.expectEqualStrings("Inner.A", result.struct_instance.field_values[0].struct_instance.type_name);
1315+
}

src/vm/compiler.zig

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ pub const Compiler = struct {
3131
chunk: *Chunk,
3232
locals: std.ArrayList(Local),
3333
scope_depth: u32,
34+
// Set of declared enum type names; used to detect when a zero-field variant references
35+
// an existing enum (nested enum coercion at runtime)
36+
known_enum_names: std.StringHashMapUnmanaged(void),
3437
allocator: std.mem.Allocator,
3538

3639
// Static entry point to compile statements to a Program
@@ -41,6 +44,7 @@ pub const Compiler = struct {
4144
.chunk = &program.chunk,
4245
.locals = .{},
4346
.scope_depth = 0,
47+
.known_enum_names = .{},
4448
.allocator = allocator,
4549
};
4650
errdefer compiler.deinit();
@@ -56,12 +60,14 @@ pub const Compiler = struct {
5660
.chunk = &program.chunk,
5761
.locals = .{},
5862
.scope_depth = 0,
63+
.known_enum_names = .{},
5964
.allocator = allocator,
6065
};
6166
}
6267

6368
pub fn deinit(self: *Compiler) void {
6469
self.locals.deinit(self.allocator);
70+
self.known_enum_names.deinit(self.allocator);
6571
}
6672

6773
// NOTE: -- Statements
@@ -81,8 +87,8 @@ pub const Compiler = struct {
8187
.var_declaration => |var_decl| try self.compileVarDeclarationStatement(var_decl),
8288
.fn_declaration => |fn_decl| try self.compileFnDeclarationStatement(fn_decl),
8389
.struct_declaration => |struct_decl| try self.compileStructDeclarationStatement(struct_decl),
90+
.enum_declaration => |ed| try self.compileEnumDeclarationStatement(ed),
8491
.@"return" => |ret| try self.compileReturnStatement(ret),
85-
else => return error.UnsupportedNode,
8692
}
8793
}
8894

@@ -170,6 +176,46 @@ pub const Compiler = struct {
170176
_ = try self.program.addStructDef(struct_obj);
171177
}
172178

179+
fn compileEnumDeclarationStatement(self: *Compiler, ed: ast.Statement.EnumDeclaration) CompileError!void {
180+
const enum_name = ed.name.lexeme;
181+
for (ed.variants) |variant| {
182+
// Build the qualified name EnumName.VariantName
183+
const variant_name = variant.name.lexeme;
184+
const type_name = try self.buildEnumVariantQualifiedName(enum_name, variant_name);
185+
186+
var field_names: [][]const u8 = undefined;
187+
188+
// Nesting an enum inside another?
189+
// e.g: enum StaffRole { Admin, Member };
190+
// enum AnyRole { StaffRole, Guest };
191+
// When compiling AnyRole, StaffRole is already defined.
192+
if (variant.fields.len == 0 and self.known_enum_names.contains(variant_name)) {
193+
// Rather than redefining the enum, just reference it
194+
const names = try self.allocator.alloc([]const u8, 1);
195+
names[0] = variant_name;
196+
field_names = names;
197+
} else {
198+
// Collect field_names
199+
field_names = try self.allocator.alloc([]const u8, variant.fields.len);
200+
for (variant.fields, 0..) |f, i| {
201+
field_names[i] = f.name.lexeme;
202+
}
203+
}
204+
205+
// The struct def is always a .case kind (for equality)
206+
_ = try self.program.addStructDef(.{
207+
.name = type_name,
208+
.kind = .case,
209+
.field_names = field_names,
210+
.body_field_names = &.{},
211+
.body_default_fn = null,
212+
});
213+
}
214+
215+
// Don't forget to add the enum to our known enum names
216+
try self.known_enum_names.put(self.allocator, enum_name, {});
217+
}
218+
173219
fn compileReturnStatement(self: *Compiler, ret: ast.Statement.Return) CompileError!void {
174220
if (ret.value) |value| {
175221
try self.compileExpression(value);
@@ -353,6 +399,22 @@ pub const Compiler = struct {
353399
}
354400

355401
fn compileFieldAccess(self: *Compiler, fa: *const ast.Expression.FieldAccess) CompileError!void {
402+
// Access an enum variant
403+
if (fa.object == .variable and self.known_enum_names.contains(fa.object.variable.token.lexeme)) {
404+
const type_name = try self.buildEnumVariantQualifiedName(fa.object.variable.token.lexeme, fa.name.lexeme);
405+
defer self.allocator.free(type_name);
406+
407+
// Search for a matching name
408+
for (self.program.struct_defs.items, 0..) |def, i| {
409+
if (std.mem.eql(u8, def.name, type_name)) {
410+
try self.emitConstant(.{ .struct_constructor = @intCast(i) }, fa.name.line);
411+
return;
412+
}
413+
}
414+
return error.UndefinedStruct;
415+
}
416+
417+
// Access a field
356418
try self.compileExpression(fa.object);
357419
const name_idx = try self.chunk.addConstant(.{ .string = fa.name.lexeme });
358420

@@ -482,4 +544,14 @@ pub const Compiler = struct {
482544

483545
return null;
484546
}
547+
548+
// Returns the qualified name EnumName.VariantName
549+
fn buildEnumVariantQualifiedName(self: *Compiler, enum_name: []const u8, variant_name: []const u8) CompileError![]const u8 {
550+
const type_name = try self.allocator.alloc(u8, enum_name.len + variant_name.len + 1);
551+
@memcpy(type_name[0..enum_name.len], enum_name);
552+
type_name[enum_name.len] = '.';
553+
@memcpy(type_name[enum_name.len + 1 ..], variant_name);
554+
555+
return type_name;
556+
}
485557
};

src/vm/value.zig

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,21 @@ pub const Value = union(enum) {
1616

1717
// Builtin function
1818
native: NativeFn,
19-
//
19+
2020
// pointer so that copies of a value share identity (u2 = u1 means u1.field == u2.field)
2121
// var u1 = User("Bob");
2222
// var u2 = u1;
2323
// if (u1.name == u2.name) { ... } // true
2424
struct_instance: *StructInstance,
2525

26+
// Index of a struct constructor
27+
// Stored in a struct defs table in Program
28+
//
29+
// This makes struct constructor first-class citizens:
30+
// enum Color { Red };
31+
// transform(Color.Red); // Constructor is passed as a variable
32+
struct_constructor: u16,
33+
2634
pub const StructInstance = struct {
2735
type_name: []const u8,
2836
field_names: []const []const u8,
@@ -71,6 +79,7 @@ pub const Value = union(enum) {
7179
}
7280
return true;
7381
},
82+
.struct_constructor => |a| a == other.struct_constructor,
7483
};
7584
}
7685

@@ -88,7 +97,11 @@ pub const Value = union(enum) {
8897
.boolean => |b| b,
8998
.int => |n| n != 0,
9099
.string => |s| s.len > 0,
91-
.function, .native, .struct_instance => true,
100+
.function,
101+
.native,
102+
.struct_instance,
103+
.struct_constructor,
104+
=> true,
92105
};
93106
}
94107

@@ -125,6 +138,7 @@ pub const Value = union(enum) {
125138
},
126139
}
127140
},
141+
.struct_constructor => |sc| try writer.print("constructor<{d}>", .{sc}),
128142
}
129143
}
130144
};

src/vm/vm.zig

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,13 +232,53 @@ pub const Vm = struct {
232232
},
233233
.native => {
234234
// Slice of the fn args
235-
const args = self.stack[base_slot + 1 .. base_slot + 1 + arity];
235+
const arg0_idx = base_slot + 1;
236+
const args = self.stack[arg0_idx .. arg0_idx + arity];
236237

237238
// Call builtin function, remove the args and push the return value
238239
const ret_value = callee.native.func(args, self.ctx);
239240
self.stack_top = base_slot;
240241
self.push(ret_value);
241242
},
243+
.struct_constructor => {
244+
// Stack: [ ..., constructor_value, arg0, arg1, ... ]
245+
// ^ base_slot
246+
const struct_def_idx = callee.struct_constructor;
247+
const def = self.program.struct_defs.items[struct_def_idx];
248+
249+
// Check arity
250+
if (def.field_names.len != arity) {
251+
return error.ArityMismatch;
252+
}
253+
254+
// Allocate args
255+
const arg0_idx = base_slot + 1;
256+
const args = self.stack[arg0_idx .. arg0_idx + def.field_names.len];
257+
var field_values = try self.allocator.alloc(Value, args.len);
258+
for (args, 0..) |arg, i| {
259+
field_values[i] = arg;
260+
}
261+
262+
// Allocate body args
263+
const body_field_values = try self.allocator.alloc(Value, def.body_field_names.len);
264+
for (body_field_values) |*v| {
265+
v.* = Value.unit;
266+
}
267+
268+
const instance = try self.allocator.create(Value.StructInstance);
269+
instance.* = .{
270+
.type_name = def.name,
271+
.field_names = def.field_names,
272+
.body_field_names = def.body_field_names,
273+
.field_values = field_values,
274+
.body_field_values = body_field_values,
275+
.kind = def.kind,
276+
};
277+
278+
// Reset stack top now that we consumed everything
279+
self.stack_top = base_slot;
280+
self.push(Value{ .struct_instance = instance });
281+
},
242282
else => return error.NotCallable,
243283
}
244284
},

0 commit comments

Comments
 (0)