commit 4cc7e5f654996bb0fdd4e5834ad7cb93857334de
parent 6a04bf164625fc290aad19d49c88f28c9d44a408
Author: Szymon Mikulicz <szymon.mikulicz@posteo.net>
Date: Tue, 1 Sep 2026 18:54:54 +0200
Objects && Instances
Diffstat:
9 files changed, 229 insertions(+), 31 deletions(-)
diff --git a/zlox/src/chunk.zig b/zlox/src/chunk.zig
@@ -27,6 +27,8 @@ pub const OP = enum(u8) {
SET_GLOBAL,
GET_LOCAL,
SET_LOCAL,
+ GET_PROPERTY,
+ SET_PROPERTY,
GET_UPVALUE,
SET_UPVALUE,
JUMP_IF_FALSE,
diff --git a/zlox/src/compiler.zig b/zlox/src/compiler.zig
@@ -116,6 +116,8 @@ pub fn Compiler(size: comptime_int) type {
T.OR => R(null, S._or, P.OR ),
T.TRUE => R(S.literal, null, P.NONE ),
T.FUN => R(S.function, null, P.NONE ),
+ T.CLASS => R(S.class, null, P.NONE ),
+ T.DOT => R(null, S.dot, P.CALL ),
else => R(null, null, P.NONE ),
// zig fmt: on
};
@@ -269,6 +271,18 @@ pub fn Compiler(size: comptime_int) type {
self.emit(OP.CALL, argCount);
}
+ fn dot(self: *Self, canAssign: bool) void {
+ self.consume(Token.IDENTIFIER, "Expect property name after '.'");
+ const name = self.identifierConstant(self.previous) catch return;
+
+ if (canAssign and self.match(Token.EQUAL)) {
+ self.expression();
+ self.emit(OP.SET_PROPERTY, name);
+ } else {
+ self.emit(OP.GET_PROPERTY, name);
+ }
+ }
+
fn argumentList(self: *Self) u8 {
var argCount: u8 = 0;
if (!self.check(Token.RIGHT_PAREN)) {
@@ -514,7 +528,9 @@ pub fn Compiler(size: comptime_int) type {
}
fn declaration(self: *Self) void {
- if (self.match(Token.FUN)) {
+ if (self.match(Token.CLASS)) {
+ self.classDeclaration();
+ } else if (self.match(Token.FUN)) {
self.funDeclaration();
} else if (self.match(Token.VAR)) {
self.varDeclaration();
@@ -527,6 +543,26 @@ pub fn Compiler(size: comptime_int) type {
if (self.panicMode) self.synchronize();
}
+ fn classDeclaration(self: *Self) void {
+ const global = self.parseVariable("Expect class name.", true) catch return;
+ self.markInitialized();
+ self.class(false);
+ self.defineVariable(global, true);
+ }
+
+ fn class(self: *Self, _: bool) void {
+ self.consume(Token.LEFT_BRACE, "Expect '{' before class body");
+ self.consume(Token.RIGHT_BRACE, "Expect '}' after class body");
+
+ const cls = self.objects.emplace_cast(Obj.Type.Class, {}) catch |err| {
+ self.errorAtPrevious("Couldn't allocate class");
+ self.lastError = err;
+ return;
+ };
+
+ self.emit(OP.CONSTANT, self.makeConstant(Value.init(cls)));
+ }
+
fn funDeclaration(self: *Self) void {
const global = self.parseVariable("Expect function name.", true) catch return;
self.markInitialized();
diff --git a/zlox/src/debug.zig b/zlox/src/debug.zig
@@ -63,6 +63,8 @@ fn _disassembleInstruction(ch: *const chunk.Chunk, offset: usize, print_fn: bool
@intFromEnum(OP.SET_LOCAL) => try byteInstruction(name, ch, offset),
@intFromEnum(OP.GET_UPVALUE) => try byteInstruction(name, ch, offset),
@intFromEnum(OP.SET_UPVALUE) => try byteInstruction(name, ch, offset),
+ @intFromEnum(OP.GET_PROPERTY) => try byteInstruction(name, ch, offset),
+ @intFromEnum(OP.SET_PROPERTY) => try byteInstruction(name, ch, offset),
@intFromEnum(OP.JUMP_IF_FALSE) => try jumpInstruction(name, true, ch, offset),
@intFromEnum(OP.JUMP_POP) => simpleInstruction(name, offset),
@intFromEnum(OP.JUMP) => try jumpInstruction(name, true, ch, offset),
diff --git a/zlox/src/gc.zig b/zlox/src/gc.zig
@@ -88,8 +88,6 @@ pub const GC = struct {
const T = @TypeOf(obj);
switch (T) {
- *Obj.String => {},
- *Obj.Native => {},
*Obj.Table => {
obj.table.ptr().for_each(self, struct {
pub fn fun(s: *Self, key: Obj.Table.Table.Key, val: Obj.Table.Table.Value) void {
@@ -118,7 +116,16 @@ pub const GC = struct {
if (obj.closed)
self.mark("u", obj.location.get());
},
- else => @compileError("Invalid type: " ++ @typeName(T)),
+ *Obj.Instance => {
+ self.mark("i", obj.cls.ptr());
+ obj.fields.ptr().for_each(self, struct {
+ pub fn fun(s: *Self, key: Obj.Instance.Fields.Key, val: Obj.Instance.Fields.Value) void {
+ s.mark("i", key);
+ s.mark("i", val);
+ }
+ }.fun);
+ },
+ else => {},
}
}
diff --git a/zlox/src/obj.zig b/zlox/src/obj.zig
@@ -17,8 +17,20 @@ pub fn Obj(fields: anytype) type {
pub const Native = @import("obj::native.zig").Native(fields);
pub const Closure = @import("obj::closure.zig").Closure(fields);
pub const Upvalue = @import("obj::upvalue.zig").Upvalue(fields);
-
- pub const Error = error{IllegalCastError} || List.Error || String.Error || Table.Error || Function.Error || Native.Error || List.Error || Closure.Error || Upvalue.Error;
+ pub const Class = @import("obj::class.zig").Class(fields);
+ pub const Instance = @import("obj::instance.zig").Instance(fields);
+
+ pub const Error = error{IllegalCastError} //
+ || List.Error //
+ || String.Error //
+ || Table.Error //
+ || Function.Error //
+ || Native.Error //
+ || List.Error //
+ || Closure.Error //
+ || Upvalue.Error //
+ || Class.Error //
+ || Instance.Error;
pub const Type = enum(u8) {
String,
@@ -28,6 +40,8 @@ pub fn Obj(fields: anytype) type {
List,
Closure,
Upvalue,
+ Class,
+ Instance,
pub fn get(self: @This()) type {
return @field(Self, @tagName(self));
diff --git a/zlox/src/obj::class.zig b/zlox/src/obj::class.zig
@@ -0,0 +1,42 @@
+const std = @import("std");
+
+const utils = @import("lib::utils.zig");
+
+const Obj = @import("obj.zig").Obj;
+
+pub fn Class(fields: anytype) type {
+ const Super = Obj(fields);
+
+ return packed struct {
+ const Self = @This();
+
+ pub const Arg = void;
+ pub const Error = error{OutOfMemory};
+
+ obj: Super,
+
+ pub fn init(_: Arg, allocator: std.mem.Allocator) Error!*Self {
+ const self: *Self = try allocator.create(Self);
+ self.* = Self{
+ .obj = Super.make(Self),
+ };
+ return self;
+ }
+
+ pub fn cast(self: anytype) utils.copy_const(@TypeOf(self), *Super) {
+ return @ptrCast(self);
+ }
+
+ pub fn format(_: *const Self, writer: *std.Io.Writer) !void {
+ _ = try writer.write("<Class>");
+ }
+
+ pub fn eql(_: *const Self, _: *const Self) bool {
+ return false;
+ }
+
+ pub fn free(self: *const Self, allocator: std.mem.Allocator) void {
+ allocator.destroy(self);
+ }
+ };
+}
diff --git a/zlox/src/obj::instance.zig b/zlox/src/obj::instance.zig
@@ -0,0 +1,55 @@
+const std = @import("std");
+
+const utils = @import("lib::utils.zig");
+const table = @import("lib::table.zig");
+const hash = @import("hash.zig");
+
+const Packed = @import("lib::packed.zig").Packed;
+const Value = @import("value.zig").Value;
+const Obj = @import("obj.zig").Obj;
+
+pub fn Instance(fields: anytype) type {
+ const Super = Obj(fields);
+
+ return packed struct {
+ const Self = @This();
+
+ pub const Arg = *Super.Class;
+ pub const Error = error{OutOfMemory};
+
+ pub const Fields = table.Table(*Super.String, Value, hash.hash_t(*Super.String), Super.String.eql);
+
+ obj: Super,
+ cls: Packed(*Super.Class),
+ fields: Packed(*Fields),
+
+ pub fn init(cls: Arg, allocator: std.mem.Allocator) Error!*Self {
+ const self: *Self = try allocator.create(Self);
+ self.* = Self{
+ .obj = Super.make(Self),
+ .cls = Packed(*Super.Class).init(cls),
+ .fields = try Packed(*Self.Fields).create(allocator),
+ };
+ self.fields.set(Self.Fields.init(allocator));
+ return self;
+ }
+
+ pub fn cast(self: anytype) utils.copy_const(@TypeOf(self), *Super) {
+ return @ptrCast(self);
+ }
+
+ pub fn format(_: *const Self, writer: *std.Io.Writer) !void {
+ _ = try writer.write("<Instance>");
+ }
+
+ pub fn eql(_: *const Self, _: *const Self) bool {
+ return false;
+ }
+
+ pub fn free(self: *const Self, allocator: std.mem.Allocator) void {
+ self.fields.ptr().deinit();
+ self.fields.destroy(allocator);
+ allocator.destroy(self);
+ }
+ };
+}
diff --git a/zlox/src/obj::table.zig b/zlox/src/obj::table.zig
@@ -19,14 +19,12 @@ pub fn Table(fields: anytype) type {
obj: Super,
table: Packed(*Self.Table),
- len: usize,
pub fn init(_: Arg, allocator: std.mem.Allocator) Error!*Self {
const self: *Self = try allocator.create(Self);
self.* = Self{
.obj = Super.make(Self),
.table = try Packed(*Self.Table).create(allocator),
- .len = 0,
};
self.table.set(Self.Table.init(allocator));
return self;
@@ -37,7 +35,6 @@ pub fn Table(fields: anytype) type {
}
pub fn set(self: *Self, key: Value, val: Value) Error!bool {
- self.len += 1;
return self.table.ptr().set(key, val);
}
@@ -46,7 +43,7 @@ pub fn Table(fields: anytype) type {
}
pub fn delete(self: *Self, key: Value) void {
- if (self.table.ptr().delete(key)) self.len -= 1;
+ _ = self.table.ptr().delete(key);
}
pub fn format(self: *const Self, writer: *std.Io.Writer) !void {
@@ -64,7 +61,7 @@ pub fn Table(fields: anytype) type {
}
};
- var printer = Printer{ .writer = writer, .count = self.len };
+ var printer = Printer{ .writer = writer, .count = self.table.ptr().count };
_ = try writer.write("[");
if (self.table.ptr().count > 0) {
try self.table.ptr().for_each_try(&printer, Printer.print);
diff --git a/zlox/src/vm.zig b/zlox/src/vm.zig
@@ -225,19 +225,34 @@ pub const VM = struct {
return (self.stackTop - (1 + distance))[0];
}
+ fn pook(self: *@This(), distance: usize, val: Value) void {
+ (self.stackTop - (1 + distance))[0] = val;
+ }
+
fn callValue(self: *@This(), callee: Value, argCount: u8) !void {
- if (callee.is(Obj.Type.Function)) {
- try self.callFunction(callee.obj.cast(.Function) catch unreachable, argCount);
- } else if (callee.is(Obj.Type.Closure)) {
- try self.callClosure(callee.obj.cast(.Closure) catch unreachable, argCount);
- } else if (callee.is(Obj.Type.Native)) {
- try self.callNative(callee.obj.cast(.Native) catch unreachable, argCount);
+ if (callee.cast_if(Obj.Type.Function)) |fun| {
+ try self.callFunction(fun, argCount);
+ } else if (callee.cast_if(Obj.Type.Closure)) |clo| {
+ try self.callClosure(clo, argCount);
+ } else if (callee.cast_if(Obj.Type.Native)) |nat| {
+ try self.callNative(nat, argCount);
+ } else if (callee.cast_if(Obj.Type.Class)) |cls| {
+ try self.callClass(cls, argCount);
} else {
self.runtimeError("Can only call functions and classes", .{});
return InterpreterError.RuntimeError;
}
}
+ fn callClass(self: *@This(), callee: *Obj.Class, argCount: u8) !void {
+ if (argCount != 0) {
+ self.runtimeError("Expected {d} arguments but got {d}", .{ 0, argCount });
+ return InterpreterError.RuntimeError;
+ }
+
+ self.pook(argCount, Value.init(try self.vm.objects.emplace_cast(.Instance, callee)));
+ }
+
fn callClosure(self: *@This(), callee: *Obj.Closure, argCount: u8) !void {
if (argCount != callee.function.ptr().arity) {
self.runtimeError("Expected {d} arguments but got {d}", .{ callee.function.ptr().arity, argCount });
@@ -384,6 +399,31 @@ pub const VM = struct {
@intFromEnum(OP.SET_LOCAL) => {
self.frame().slots[self.read_byte()] = self.peek(0);
},
+ @intFromEnum(OP.GET_PROPERTY) => {
+ if (self.peek(0).cast_if(Obj.Type.Instance)) |instance| {
+ const field = self.read_string();
+ const prop = instance.fields.ptr().get(field) catch {
+ self.runtimeError("Undefined property '{f}'", .{field});
+ return InterpreterError.RuntimeError;
+ };
+ _ = self.pop();
+ self.push(prop);
+ } else {
+ self.runtimeError("Only instances have properties, found: {s}", .{self.peek(0).typeName()});
+ return InterpreterError.RuntimeError;
+ }
+ },
+ @intFromEnum(OP.SET_PROPERTY) => {
+ if (self.peek(1).cast_if(Obj.Type.Instance)) |instance| {
+ _ = try instance.fields.ptr().set(self.read_string(), self.peek(0));
+ const val = self.pop();
+ _ = self.pop();
+ self.push(val);
+ } else {
+ self.runtimeError("Only instances have properties, found: {s}", .{self.peek(0).typeName()});
+ return InterpreterError.RuntimeError;
+ }
+ },
@intFromEnum(OP.GET_GLOBAL) => {
const name = self.read_string();
const global = self.vm.globals.get(name) catch {
@@ -419,32 +459,34 @@ pub const VM = struct {
},
@intFromEnum(OP.GET_INDEX) => {
const key = self.pop();
- const obj = self.pop();
+ const col = self.pop();
var pushed = false;
- if (obj.is(Value.obj)) {
- switch (obj.obj.type) {
- .Function, .Native, .Closure, .Upvalue => {},
- inline else => |tp| {
- self.push((obj.obj.cast(tp) catch unreachable).get(key) catch Value.init({}));
+
+ if (col.cast_if(Value.obj)) |obj| {
+ switch (obj.type) {
+ inline .Table, .String, .List => |tp| {
+ self.push((obj.cast(tp) catch unreachable).get(key) catch Value.init({}));
pushed = true;
},
+ else => {},
}
}
+
if (!pushed) {
- self.runtimeError("Cannot index a value of type {s}", .{obj.typeName()});
+ self.runtimeError("Cannot index a value of type {s}", .{col.typeName()});
return InterpreterError.RuntimeError;
}
},
@intFromEnum(OP.SET_INDEX) => {
const val = self.pop();
const key = self.pop();
- const obj = self.pop();
+ const col = self.pop();
var pushed = false;
- if (obj.is(Value.obj)) {
- switch (obj.obj.type) {
- .Function, .Native, .Closure, .Upvalue, .String => {},
- inline else => |tp| {
- var m = obj.obj.cast(tp) catch unreachable;
+
+ if (col.cast_if(Value.obj)) |obj| {
+ switch (obj.type) {
+ inline .Table, .List => |tp| {
+ var m = obj.cast(tp) catch unreachable;
if (val.is(Value.nil)) {
m.delete(key);
} else {
@@ -452,10 +494,11 @@ pub const VM = struct {
}
pushed = true;
},
+ else => {},
}
}
if (!pushed) {
- self.runtimeError("Cannot index a value of type {s}", .{obj.typeName()});
+ self.runtimeError("Cannot index a value of type {s}", .{col.typeName()});
return InterpreterError.RuntimeError;
}
self.push(val);