commit 6ebfe7eb920247e8c0215ca568dec9634a388f65
parent f9e90b557359a18f30ebeed065d556edd34e6b57
Author: Szymon Mikulicz <szymon.mikulicz@posteo.net>
Date: Thu, 3 Sep 2026 18:54:51 +0200
Methods and This
Diffstat:
7 files changed, 167 insertions(+), 71 deletions(-)
diff --git a/zlox/src/compiler.zig b/zlox/src/compiler.zig
@@ -55,13 +55,26 @@ pub fn Compiler(size: comptime_int) type {
enclosing: ?*Self,
upvalues: [upvalues_size]Upvalue,
upvaluesCount: u8,
+ currentClass: ?*Class,
+
+ const Class = struct {
+ enclosing: ?*Class,
+ };
const Self = @This();
+ pub const Stack = size;
pub const Upvalue = struct {
+ pub const Type = enum(u8) {
+ local = 0,
+ remote = 1,
+ empty = 2,
+ };
+
index: u8,
- isLocal: bool,
+ type: Type,
};
+
const upvalues_size = std.math.maxInt(u8);
const Local = struct {
@@ -115,9 +128,10 @@ pub fn Compiler(size: comptime_int) type {
T.NIL => R(S.literal, null, P.NONE ),
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.FUN => R(S.funExpression, null, P.NONE ),
T.CLASS => R(S.class, null, P.NONE ),
T.DOT => R(null, S.dot, P.CALL ),
+ T.THIS => R(S.this, null, P.NONE ),
else => R(null, null, P.NONE ),
// zig fmt: on
};
@@ -362,6 +376,14 @@ pub fn Compiler(size: comptime_int) type {
}
}
+ fn this(self: *Self, _: bool) void {
+ if (self.currentClass) |_| {
+ self.variable(false);
+ } else {
+ self.errorAtPrevious("Can't use 'this' outside of class.");
+ }
+ }
+
fn variable(self: *Self, canAssign: bool) void {
self.namedVariable(self.previous, canAssign);
}
@@ -390,20 +412,22 @@ pub fn Compiler(size: comptime_int) type {
if (self.enclosing) |enclosing| {
if (enclosing.resolveLocal(name)) |local| {
enclosing.locals[local].captured = true;
- return self.addUpvalue(local, true) catch null;
+ return self.addUpvalue(local, .local) catch null;
} else if (enclosing.resolveUpvalue(name)) |upvalue| {
- return self.addUpvalue(upvalue, false) catch null;
+ return self.addUpvalue(upvalue, .remote) catch null;
}
}
return null;
}
- fn addUpvalue(self: *Self, idx: u8, isLocal: bool) !u8 {
+ fn addUpvalue(self: *Self, idx: u8, tp: Upvalue.Type) !u8 {
const count = self.upvaluesCount;
- for (self.upvalues[0..count], 0..) |upvalue, i| {
- if (upvalue.index == idx and upvalue.isLocal == isLocal) {
- return @intCast(i);
+ if (tp != .empty) {
+ for (self.upvalues[0..count], 0..) |upvalue, i| {
+ if (upvalue.index == idx and upvalue.type == tp) {
+ return @intCast(i);
+ }
}
}
@@ -413,7 +437,7 @@ pub fn Compiler(size: comptime_int) type {
return self.lastError;
}
- self.upvalues[count] = .{ .index = idx, .isLocal = isLocal };
+ self.upvalues[count] = .{ .index = idx, .type = tp };
self.upvaluesCount += 1;
return count;
}
@@ -554,16 +578,21 @@ pub fn Compiler(size: comptime_int) type {
};
self.emit(OP.CONSTANT, self.makeConstant(Value.init(cls)));
self.consume(Token.LEFT_BRACE, "Expect '{' before class body");
+
+ var curcls = Class{ .enclosing = self.currentClass };
+ self.currentClass = &curcls;
while (!self.check(Token.RIGHT_BRACE) and !self.check(Token.EOF)) {
self.method();
}
+ self.currentClass = self.currentClass.?.enclosing;
+
self.consume(Token.RIGHT_BRACE, "Expect '}' after class body");
}
fn method(self: *Self) void {
self.consume(Token.IDENTIFIER, "Expect method name");
const constant = self.identifierConstant(self.previous) catch return;
- self.function(false);
+ self.function(true);
self.emit(OP.METHOD, constant);
}
@@ -574,7 +603,11 @@ pub fn Compiler(size: comptime_int) type {
self.defineVariable(global, true);
}
- fn function(self: *Self, _: bool) void {
+ fn funExpression(self: *Self, _: bool) void {
+ self.function(false);
+ }
+
+ fn function(self: *Self, isMethod: bool) void {
const chunk = self.objects.emplace(.Chunk, {}) catch |err| {
self.errorAtPrevious("Couldn't allocate chunk");
self.lastError = err;
@@ -605,6 +638,18 @@ pub fn Compiler(size: comptime_int) type {
}
}
compiler.consume(Token.RIGHT_PAREN, "Expect ')' after parameters");
+
+ if (isMethod) {
+ _ = compiler.addUpvalue(0, .empty) catch return;
+ compiler.locals[0] = .{
+ .name = scanner.Token{ .type = Token.THIS, .lexeme = "this", .line = -1, .column = 0 },
+ .depth = compiler.scopeDepth,
+ };
+ compiler.emit(OP.GET_UPVALUE, 0);
+ compiler.emit(OP.SET_LOCAL, 0);
+ compiler.emitOP(OP.POP);
+ }
+
compiler.consume(Token.LEFT_BRACE, "Expect '{' before function body");
compiler.block();
@@ -614,22 +659,25 @@ pub fn Compiler(size: comptime_int) type {
if (compiler.hadError) {
self.lastError = compiler.lastError;
} else {
- const fun = self.objects.emplace(.Function, .{
- .type = .Function,
- .chunk = compiler.end() catch return,
- .arity = arity,
- }) catch |err| {
- self.errorAtPrevious("Couldn't allocate function");
- self.lastError = err;
- return;
- };
- self.emit(OP.CONSTANT, self.makeConstant(Value.init(fun.cast())));
-
- if (compiler.upvaluesCount != 0) {
- self.emit(OP.CLOSURE, compiler.upvaluesCount);
+ const endchunk = compiler.end() catch return;
+ if (compiler.upvaluesCount == 0) {
+ const fun = self.objects.emplace(.Function, .{
+ .type = .Function,
+ .chunk = endchunk,
+ .arity = arity,
+ }) catch |err| {
+ self.errorAtPrevious("Couldn't allocate function");
+ self.lastError = err;
+ return;
+ };
+ self.emit(OP.CONSTANT, self.makeConstant(Value.init(fun.cast())));
+ } else {
+ self.emit(OP.CONSTANT, self.makeConstant(Value.init(endchunk.cast())));
+ self.emit(OP.CLOSURE, arity);
+ self.emitByte(compiler.upvaluesCount);
for (compiler.upvalues[0..compiler.upvaluesCount]) |upvalue| {
- self.emitByte(if (upvalue.isLocal) 1 else 0);
+ self.emitByte(@intFromEnum(upvalue.type));
self.emitByte(upvalue.index);
}
}
@@ -1026,8 +1074,9 @@ pub fn Compiler(size: comptime_int) type {
.localCount = 1,
.scopeDepth = 0,
.enclosing = null,
- .upvalues = @splat(Upvalue{ .index = 0, .isLocal = false }),
+ .upvalues = @splat(Upvalue{ .index = 0, .type = .empty }),
.upvaluesCount = 0,
+ .currentClass = null,
};
self.locals[0].depth = 0;
@@ -1038,6 +1087,7 @@ pub fn Compiler(size: comptime_int) type {
var enclosed = try Self.init(enclosing.scanner, enclosing.objects, chunk);
enclosed.current = enclosing.current;
enclosed.enclosing = enclosing;
+ enclosed.currentClass = enclosing.currentClass;
enclosed.beginScope();
return enclosed;
diff --git a/zlox/src/debug.zig b/zlox/src/debug.zig
@@ -5,6 +5,7 @@ const value = @import("value.zig");
const Obj = @import("gc.zig").GC.Obj;
const Error = Obj.Error;
const OP = @import("op.zig").OP;
+const Compiler = @import("vm.zig").VM.Compiler;
const print = std.debug.print;
pub fn disassembleChunk(ch: *const Obj.Chunk) Error!void {
@@ -73,7 +74,7 @@ fn _disassembleInstruction(ch: *const Obj.Chunk, offset: usize, print_fn: bool)
@intFromEnum(OP.SET_INDEX) => simpleInstruction(name, offset),
@intFromEnum(OP.GET_INDEX) => simpleInstruction(name, offset),
@intFromEnum(OP.CALL) => try byteInstruction(name, ch, offset),
- @intFromEnum(OP.CLOSURE) => try byteInstruction(name, ch, offset),
+ @intFromEnum(OP.CLOSURE) => try closureInstruction(name, ch, offset),
@intFromEnum(OP.CLOSE_UPVALUE) => simpleInstruction(name, offset),
else => blk: {
print("Unknown opcode {d} {s}\n", .{ op, name });
@@ -91,9 +92,13 @@ fn constantInstruction(name: []const u8, ch: *const Obj.Chunk, offset: usize, pr
const constant = try ch.code.ptr().get(offset + 1);
const constval = try ch.constants.ptr().get(constant);
print("{s:<32} {d:4} '{f}'\n", .{ name, constant, constval });
- if (print_fn) if (constval.cast_if(Obj.Type.Function)) |function| {
- try disassembleChunk(function.chunk.ptr());
- };
+ if (print_fn) {
+ if (constval.cast_if(Obj.Type.Function)) |function| {
+ try disassembleChunk(function.chunk.ptr());
+ } else if (constval.cast_if(Obj.Type.Chunk)) |chunk| {
+ try disassembleChunk(chunk);
+ }
+ }
return offset + 2;
}
@@ -111,21 +116,20 @@ fn jumpInstruction(name: []const u8, sign: bool, ch: *const Obj.Chunk, offset: u
return offset + 3;
}
-fn closureInstruction(name: []const u8, ch: *const Obj.Chunk, offset: usize, print_fn: bool) Error!usize {
+fn closureInstruction(name: []const u8, ch: *const Obj.Chunk, offset: usize) Error!usize {
var off = offset + 1;
- const constant = try ch.code.ptr().get(off);
- const val = try ch.constants.ptr().get(constant);
- const function = try val.obj.cast(.Function);
- print("{s:<32} {d:4} '{f}'\n", .{ name, constant, function });
- for (0..function.upvalue_count) |_| {
- const isLocal = try ch.code.ptr().get(off + 1);
- const idx = try ch.code.ptr().get(off + 2);
+ const arity = try ch.code.ptr().get(off);
+ const count = try ch.code.ptr().get(off + 1);
+ off += 2;
+
+ print("{s:<32} {d:4} {d}\n", .{ name, arity, count });
+ for (0..count) |_| {
+ const tp = try ch.code.ptr().get(off);
+ const idx = try ch.code.ptr().get(off + 1);
try print_offset(ch, off + 1);
- print("{s:<38}|-> {s} {d}\n", .{ "", if (isLocal == 1) "local" else "upvalue", idx });
+ print("{s:<38}|-> {s} {d}\n", .{ "", @tagName(@as(Compiler.Upvalue.Type, @enumFromInt(tp))), idx });
off += 2;
}
- if (print_fn)
- try disassembleChunk(function.chunk.ptr());
return off + 1;
}
diff --git a/zlox/src/gc.zig b/zlox/src/gc.zig
@@ -101,7 +101,7 @@ pub const GC = struct {
if (obj.upvalues.ptr()) |upvalues|
for (upvalues) |upvalue_ptr|
if (upvalue_ptr) |upvalue|
- self.mark("c", upvalue);
+ self.mark("f", upvalue);
},
*Obj.Chunk => {
for (obj.constants.ptr().slice()) |constant|
diff --git a/zlox/src/obj::upvalue.zig b/zlox/src/obj::upvalue.zig
@@ -12,7 +12,7 @@ pub fn Upvalue(fields: anytype) type {
return packed struct {
const Self = @This();
- pub const Arg = struct { val: *Value, slot: u8 };
+ pub const Arg = struct { val: *Value, slot: u8, closed: bool = false };
pub const Error = error{OutOfMemory};
obj: Super,
@@ -24,8 +24,11 @@ pub fn Upvalue(fields: anytype) type {
const self: *Self = try allocator.create(Self);
self.* = Self{
.obj = Super.make(Self),
- .location = Packed(*Value).init(arg.val),
- .closed = false,
+ .location = if (arg.closed)
+ try Packed(*Value).create2(allocator, arg.val.*)
+ else
+ Packed(*Value).init(arg.val),
+ .closed = arg.closed,
.slot = arg.slot,
};
return self;
diff --git a/zlox/src/vm.zig b/zlox/src/vm.zig
@@ -22,6 +22,10 @@ pub const VM = struct {
globals: Globals,
allocator: std.mem.Allocator,
+ pub const CALLSTACK = 64;
+ pub const STACK = 256;
+ pub const Compiler = compiler.Compiler(STACK);
+
pub const Global = struct {
val: Value,
con: bool,
@@ -110,17 +114,14 @@ pub const VM = struct {
}
pub fn interpret(self: *@This(), source: []const u8, dbg: bool) InterpreterError!void {
- const callstack_size = 64;
- const stack_size = 256;
-
try self.objects.push_callback(&VM.gc_callback, self);
defer self.objects.pop_callback();
- const chunk = try compiler.Compiler(stack_size).compile(source, &self.objects);
+ const chunk = try Compiler.compile(source, &self.objects);
if (dbg) try debug.disassembleChunk(chunk);
- try Interpreter(callstack_size, stack_size).run(self, chunk, dbg);
+ try Interpreter(CALLSTACK, Compiler.Stack).run(self, chunk, dbg);
}
fn Interpreter(callstack_size: comptime_int, stack_size: comptime_int) type {
@@ -133,7 +134,7 @@ pub const VM = struct {
stackTop: [*]Value,
stack: [stack_size]Value,
vm: *VM,
- open_upvalues: List,
+ upvalues: List,
pub fn run(vm: *VM, chunk: *Obj.Chunk, dbg: bool) InterpreterError!void {
var self = @This(){
@@ -142,14 +143,14 @@ pub const VM = struct {
.stack = @splat(Value.init({})),
.stackTop = undefined,
.vm = vm,
- .open_upvalues = List.init(vm.allocator),
+ .upvalues = List.init(vm.allocator),
};
self.stackTop = &self.stack;
try vm.objects.push_callback(&Self.gc_callback, &self);
defer vm.objects.pop_callback();
- defer self.open_upvalues.free();
+ defer self.upvalues.free();
self.push(Value.init(chunk.cast()));
@@ -174,7 +175,7 @@ pub const VM = struct {
self.vm.objects.mark("F", self.frames[frame_idx].callee);
}
- var iter = self.open_upvalues.iter();
+ var iter = self.upvalues.iter();
while (iter.next()) |upval| {
self.vm.objects.mark("U", upval);
}
@@ -279,7 +280,7 @@ pub const VM = struct {
}
fn captureUpvalue(self: *@This(), slot: u8) !*Obj.Upvalue {
- var iter = self.open_upvalues.iter();
+ var iter = self.upvalues.iter();
while (iter.next()) |val| {
if (val.slot == slot)
@@ -295,7 +296,7 @@ pub const VM = struct {
}
fn closeUpvalues(self: *@This(), slot: u8) !void {
- var iter = self.open_upvalues.iter();
+ var iter = self.upvalues.iter();
while (iter.next()) |upval| {
if (upval.slot < slot) break;
@@ -392,11 +393,25 @@ pub const VM = struct {
self.frame().slots[self.read_byte()] = self.peek(0);
},
@intFromEnum(OP.GET_PROPERTY) => {
- if (self.peek(0).cast_if(Obj.Type.Instance)) |instance| {
+ var val = self.peek(0);
+ if (val.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;
+ const prop = instance.fields.ptr().get(field) catch blk: {
+ const method = instance.cls.ptr().methods.ptr().get(field) catch {
+ self.runtimeError("Undefined property '{f}'", .{field});
+ return InterpreterError.RuntimeError;
+ };
+ if (method.upvalues.ptr()) |upvalues| {
+ if (upvalues[0] == null) {
+ upvalues[0] = try self.vm.objects.emplace(.Upvalue, .{
+ .val = &val,
+ .slot = 0,
+ .closed = true,
+ });
+ method.type = .Method;
+ }
+ }
+ break :blk Value.init(method.cast());
};
_ = self.pop();
self.push(prop);
@@ -438,12 +453,12 @@ pub const VM = struct {
@intFromEnum(OP.GET_UPVALUE) => {
const closure = self.frame().callee;
const index = self.read_byte();
- self.push(closure.upvalues.at(index).?.location.ptr().*);
+ self.push(closure.upvalues.at(index).?.location.get());
},
@intFromEnum(OP.SET_UPVALUE) => {
const closure = self.frame().callee;
const index = self.read_byte();
- closure.upvalues.at(index).?.location.ptr().* = self.peek(0);
+ closure.upvalues.at(index).?.location.set(self.peek(0));
},
@intFromEnum(OP.CLOSE_UPVALUE) => {
try self.closeUpvalues(self.current_slot());
@@ -500,24 +515,29 @@ pub const VM = struct {
try self.callValue(self.peek(argCount), argCount);
},
@intFromEnum(OP.CLOSURE) => {
+ const arity = self.read_byte();
const count = self.read_byte();
- const function = try self.pop().obj.cast(.Function);
+ const chunk = try self.pop().obj.cast(.Chunk);
+
const closure = try self.vm.objects.emplace(.Function, .{
.type = .Closure,
- .chunk = function.chunk.ptr(),
- .arity = function.arity,
+ .chunk = chunk,
+ .arity = arity,
.upvalues = count,
});
+
self.push(Value.init(closure.cast()));
for (closure.upvalues.ptr().?) |*upvalue| {
- const isLocal = self.read_byte();
+ const tp = self.read_byte();
const slot = self.read_byte();
- if (isLocal == 1) {
- upvalue.* = try self.captureUpvalue(slot);
- } else {
- upvalue.* = self.frame().callee.upvalues.at(slot);
- }
+ const U = Compiler.Upvalue.Type;
+ upvalue.* = switch (tp) {
+ @intFromEnum(U.local) => try self.captureUpvalue(slot),
+ @intFromEnum(U.remote) => self.frame().callee.upvalues.at(slot),
+ @intFromEnum(U.empty) => null,
+ else => return InterpreterError.RuntimeError,
+ };
}
},
@intFromEnum(OP.METHOD) => {
diff --git a/zlox/test/nested.lox b/zlox/test/nested.lox
@@ -0,0 +1,11 @@
+class Nested {
+ method() {
+ fun function() {
+ print this;
+ }
+
+ function();
+ }
+}
+
+Nested().method();
diff --git a/zlox/test/scone.lox b/zlox/test/scone.lox
@@ -0,0 +1,8 @@
+class Scone {
+ topping(first, second) {
+ print "scone with " + first + " and " + second;
+ }
+}
+
+var scone = Scone();
+scone.topping("berries", "cream");