├── .gitignore ├── src ├── tests.zig ├── common.zig ├── chunk.zig ├── table.zig ├── debug.zig ├── scanner.zig ├── value.zig ├── memory.zig ├── main.zig ├── object.zig ├── vm.zig └── compiler.zig ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | zig-cache 3 | zig-out 4 | scripts/ -------------------------------------------------------------------------------- /src/tests.zig: -------------------------------------------------------------------------------- 1 | const main = @import("./main.zig"); 2 | const std = @import("std"); 3 | 4 | test { 5 | std.testing.refAllDecls(@This()); 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZLox 2 | 3 | A stack-based bytecode Virtual Machine for the [Lox Programming Language](https://craftinginterpreters.com/the-lox-language.html) written in [Zig](https://ziglang.org/). 4 | 5 | ## Run 6 | 7 | Start REPL: 8 | ```bash 9 | $ zig build run 10 | > print 1+1; 11 | 2 12 | > 13 | ``` 14 | 15 | ## Test 16 | 17 | ```bash 18 | $ zig build test 19 | All tests passed. 20 | ``` 21 | 22 | ## Troubleshoot 23 | 24 | Enable tracing: 25 | ```bash 26 | $ zig build -Denable-tracing 27 | ``` 28 | 29 | Enable disassembly: 30 | ```bash 31 | $ zig build -Denable-dump 32 | ``` 33 | 34 | For more build options use `zig build --help` -------------------------------------------------------------------------------- /src/common.zig: -------------------------------------------------------------------------------- 1 | const Value = @import("./value.zig").Value; 2 | const std = @import("std"); 3 | const builtin = @import("builtin"); 4 | pub const alloc = if (builtin.mode == std.builtin.Mode.Debug) std.testing.allocator else std.heap.c_allocator; 5 | pub const trace_enabled = @import("build_options").trace_enable; 6 | pub const dump_enabled = @import("build_options").dump_code; 7 | pub const stress_gc = @import("build_options").stress_gc; 8 | pub const log_gc = @import("build_options").log_gc; 9 | pub const nan_boxing = @import("build_options").nan_boxing; 10 | pub const gc_heap_growth_factor = 2; 11 | pub const stdout = if (builtin.is_test) buffer_stream.writer() else std.io.getStdOut().writer(); 12 | pub const stderr = std.io.getStdErr().writer(); 13 | 14 | pub var buffer: if (builtin.is_test) [1 << 10 << 10]u8 else [0]u8 = if (builtin.is_test) .{0} ** (1 << 10 << 10) else .{}; 15 | pub var buffer_stream = if (builtin.is_test) std.io.fixedBufferStream(buffer[0..]) else @compileError("only used for testing"); 16 | 17 | pub fn add(a: f64, b: f64) Value { 18 | return Value.Number(a + b); 19 | } 20 | 21 | pub fn sub(a: f64, b: f64) Value { 22 | return Value.Number(a - b); 23 | } 24 | 25 | pub fn mul(a: f64, b: f64) Value { 26 | return Value.Number(a * b); 27 | } 28 | 29 | pub fn div(a: f64, b: f64) Value { 30 | return Value.Number(a / b); 31 | } 32 | 33 | pub fn greater(a: f64, b: f64) Value { 34 | return Value.Boolean(a > b); 35 | } 36 | 37 | pub fn less(a: f64, b: f64) Value { 38 | return Value.Boolean(a < b); 39 | } 40 | 41 | pub inline fn growCapacity(old_c: anytype) @TypeOf(old_c) { 42 | return if (old_c < 8) 8 else old_c * 2; 43 | } 44 | -------------------------------------------------------------------------------- /src/chunk.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const memory = @import("./memory.zig"); 3 | const common = @import("./common.zig"); 4 | const ValueArray = @import("./value.zig").ValueArray; 5 | const Value = @import("./value.zig").Value; 6 | 7 | pub const OpCode = enum(u8) { 8 | op_constant, // arg 1: constant index 9 | op_nil, 10 | op_true, 11 | op_false, 12 | op_pop, 13 | op_get_local, 14 | op_get_global, 15 | op_get_upvalue, 16 | op_define_global, 17 | op_set_local, 18 | op_set_global, 19 | op_set_upvalue, 20 | op_get_property, 21 | op_set_property, 22 | op_equal, 23 | op_greater, 24 | op_less, 25 | op_add, 26 | op_subtract, 27 | op_multiply, 28 | op_divide, 29 | op_not, 30 | op_negate, 31 | op_print, 32 | op_jump, 33 | op_jump_if_false, 34 | op_loop, 35 | op_call, 36 | op_invoke, 37 | op_super_invoke, 38 | op_closure, 39 | op_close_upvalue, 40 | op_return, 41 | op_class, 42 | op_inherit, 43 | op_get_super, 44 | op_method, 45 | }; 46 | 47 | pub const Chunk = struct { 48 | allocator: std.mem.Allocator, 49 | count: u32 = 0, 50 | capacity: u32 = 0, 51 | code: [*]u8 = undefined, 52 | constants: ValueArray, 53 | lines: [*]u32 = undefined, 54 | 55 | pub fn init(allocator: std.mem.Allocator) Chunk { 56 | return .{ 57 | .allocator = allocator, 58 | .constants = ValueArray.init(allocator), 59 | }; 60 | } 61 | 62 | pub fn writeChunk(self: *Chunk, byte: u8, line: u32) void { 63 | if (self.capacity <= self.count) { 64 | const old_c = self.capacity; 65 | self.capacity = common.growCapacity(old_c); 66 | self.code = memory.growArray(u8, self.code, old_c, self.capacity); 67 | self.lines = memory.growArray(u32, self.lines, old_c, self.capacity); 68 | } 69 | self.code[self.count] = byte; 70 | self.lines[self.count] = line; 71 | self.count += 1; 72 | } 73 | 74 | pub fn addConstant(self: *Chunk, value: Value) u32 { 75 | memory.vm.push(value); 76 | self.constants.writeValueArray(value); 77 | _ = memory.vm.pop(); 78 | return @intCast(u32, self.constants.count - 1); 79 | } 80 | 81 | pub fn freeChunk(self: *Chunk) void { 82 | memory.freeArray(u8, self.code, self.capacity); 83 | memory.freeArray(u32, self.lines, self.capacity); 84 | self.count = 0; 85 | self.capacity = 0; 86 | self.code = undefined; 87 | self.constants.freeValueArray(); 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /src/table.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const memory = @import("./memory.zig"); 3 | const common = @import("./common.zig"); 4 | const Value = @import("./value.zig").Value; 5 | const object = @import("./object.zig"); 6 | const ObjString = object.ObjString; 7 | const Obj = object.Obj; 8 | 9 | const alloc = @import("./common.zig").alloc; 10 | 11 | pub const Table = struct { 12 | const Self = @This(); 13 | const max_load = 0.75; 14 | 15 | count: usize = 0, 16 | capacity: usize = 0, 17 | entries: [*]Entry = undefined, 18 | 19 | pub fn initTable() Self { 20 | return .{}; 21 | } 22 | 23 | pub fn freeTable(self: *Self) void { 24 | memory.freeArray(Entry, self.entries, self.capacity); 25 | self.capacity = 0; 26 | self.count = 0; 27 | } 28 | 29 | pub fn tableSet(self: *Self, key: *ObjString, value: Value) bool { 30 | if (self.count + 1 > @floatToInt(usize, @intToFloat(f32, self.capacity) * max_load)) { 31 | const capacity = common.growCapacity(self.capacity); 32 | self.adjustCapacity(capacity); 33 | } 34 | const entry = findEntry(self.entries, self.capacity, key); 35 | const isNewKey = entry.key == null; 36 | if (isNewKey and entry.value.isNil()) self.count += 1; 37 | 38 | entry.key = key; 39 | entry.value = value; 40 | return isNewKey; 41 | } 42 | 43 | pub fn tableGet(self: *Self, key: *ObjString) ?Value { 44 | if (self.count == 0) return null; 45 | 46 | const entry = findEntry(self.entries, self.capacity, key); 47 | if (entry.key == null) return null; 48 | 49 | return entry.value; 50 | } 51 | 52 | pub fn tableDelete(self: *Self, key: *ObjString) bool { 53 | if (self.count == 0) return false; 54 | 55 | const entry = findEntry(self.entries, self.capacity, key); 56 | if (entry.key == null) return false; 57 | 58 | entry.key = null; 59 | entry.value = Value.Boolean(true); 60 | return true; 61 | } 62 | 63 | pub fn findString(self: *Self, chars: [*]const u8, length: usize, hash: u32) ?*ObjString { 64 | if (self.count == 0) return null; 65 | 66 | var index = hash & (self.capacity - 1); 67 | while (true) { 68 | const entry = &self.entries[index]; 69 | if (entry.key == null) { 70 | if (entry.value.isNil()) return null; 71 | } else if (entry.key.?.hash == hash and std.mem.eql(u8, entry.key.?.chars[0..length], chars[0..length])) { 72 | return entry.key; 73 | } 74 | index = (index + 1) & (self.capacity - 1); 75 | } 76 | } 77 | 78 | pub fn removeWhite(self: *Self) void { 79 | for (self.entries[0..self.capacity]) |*entry| { 80 | if (entry.key) |key| { 81 | if (!key.obj.is_marked) { 82 | _ = self.tableDelete(key); 83 | } 84 | } 85 | } 86 | } 87 | 88 | pub fn markTable(self: *Self) void { 89 | for (self.entries[0..self.capacity]) |*entry| { 90 | memory.markObject(@ptrCast(?*Obj, entry.key)); 91 | memory.markValue(&entry.value); 92 | } 93 | } 94 | 95 | pub fn tableAddAll(to: *Self, from: *Self) void { 96 | for (from.entries[0..from.capacity]) |entry| { 97 | if (entry.key) |key| { 98 | _ = to.tableSet(key, entry.value); 99 | } 100 | } 101 | } 102 | 103 | fn adjustCapacity(self: *Self, capacity: usize) void { 104 | const entries = memory.allocate(Entry, capacity); 105 | for (entries[0..capacity]) |*entry| { 106 | entry.key = null; 107 | entry.value = Value.Nil(); 108 | } 109 | 110 | self.count = 0; 111 | for (self.entries[0..self.capacity]) |*entry| { 112 | if (entry.key == null) continue; 113 | const dest = findEntry(entries, capacity, entry.key.?); 114 | dest.key = entry.key; 115 | dest.value = entry.value; 116 | self.count += 1; 117 | } 118 | memory.freeArray(Entry, self.entries, self.capacity); 119 | 120 | self.entries = entries; 121 | self.capacity = capacity; 122 | } 123 | }; 124 | 125 | const Entry = struct { 126 | key: ?*ObjString, 127 | value: Value, 128 | }; 129 | 130 | fn findEntry(entries: [*]Entry, capacity: usize, key: *ObjString) *Entry { 131 | var index = key.hash & (capacity - 1); 132 | var tombstone: ?*Entry = null; 133 | while (true) { 134 | const entry = &entries[index]; 135 | if (entry.key == null) { 136 | if (entry.value.isNil()) { 137 | return tombstone orelse entry; 138 | } else { 139 | if (tombstone == null) tombstone = entry; 140 | } 141 | } else if (entry.key == key) return entry; 142 | index = (index + 1) & (capacity - 1); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/debug.zig: -------------------------------------------------------------------------------- 1 | const chunk = @import("./chunk.zig"); 2 | const std = @import("std"); 3 | const value = @import("./value.zig"); 4 | const common = @import("./common.zig"); 5 | 6 | const stdout = common.stdout; 7 | 8 | pub fn disassembleChunk(c: *chunk.Chunk, name: []const u8) void { 9 | stdout.print("== {s} ==\n", .{name}) catch unreachable; 10 | var offset: u32 = 0; 11 | while (offset < c.count) { 12 | offset = disassembleInstruction(c, offset); 13 | } 14 | } 15 | 16 | pub fn disassembleInstruction(c: *chunk.Chunk, offset: u32) u32 { 17 | stdout.print("{d:0>4} ", .{offset}) catch unreachable; 18 | if (offset > 0 and c.lines[offset] == c.lines[offset - 1]) { 19 | stdout.print(" | ", .{}) catch unreachable; 20 | } else { 21 | stdout.print("{d:4} ", .{c.lines[offset]}) catch unreachable; 22 | } 23 | const instruction = c.code[offset]; 24 | return switch (@intToEnum(chunk.OpCode, instruction)) { 25 | .op_return => simpleInstruction("OP_RETURN", offset), 26 | .op_constant => constantInstruction("OP_CONSTANT", c, offset), 27 | .op_not => simpleInstruction("OP_NOT", offset), 28 | .op_negate => simpleInstruction("OP_NEGATE", offset), 29 | .op_add => simpleInstruction("OP_ADD", offset), 30 | .op_subtract => simpleInstruction("OP_SUBTRACT", offset), 31 | .op_multiply => simpleInstruction("OP_MULTIPLY", offset), 32 | .op_divide => simpleInstruction("OP_DIVIDE", offset), 33 | .op_nil => simpleInstruction("OP_NIL", offset), 34 | .op_true => simpleInstruction("OP_TRUE", offset), 35 | .op_false => simpleInstruction("OP_FALSE", offset), 36 | .op_equal => simpleInstruction("OP_EQUAL", offset), 37 | .op_greater => simpleInstruction("OP_GREATER", offset), 38 | .op_less => simpleInstruction("OP_LESS", offset), 39 | .op_print => simpleInstruction("OP_PRINT", offset), 40 | .op_pop => simpleInstruction("OP_POP", offset), 41 | .op_define_global => constantInstruction("OP_DEFINE_GLOBAL", c, offset), 42 | .op_get_global => constantInstruction("OP_GET_GLOBAL", c, offset), 43 | .op_set_global => constantInstruction("OP_SET_GLOBAL", c, offset), 44 | .op_get_local => byteInstruction("OP_GET_LOCAL", c, offset), 45 | .op_set_local => byteInstruction("OP_SET_LOCAL", c, offset), 46 | .op_jump => jumpInstruction("OP_JUMP", 1, c, offset), 47 | .op_jump_if_false => jumpInstruction("OP_JUMP_IF_FALSE", 1, c, offset), 48 | .op_loop => jumpInstruction("OP_LOOP", -1, c, offset), 49 | .op_call => byteInstruction("OP_CALL", c, offset), 50 | .op_get_upvalue => byteInstruction("OP_GET_UPVALUE", c, offset), 51 | .op_set_upvalue => byteInstruction("OP_SET_UPVALUE", c, offset), 52 | .op_close_upvalue => simpleInstruction("OP_CLOSE_UPVALUE", offset), 53 | .op_closure => blk: { 54 | var offset_t = offset; 55 | offset_t += 1; 56 | const con = c.code[offset_t]; 57 | offset_t += 1; 58 | stdout.print("{s:<16} {d:4} ", .{ "OP_CLOSURE", con }) catch unreachable; 59 | value.printValue(c.constants.values[con], stdout); 60 | stdout.print("\n", .{}) catch unreachable; 61 | const function = c.constants.values[con].asObject().asFunction(); 62 | var i: usize = 0; 63 | while (i < function.upvalue_count) : (i += 1) { 64 | const is_local = c.code[offset_t]; 65 | offset_t += 1; 66 | const index = c.code[offset_t]; 67 | offset_t += 1; 68 | const name: []const u8 = if (is_local == 1) "local" else "upvalue"; 69 | stdout.print("{d:0>4} | {s} {d}\n", .{ offset_t - 2, name, index }) catch unreachable; 70 | } 71 | break :blk offset_t; 72 | }, 73 | .op_class => constantInstruction("OP_CLASS", c, offset), 74 | .op_get_property => constantInstruction("OP_GET_PROPERTY", c, offset), 75 | .op_set_property => constantInstruction("OP_SET_PROPERTY", c, offset), 76 | .op_method => constantInstruction("OP_METHOD", c, offset), 77 | .op_invoke => invokeInstruction("OP_INVOKE", c, offset), 78 | .op_inherit => simpleInstruction("OP_INHERIT", offset), 79 | .op_get_super => constantInstruction("OP_GET_SUPER", c, offset), 80 | .op_super_invoke => invokeInstruction("OP_SUPER_INVOKE", c, offset), 81 | }; 82 | } 83 | 84 | fn simpleInstruction(name: []const u8, offset: u32) u32 { 85 | stdout.print("{s}\n", .{name}) catch unreachable; 86 | return offset + 1; 87 | } 88 | 89 | fn byteInstruction(name: []const u8, c: *chunk.Chunk, offset: u32) u32 { 90 | const slot = c.code[offset + 1]; 91 | stdout.print("{s:<16} {d:4}\n", .{ name, slot }) catch unreachable; 92 | return offset + 2; 93 | } 94 | 95 | fn constantInstruction(name: []const u8, c: *chunk.Chunk, offset: u32) u32 { 96 | const index = c.code[offset + 1]; 97 | stdout.print("{s:<16} {d:4} '", .{ name, index }) catch unreachable; 98 | value.printValue(c.constants.values[index], stdout); 99 | stdout.print("'\n", .{}) catch unreachable; 100 | return offset + 2; 101 | } 102 | 103 | fn jumpInstruction(name: []const u8, sign: isize, c: *chunk.Chunk, offset: u32) u32 { 104 | var jump: u16 = c.code[offset + 1]; 105 | jump <<= 8; 106 | jump |= c.code[offset + 2]; 107 | stdout.print("{s:<16} {d:4} -> {d}\n", .{ name, offset, offset + 3 + sign * jump }) catch unreachable; 108 | return offset + 3; 109 | } 110 | 111 | fn invokeInstruction(name: []const u8, c: *chunk.Chunk, offset: u32) u32 { 112 | const constant = c.code[offset + 1]; 113 | const arg_count = c.code[offset + 2]; 114 | stdout.print("{s:<16} ({d} args) {d:4} ", .{ name, arg_count, constant }) catch unreachable; 115 | value.printValue(c.constants.values[constant], stdout); 116 | stdout.print("\n", .{}) catch unreachable; 117 | return offset + 3; 118 | } 119 | -------------------------------------------------------------------------------- /src/scanner.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const Token = struct { 4 | t: TokenType, 5 | start: [*]const u8, 6 | length: usize, 7 | line: usize, 8 | }; 9 | 10 | pub const TokenType = enum(u8) { 11 | 12 | // single char 13 | left_paren, 14 | right_paren, 15 | left_brace, 16 | right_brace, 17 | comma, 18 | dot, 19 | minus, 20 | plus, 21 | semicolon, 22 | slash, 23 | star, 24 | // multi char 25 | bang, 26 | bang_equal, 27 | equal, 28 | equal_equal, 29 | greater, 30 | greater_equal, 31 | less, 32 | less_equal, 33 | // literals 34 | identifier, 35 | string, 36 | number, 37 | // keywords 38 | lox_and, 39 | lox_class, 40 | lox_else, 41 | lox_false, 42 | lox_for, 43 | lox_fun, 44 | lox_if, 45 | lox_nil, 46 | lox_or, 47 | lox_print, 48 | lox_return, 49 | lox_super, 50 | lox_this, 51 | lox_true, 52 | lox_var, 53 | lox_while, 54 | lox_error, 55 | lox_eof, 56 | }; 57 | 58 | pub const Scanner = struct { 59 | start: [*]const u8, 60 | current: [*]const u8, 61 | line: usize = 1, 62 | 63 | const Self = @This(); 64 | 65 | pub fn init(source: [:0]const u8) Self { 66 | return .{ 67 | .start = source.ptr, 68 | .current = source.ptr, 69 | }; 70 | } 71 | 72 | pub fn scanToken(self: *Self) Token { 73 | self.skipWhitespace(); 74 | self.start = self.current; 75 | if (self.isAtEnd()) return self.makeToken(.lox_eof); 76 | 77 | const c = self.advance(); 78 | if (isAlpha(c)) { 79 | return self.identifier(); 80 | } 81 | if (std.ascii.isDigit(c)) { 82 | return self.number(); 83 | } 84 | 85 | switch (c) { 86 | '(' => return self.makeToken(.left_paren), 87 | ')' => return self.makeToken(.right_paren), 88 | '{' => return self.makeToken(.left_brace), 89 | '}' => return self.makeToken(.right_brace), 90 | ';' => return self.makeToken(.semicolon), 91 | ',' => return self.makeToken(.comma), 92 | '.' => return self.makeToken(.dot), 93 | '-' => return self.makeToken(.minus), 94 | '+' => return self.makeToken(.plus), 95 | '/' => return self.makeToken(.slash), 96 | '*' => return self.makeToken(.star), 97 | 98 | '!' => return self.makeToken(if (self.match('=')) .bang_equal else .bang), 99 | '=' => return self.makeToken(if (self.match('=')) .equal_equal else .equal), 100 | '<' => return self.makeToken(if (self.match('=')) .less_equal else .less), 101 | '>' => return self.makeToken(if (self.match('=')) .greater_equal else .greater), 102 | 103 | '"' => return self.string(), 104 | 105 | else => return self.errorToken("Unexpected character."), 106 | } 107 | } 108 | 109 | fn identifier(self: *Self) Token { 110 | while (isAlpha(self.peek()) or std.ascii.isDigit(self.peek())) _ = self.advance(); 111 | return self.makeToken(self.identifierType()); 112 | } 113 | 114 | fn number(self: *Self) Token { 115 | while (std.ascii.isDigit(self.peek())) { 116 | _ = self.advance(); 117 | } 118 | 119 | if (self.peek() == '.' and std.ascii.isDigit(self.peekNext())) { 120 | _ = self.advance(); 121 | while (std.ascii.isDigit(self.peek())) { 122 | _ = self.advance(); 123 | } 124 | } 125 | 126 | return self.makeToken(.number); 127 | } 128 | 129 | fn string(self: *Self) Token { 130 | while (self.peek() != '"' and !self.isAtEnd()) { 131 | if (self.peek() == '\n') self.line += 1; 132 | _ = self.advance(); 133 | } 134 | if (self.isAtEnd()) { 135 | return self.errorToken("Unterminated string."); 136 | } 137 | 138 | // closing quote 139 | _ = self.advance(); 140 | return self.makeToken(.string); 141 | } 142 | 143 | fn identifierType(self: *Self) TokenType { 144 | return switch (self.start[0]) { 145 | 'a' => self.checkKeyword(1, 2, "nd", .lox_and), 146 | 'c' => self.checkKeyword(1, 4, "lass", .lox_class), 147 | 'e' => self.checkKeyword(1, 3, "lse", .lox_else), 148 | 'f' => blk: { 149 | if (@ptrToInt(self.current) - @ptrToInt(self.start) > 1) { 150 | break :blk switch (self.start[1]) { 151 | 'a' => self.checkKeyword(2, 3, "lse", .lox_false), 152 | 'o' => self.checkKeyword(2, 1, "r", .lox_for), 153 | 'u' => self.checkKeyword(2, 1, "n", .lox_fun), 154 | else => .identifier, 155 | }; 156 | } else break :blk .identifier; 157 | }, 158 | 't' => blk: { 159 | if (@ptrToInt(self.current) - @ptrToInt(self.start) > 1) { 160 | break :blk switch (self.start[1]) { 161 | 'h' => self.checkKeyword(2, 2, "is", .lox_this), 162 | 'r' => self.checkKeyword(2, 2, "ue", .lox_true), 163 | else => .identifier, 164 | }; 165 | } else break :blk .identifier; 166 | }, 167 | 'i' => self.checkKeyword(1, 1, "f", .lox_if), 168 | 'n' => self.checkKeyword(1, 2, "il", .lox_nil), 169 | 'o' => self.checkKeyword(1, 1, "r", .lox_or), 170 | 'p' => self.checkKeyword(1, 4, "rint", .lox_print), 171 | 'r' => self.checkKeyword(1, 5, "eturn", .lox_return), 172 | 's' => self.checkKeyword(1, 4, "uper", .lox_super), 173 | 'v' => self.checkKeyword(1, 2, "ar", .lox_var), 174 | 'w' => self.checkKeyword(1, 4, "hile", .lox_while), 175 | else => .identifier, 176 | }; 177 | } 178 | 179 | inline fn isAlpha(char: u8) bool { 180 | return std.ascii.isAlpha(char) or char == '_'; 181 | } 182 | 183 | inline fn checkKeyword(self: *Self, comptime start: comptime_int, comptime length: comptime_int, rest: [:0]const u8, t: TokenType) TokenType { 184 | if (@ptrToInt(self.current) - @ptrToInt(self.start) == start + length and 185 | std.mem.eql(u8, (self.start + start)[0..length], rest[0..length])) 186 | { 187 | return t; 188 | } 189 | return .identifier; 190 | } 191 | 192 | fn isAtEnd(self: *Self) bool { 193 | return self.current[0] == 0; 194 | } 195 | 196 | fn advance(self: *Self) u8 { 197 | defer self.current += 1; 198 | return self.current[0]; 199 | } 200 | 201 | fn skipWhitespace(self: *Self) void { 202 | while (true) { 203 | const c = self.peek(); 204 | switch (c) { 205 | ' ', '\r', '\t' => _ = self.advance(), 206 | '\n' => { 207 | self.line += 1; 208 | _ = self.advance(); 209 | }, 210 | '/' => { 211 | if (self.peekNext() == '/') { 212 | while (self.peek() != '\n' and !self.isAtEnd()) { 213 | _ = self.advance(); 214 | } 215 | } else return; 216 | }, 217 | else => return, 218 | } 219 | } 220 | } 221 | 222 | fn peek(self: *Self) u8 { 223 | return self.current[0]; 224 | } 225 | 226 | fn peekNext(self: *Self) u8 { 227 | if (self.isAtEnd()) return 0; 228 | return self.current[1]; 229 | } 230 | 231 | fn match(self: *Self, char: u8) bool { 232 | if (self.isAtEnd()) return false; 233 | if (self.current[0] != char) return false; 234 | self.current += 1; 235 | return true; 236 | } 237 | 238 | fn makeToken(self: *Self, t: TokenType) Token { 239 | return .{ 240 | .t = t, 241 | .start = self.start, 242 | .length = @ptrToInt(self.current) - @ptrToInt(self.start), 243 | .line = self.line, 244 | }; 245 | } 246 | 247 | fn errorToken(self: *Self, message: []const u8) Token { 248 | return .{ 249 | .t = .lox_error, 250 | .start = message.ptr, 251 | .length = message.len, 252 | .line = self.line, 253 | }; 254 | } 255 | }; 256 | -------------------------------------------------------------------------------- /src/value.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const memory = @import("./memory.zig"); 3 | const object = @import("./object.zig"); 4 | const common = @import("./common.zig"); 5 | const Obj = object.Obj; 6 | 7 | pub const ValueType = enum { 8 | val_bool, 9 | val_nil, 10 | val_number, 11 | val_obj, 12 | }; 13 | 14 | const ensureSize = blk: { 15 | if (common.nan_boxing) { 16 | if (@sizeOf(Value) > @sizeOf(u64)) { 17 | @compileError("Expected Value to be 8 byte large with NaN boxing."); 18 | } 19 | } 20 | break :blk undefined; 21 | }; 22 | 23 | pub const Value = if (common.nan_boxing) 24 | struct { 25 | const QNAN = 0x7ffc << 48; 26 | const SIGN_BIT = 1 << 63; 27 | const TAG_NIL = 1; 28 | const TAG_FALSE = 2; 29 | const TAG_TRUE = 3; 30 | v: u64, 31 | pub inline fn Boolean(value: bool) Value { 32 | const tag: u64 = if (value) TAG_TRUE else TAG_FALSE; 33 | return .{ .v = QNAN | tag }; 34 | } 35 | pub inline fn Nil() Value { 36 | return .{ .v = QNAN | TAG_NIL }; 37 | } 38 | pub inline fn Number(value: f64) Value { 39 | return .{ .v = @bitCast(u64, value) }; 40 | } 41 | pub inline fn Object(obj: *Obj) Value { 42 | return .{ .v = SIGN_BIT | QNAN | @ptrToInt(obj) }; 43 | } 44 | 45 | pub inline fn asBoolean(self: *const Value) bool { 46 | return self.v == QNAN | TAG_TRUE; 47 | } 48 | pub inline fn asObject(self: *const Value) *Obj { 49 | return @intToPtr(*Obj, self.v & ~(@intCast(u64, (SIGN_BIT | QNAN)))); 50 | } 51 | pub inline fn asNumber(self: *const Value) f64 { 52 | return @bitCast(f64, self.v); 53 | } 54 | 55 | pub inline fn isBool(self: *const Value) bool { 56 | return self.v | 1 == QNAN | TAG_TRUE; 57 | } 58 | pub inline fn isNumber(self: *const Value) bool { 59 | return self.v & QNAN != QNAN; 60 | } 61 | pub inline fn isNil(self: *const Value) bool { 62 | return self.v == QNAN | TAG_NIL; 63 | } 64 | pub inline fn isObject(self: *const Value) bool { 65 | return self.v & (QNAN | SIGN_BIT) == (QNAN | SIGN_BIT); 66 | } 67 | pub inline fn isString(self: *const Value) bool { 68 | if (!self.isObject()) return false; 69 | return self.asObject().t == .obj_string; 70 | } 71 | pub inline fn isFunction(self: *const Value) bool { 72 | if (!self.isObject()) return false; 73 | return self.asObject().t == .obj_function; 74 | } 75 | pub inline fn isClass(self: *const Value) bool { 76 | if (!self.isObject()) return false; 77 | return self.asObject().t == .obj_class; 78 | } 79 | pub inline fn isInstance(self: *const Value) bool { 80 | if (!self.isObject()) return false; 81 | return self.asObject().t == .obj_instance; 82 | } 83 | pub inline fn isBoundMethod(self: *const Value) bool { 84 | if (!self.isObject()) return false; 85 | return self.asObject().t == .obj_bound_method; 86 | } 87 | } 88 | else 89 | struct { 90 | t: ValueType, 91 | as: union { 92 | boolean: bool, 93 | number: f64, 94 | obj: *Obj, 95 | }, 96 | 97 | pub inline fn Boolean(value: bool) Value { 98 | return .{ .t = .val_bool, .as = .{ 99 | .boolean = value, 100 | } }; 101 | } 102 | pub inline fn Nil() Value { 103 | return .{ .t = .val_nil, .as = .{ 104 | .number = 0, 105 | } }; 106 | } 107 | pub inline fn Number(value: f64) Value { 108 | return .{ .t = .val_number, .as = .{ 109 | .number = value, 110 | } }; 111 | } 112 | pub inline fn Object(obj: *Obj) Value { 113 | return .{ .t = .val_obj, .as = .{ 114 | .obj = obj, 115 | } }; 116 | } 117 | 118 | pub inline fn asBoolean(self: *const Value) bool { 119 | return self.as.boolean; 120 | } 121 | pub inline fn asObject(self: *const Value) *Obj { 122 | return self.as.obj; 123 | } 124 | pub inline fn asNumber(self: *const Value) f64 { 125 | return self.as.number; 126 | } 127 | 128 | pub inline fn isBool(self: *const Value) bool { 129 | return self.t == .val_bool; 130 | } 131 | pub inline fn isNumber(self: *const Value) bool { 132 | return self.t == .val_number; 133 | } 134 | pub inline fn isNil(self: *const Value) bool { 135 | return self.t == .val_nil; 136 | } 137 | pub inline fn isObject(self: *const Value) bool { 138 | return self.t == .val_obj; 139 | } 140 | pub inline fn isString(self: *const Value) bool { 141 | if (!self.isObject()) return false; 142 | return self.asObject().t == .obj_string; 143 | } 144 | pub inline fn isFunction(self: *const Value) bool { 145 | if (!self.isObject()) return false; 146 | return self.asObject().t == .obj_function; 147 | } 148 | pub inline fn isClass(self: *const Value) bool { 149 | if (!self.isObject()) return false; 150 | return self.asObject().t == .obj_class; 151 | } 152 | pub inline fn isInstance(self: *const Value) bool { 153 | if (!self.isObject()) return false; 154 | return self.asObject().t == .obj_instance; 155 | } 156 | pub inline fn isBoundMethod(self: *const Value) bool { 157 | if (!self.isObject()) return false; 158 | return self.asObject().t == .obj_bound_method; 159 | } 160 | }; 161 | 162 | pub fn printValue(v: Value, writer: anytype) void { 163 | if (common.nan_boxing) { 164 | if (v.isBool()) { 165 | (if (v.asBoolean()) writer.print("true", .{}) else writer.print("false", .{})) catch unreachable; 166 | } 167 | if (v.isNil()) { 168 | writer.print("nil", .{}) catch unreachable; 169 | } 170 | if (v.isNumber()) { 171 | writer.print("{d}", .{v.asNumber()}) catch unreachable; 172 | } 173 | if (v.isObject()) { 174 | v.asObject().print(writer); 175 | } 176 | } else { 177 | switch (v.t) { 178 | .val_bool => (if (v.asBoolean()) writer.print("true", .{}) else writer.print("false", .{})) catch unreachable, 179 | .val_nil => writer.print("nil", .{}) catch unreachable, 180 | .val_number => writer.print("{d}", .{v.as.number}) catch unreachable, 181 | .val_obj => v.as.obj.print(writer), 182 | } 183 | } 184 | } 185 | 186 | pub fn valuesEqual(val1: Value, val2: Value) bool { 187 | if (common.nan_boxing) { 188 | if (val1.isNumber() and val2.isNumber()) { 189 | return val1.asNumber() == val2.asNumber(); 190 | } 191 | return val1.v == val2.v; 192 | } else { 193 | if (val1.t != val2.t) return false; 194 | switch (val1.t) { 195 | .val_bool => return val1.asBoolean() == val2.asBoolean(), 196 | .val_nil => return true, 197 | .val_number => return val1.asNumber() == val2.asNumber(), 198 | .val_obj => return val1.asObject() == val2.asObject(), 199 | } 200 | } 201 | } 202 | 203 | pub const ValueArray = struct { 204 | allocator: std.mem.Allocator, 205 | capacity: u32 = 0, 206 | count: u32 = 0, 207 | values: [*]Value = undefined, 208 | 209 | pub fn init(allocator: std.mem.Allocator) ValueArray { 210 | return .{ 211 | .allocator = allocator, 212 | }; 213 | } 214 | 215 | pub fn writeValueArray(self: *ValueArray, value: Value) void { 216 | if (self.capacity <= self.count) { 217 | const old_c = self.capacity; 218 | self.capacity = if (old_c < 8) 8 else old_c * 2; 219 | self.values = memory.growArray(Value, self.values, old_c, self.capacity); 220 | } 221 | self.values[self.count] = value; 222 | self.count += 1; 223 | } 224 | 225 | pub fn freeValueArray(self: *ValueArray) void { 226 | memory.freeArray(Value, self.values, self.capacity); 227 | self.count = 0; 228 | self.capacity = 0; 229 | self.values = undefined; 230 | } 231 | }; 232 | 233 | test "refAllDecls" { 234 | const file = @import("./value.zig"); 235 | std.testing.refAllDecls(file); 236 | } 237 | -------------------------------------------------------------------------------- /src/memory.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const object = @import("./object.zig"); 3 | const compiler = @import("./compiler.zig"); 4 | const common = @import("./common.zig"); 5 | const value = @import("./value.zig"); 6 | const VM = @import("./vm.zig").VM; 7 | const Value = value.Value; 8 | const ValueArray = value.ValueArray; 9 | const Obj = object.Obj; 10 | const ObjString = object.ObjString; 11 | const ObjFunction = object.ObjFunction; 12 | const ObjNative = object.ObjNative; 13 | const ObjClosure = object.ObjClosure; 14 | const ObjUpvalue = object.ObjUpvalue; 15 | const ObjClass = object.ObjClass; 16 | const ObjInstance = object.ObjInstance; 17 | const ObjBoundMethod = object.ObjBoundMethod; 18 | 19 | // TODO remove alloc parameters 20 | const alloc: std.mem.Allocator = common.alloc; 21 | 22 | pub var vm: *VM = undefined; 23 | 24 | pub fn initGC(_vm: *VM) void { 25 | vm = _vm; 26 | object.initGC(_vm); 27 | } 28 | 29 | pub fn growArray(comptime t: type, ptr: [*]t, old_cap: u32, new_cap: u32) [*]t { 30 | return reallocate(ptr, old_cap, new_cap) orelse ptr; 31 | } 32 | 33 | pub fn freeArray(comptime t: type, ptr: [*]t, old_count: usize) void { 34 | _ = reallocate(ptr, old_count, 0); 35 | return; 36 | } 37 | 38 | fn free(comptime t: type, ptr: *t) void { 39 | _ = reallocate(@ptrCast([*]t, ptr), 1, 0); 40 | return; 41 | } 42 | 43 | pub fn allocate(comptime t: type, count: usize) [*]t { 44 | if (count < 1) return undefined; 45 | var tmp: [*]t = undefined; 46 | return reallocate(tmp, 0, count).?; 47 | } 48 | 49 | pub fn reallocate(pointer: anytype, old_size: usize, new_size: usize) ?@TypeOf(pointer) { 50 | const info = @typeInfo(@TypeOf(pointer)); 51 | if (info != .Pointer) { 52 | unreachable; 53 | } 54 | vm.bytes_allocated += @intCast(isize, new_size) - @intCast(isize, old_size); 55 | if (comptime common.stress_gc) { 56 | if (new_size > old_size) { 57 | collectGarbage(); 58 | } 59 | } 60 | if (vm.bytes_allocated > vm.next_gc) { 61 | collectGarbage(); 62 | } 63 | if (new_size == 0) { 64 | alloc.free(pointer[0..old_size]); 65 | return null; 66 | } 67 | if (old_size == 0) { 68 | return (alloc.alloc(info.Pointer.child, new_size) catch std.os.exit(1)).ptr; 69 | } 70 | return (alloc.realloc(pointer[0..old_size], new_size) catch std.os.exit(1)).ptr; 71 | } 72 | 73 | pub fn freeObjects() void { 74 | var obj = VM.objects; 75 | while (obj) |o| { 76 | const next = o.next; 77 | freeObject(o); 78 | obj = next; 79 | } 80 | VM.objects = null; 81 | } 82 | 83 | fn freeObject(o: *Obj) void { 84 | if (comptime common.log_gc) { 85 | const stdout = common.stdout; 86 | stdout.print("{*} free type {any}\n", .{ o, o.t }) catch unreachable; 87 | } 88 | 89 | switch (o.t) { 90 | .obj_string => { 91 | const str = o.asString(); 92 | freeArray(u8, str.chars, str.length + 1); 93 | free(ObjString, str); 94 | }, 95 | .obj_function => { 96 | const function = o.asFunction(); 97 | function.chunk().freeChunk(); 98 | free(ObjFunction, function); 99 | }, 100 | .obj_native => { 101 | free(ObjNative, o.asNative()); 102 | }, 103 | .obj_closure => { 104 | const closure = o.asClosure(); 105 | freeArray(?*ObjUpvalue, closure.upvalues, closure.upvalue_count); 106 | free(ObjClosure, o.asClosure()); 107 | }, 108 | .obj_upvalue => { 109 | free(ObjUpvalue, o.asUpvalue()); 110 | }, 111 | .obj_class => { 112 | o.asClass().methods().freeTable(); 113 | free(ObjClass, o.asClass()); 114 | }, 115 | .obj_instance => { 116 | const instance = o.asInstance(); 117 | instance.fields().freeTable(); 118 | free(ObjInstance, instance); 119 | }, 120 | .obj_bound_method => { 121 | free(ObjBoundMethod, o.asBoundMethod()); 122 | }, 123 | } 124 | } 125 | 126 | fn collectGarbage() void { 127 | if (comptime common.log_gc) { 128 | const stdout = common.stdout; 129 | stdout.print("-- gc begin\n", .{}) catch unreachable; 130 | } 131 | const before = vm.bytes_allocated; 132 | 133 | markRoots(); 134 | traceReferences(); 135 | VM.strings.removeWhite(); 136 | sweep(); 137 | vm.next_gc = vm.bytes_allocated * common.gc_heap_growth_factor; 138 | 139 | if (comptime common.log_gc) { 140 | const stdout = common.stdout; 141 | stdout.print("-- gc end\n", .{}) catch unreachable; 142 | stdout.print(" collected {d} bytes (from {d} to {d}) next at {d}\n", .{ before - vm.bytes_allocated, before, vm.bytes_allocated, vm.next_gc }) catch unreachable; 143 | } 144 | } 145 | 146 | fn markRoots() void { 147 | markObject(@ptrCast(*Obj, vm.init_string)); 148 | for (vm.stack) |*slot| { 149 | markValue(slot); 150 | } 151 | for (vm.frames[0..vm.frame_count]) |frame| { 152 | markObject(@ptrCast(*Obj, frame.closure)); 153 | } 154 | var up_value = vm.open_upvalues; 155 | while (up_value) |uval| : (up_value = uval.next) { 156 | markObject(@ptrCast(*Obj, uval)); 157 | } 158 | vm.globals.markTable(); 159 | compiler.markCompilerRoots(); 160 | } 161 | 162 | fn traceReferences() void { 163 | while (vm.gray_count > 0) { 164 | vm.gray_count -= 1; 165 | var obj = vm.gray_stack[vm.gray_count]; 166 | blackenObject(obj); 167 | } 168 | } 169 | 170 | fn sweep() void { 171 | var previous: ?*Obj = null; 172 | var obj = VM.objects; 173 | while (obj) |o| { 174 | if (o.is_marked) { 175 | o.is_marked = false; 176 | previous = obj; 177 | obj = o.next; 178 | } else { 179 | var unreached = o; 180 | obj = o.next; 181 | if (previous) |p| { 182 | p.next = obj; 183 | } else { 184 | VM.objects = obj; 185 | } 186 | 187 | freeObject(unreached); 188 | } 189 | } 190 | } 191 | 192 | pub fn blackenObject(obj: *Obj) void { 193 | if (comptime common.log_gc) { 194 | const stdout = common.stdout; 195 | stdout.print("{*} blacken ", .{obj}) catch unreachable; 196 | value.printValue(Value.Object(obj), stdout); 197 | stdout.print("\n", .{}) catch unreachable; 198 | } 199 | switch (obj.t) { 200 | .obj_upvalue => { 201 | markValue(obj.asUpvalue().closed()); 202 | }, 203 | .obj_function => { 204 | var function = obj.asFunction(); 205 | markObject(@ptrCast(?*Obj, function.name)); 206 | markArray(&function.chunk().constants); 207 | }, 208 | .obj_closure => { 209 | var closure = obj.asClosure(); 210 | markObject(@ptrCast(*Obj, closure.function)); 211 | for (closure.upvalues[0..closure.upvalue_count]) |uv| { 212 | markObject(@ptrCast(?*Obj, uv)); 213 | } 214 | }, 215 | .obj_instance => { 216 | var instance = obj.asInstance(); 217 | markObject(@ptrCast(*Obj, instance.class)); 218 | instance.fields().markTable(); 219 | }, 220 | .obj_class => { 221 | const class = obj.asClass(); 222 | markObject(@ptrCast(*Obj, class.name)); 223 | class.methods().markTable(); 224 | }, 225 | .obj_bound_method => { 226 | const bound = obj.asBoundMethod(); 227 | markValue(bound.receiver()); 228 | markObject(@ptrCast(*Obj, bound.method)); 229 | }, 230 | .obj_native, .obj_string => {}, 231 | } 232 | } 233 | 234 | pub fn markValue(slot: *Value) void { 235 | if (slot.isObject()) { 236 | markObject(slot.asObject()); 237 | } 238 | } 239 | 240 | pub fn markArray(array: *ValueArray) void { 241 | for (array.values[0..array.count]) |*val| { 242 | markValue(val); 243 | } 244 | } 245 | 246 | pub fn markObject(obj: ?*Obj) void { 247 | if (obj) |o| { 248 | if (o.is_marked) return; 249 | if (comptime common.log_gc) { 250 | const stdout = common.stdout; 251 | stdout.print("{*} mark ", .{o}) catch unreachable; 252 | value.printValue(Value.Object(o), stdout); 253 | stdout.print("\n", .{}) catch unreachable; 254 | } 255 | o.is_marked = true; 256 | 257 | if (vm.gray_stack.len < vm.gray_count + 1) { 258 | const gray_capacity = common.growCapacity(vm.gray_stack.len); 259 | vm.gray_stack = alloc.realloc(vm.gray_stack, gray_capacity) catch std.os.exit(1); 260 | } 261 | vm.gray_stack[vm.gray_count] = o; 262 | vm.gray_count += 1; 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const chunk = @import("./chunk.zig"); 3 | const vm = @import("./vm.zig"); 4 | const common = @import("./common.zig"); 5 | 6 | const stdout = common.stdout; 7 | const stdin = std.io.getStdIn().reader(); 8 | var alloc = @import("./common.zig").alloc; 9 | 10 | var v: vm.VM = .{}; 11 | 12 | pub fn main() anyerror!void { 13 | v.initVM(); 14 | defer v.freeVM(); 15 | var args = std.process.args(); 16 | _ = args.skip(); 17 | var argv: [1][]const u8 = undefined; 18 | var index: u8 = 0; 19 | while (args.next(alloc)) |arg| { 20 | if (index >= 1) break; 21 | argv[index] = arg catch continue; 22 | index += 1; 23 | } 24 | if (index == 0) { 25 | repl(); 26 | } else if (index == 1) { 27 | runFile(argv[0]); 28 | } else { 29 | std.log.err("Usage: clox [path]\n", .{}); 30 | } 31 | } 32 | 33 | fn repl() void { 34 | var line: [1024]u8 = undefined; 35 | while (true) { 36 | stdout.print("> ", .{}) catch unreachable; 37 | 38 | if (stdin.readUntilDelimiterOrEof(&line, '\n') catch null) |slice| { 39 | var source = line[0 .. slice.len + 1]; 40 | source[slice.len] = 0; 41 | _ = v.interpret(source[0..slice.len :0]); 42 | } else { 43 | stdout.print("\n", .{}) catch unreachable; 44 | break; 45 | } 46 | } 47 | } 48 | 49 | fn runFile(path: []const u8) void { 50 | const source = readFile(path); 51 | const result = v.interpret(source); 52 | alloc.free(source); 53 | switch (result) { 54 | vm.InterpretResult.interpret_compile_error => std.os.exit(65), 55 | vm.InterpretResult.interpret_runtime_error => std.os.exit(70), 56 | else => {}, 57 | } 58 | } 59 | 60 | fn readFile(path: []const u8) [:0]const u8 { 61 | var file = std.fs.cwd().openFile(path, .{}) catch { 62 | std.log.err("Could not open file {s}.\n", .{path}); 63 | std.os.exit(74); 64 | }; 65 | defer file.close(); 66 | 67 | var buffer = alloc.alloc(u8, (file.stat() catch std.os.exit(74)).size + 1) catch { 68 | std.log.err("Could not allocate memory.\n", .{}); 69 | std.os.exit(74); 70 | }; 71 | 72 | const length = file.readAll(buffer) catch { 73 | std.log.err("Could not read file {s}.\n", .{path}); 74 | std.os.exit(74); 75 | }; 76 | buffer[length] = 0; 77 | return buffer[0..length :0]; 78 | } 79 | 80 | test "main-test" { 81 | v.initVM(); 82 | defer v.freeVM(); 83 | try std.testing.expectEqual(v.interpret("print -(3*8) == ---24;"), vm.InterpretResult.interpret_ok); 84 | } 85 | 86 | test "main-string" { 87 | v.initVM(); 88 | defer v.freeVM(); 89 | try std.testing.expectEqual(v.interpret("print \"hello!\";"), vm.InterpretResult.interpret_ok); 90 | } 91 | 92 | test "global var" { 93 | v.initVM(); 94 | defer v.freeVM(); 95 | try std.testing.expectEqual(v.interpret( 96 | \\var breakfast = "corn flakes"; 97 | \\breakfast = breakfast + " " + breakfast; 98 | \\print breakfast; 99 | ), vm.InterpretResult.interpret_ok); 100 | } 101 | 102 | test "local var" { 103 | v.initVM(); 104 | defer v.freeVM(); 105 | common.buffer_stream.reset(); 106 | const expected = "hello another world\n"; 107 | try std.testing.expectEqual(v.interpret( 108 | \\var global = "hello"; 109 | \\{ 110 | \\ var local = "world"; 111 | \\ { 112 | \\ var local = " another "; 113 | \\ global = global + local; 114 | \\ } 115 | \\ global = global + local; 116 | \\} 117 | \\print global; 118 | ), vm.InterpretResult.interpret_ok); 119 | try std.testing.expectEqualStrings(expected, common.buffer_stream.getWritten()); 120 | } 121 | 122 | test "loops" { 123 | v.initVM(); 124 | defer v.freeVM(); 125 | common.buffer_stream.reset(); 126 | const expected = "1000\n"; 127 | try std.testing.expectEqual(v.interpret( 128 | \\var counter = 0; 129 | \\for (var index = 0; index < 1000; index = index + 1) { 130 | \\ counter = counter + 1; 131 | \\} 132 | \\print counter; 133 | ), vm.InterpretResult.interpret_ok); 134 | try std.testing.expectEqualStrings(expected, common.buffer_stream.getWritten()); 135 | } 136 | 137 | test "functions" { 138 | v.initVM(); 139 | defer v.freeVM(); 140 | common.buffer_stream.reset(); 141 | const expected = "true\nfalse\ntrue\n"; 142 | try std.testing.expectEqual(v.interpret( 143 | \\fun even(num) { 144 | \\ if (num == 0) return true; 145 | \\ return odd(num - 1); 146 | \\} 147 | \\fun odd(num) { 148 | \\ if (num == 0) return false; 149 | \\ return even(num - 1); 150 | \\} 151 | \\print even(22); 152 | \\print even(21); 153 | \\print odd(3); 154 | ), vm.InterpretResult.interpret_ok); 155 | try std.testing.expectEqualStrings(expected, common.buffer_stream.getWritten()); 156 | } 157 | 158 | test "closure_outer" { 159 | v.initVM(); 160 | defer v.freeVM(); 161 | common.buffer_stream.reset(); 162 | const expected = "outside\n"; 163 | try std.testing.expectEqual(v.interpret( 164 | \\fun outer() { 165 | \\var x = "outside"; 166 | \\fun inner() { 167 | \\print x; 168 | \\} 169 | \\inner(); 170 | \\} 171 | \\outer(); 172 | ), vm.InterpretResult.interpret_ok); 173 | try std.testing.expectEqualStrings(expected, common.buffer_stream.getWritten()); 174 | } 175 | 176 | test "closure_reference" { 177 | v.initVM(); 178 | defer v.freeVM(); 179 | common.buffer_stream.reset(); 180 | const expected = "updated\n"; 181 | try std.testing.expectEqual(v.interpret( 182 | \\var globalSet; 183 | \\var globalGet; 184 | \\fun main() { 185 | \\ var a = "initial"; 186 | \\ fun set() { a = "updated"; } 187 | \\ fun get() { print a; } 188 | \\ globalSet = set; 189 | \\ globalGet = get; 190 | \\} 191 | \\main(); 192 | \\globalSet(); 193 | \\globalGet(); 194 | ), vm.InterpretResult.interpret_ok); 195 | try std.testing.expectEqualStrings(expected, common.buffer_stream.getWritten()); 196 | } 197 | 198 | test "field_access" { 199 | v.initVM(); 200 | defer v.freeVM(); 201 | common.buffer_stream.reset(); 202 | const expected = "3\n"; 203 | try std.testing.expectEqual(v.interpret( 204 | \\class Pair {} 205 | \\var pair = Pair(); 206 | \\pair.first = 1; 207 | \\pair.second = 2; 208 | \\print pair.first + pair.second; 209 | ), vm.InterpretResult.interpret_ok); 210 | try std.testing.expectEqualStrings(expected, common.buffer_stream.getWritten()); 211 | } 212 | 213 | test "methods" { 214 | v.initVM(); 215 | defer v.freeVM(); 216 | common.buffer_stream.reset(); 217 | const expected = "Enjoy your cup of coffee and chicory\n"; 218 | try std.testing.expectEqual(v.interpret( 219 | \\class CoffeeMaker { 220 | \\ init(coffee) { 221 | \\ this.coffee = coffee; 222 | \\ } 223 | \\ brew() { 224 | \\ print "Enjoy your cup of " + this.coffee; 225 | \\ this.coffee = nil; 226 | \\ } 227 | \\} 228 | \\var maker = CoffeeMaker("coffee and chicory"); 229 | \\maker.brew(); 230 | ), vm.InterpretResult.interpret_ok); 231 | try std.testing.expectEqualStrings(expected, common.buffer_stream.getWritten()); 232 | } 233 | 234 | test "field call" { 235 | v.initVM(); 236 | defer v.freeVM(); 237 | common.buffer_stream.reset(); 238 | const expected = "indirection\n"; 239 | try std.testing.expectEqual(v.interpret( 240 | \\class CoffeeMaker { 241 | \\ init() { 242 | \\ fun f() { 243 | \\ print this.word; 244 | \\ } 245 | \\ this.fn = f; 246 | \\ } 247 | \\} 248 | \\var maker = CoffeeMaker(); 249 | \\maker.word = "indirection"; 250 | \\maker.fn(); 251 | ), vm.InterpretResult.interpret_ok); 252 | try std.testing.expectEqualStrings(expected, common.buffer_stream.getWritten()); 253 | } 254 | 255 | test "super" { 256 | v.initVM(); 257 | defer v.freeVM(); 258 | common.buffer_stream.reset(); 259 | const expected = "Cutting food...\nCooking at 90 degrees!\n"; 260 | try std.testing.expectEqual(v.interpret( 261 | \\class Base { 262 | \\ cook() { 263 | \\ print "Cooking at " + this.getTemp(); 264 | \\ } 265 | \\ getTemp() { 266 | \\ return "100 degrees!"; 267 | \\ } 268 | \\} 269 | \\class Derived < Base { 270 | \\ getTemp() { 271 | \\ return "90 degrees!"; 272 | \\ } 273 | \\ cook() { 274 | \\ print "Cutting food..."; 275 | \\ super.cook(); 276 | \\ } 277 | \\} 278 | \\var d = Derived(); 279 | \\d.cook(); 280 | ), vm.InterpretResult.interpret_ok); 281 | try std.testing.expectEqualStrings(expected, common.buffer_stream.getWritten()); 282 | } 283 | -------------------------------------------------------------------------------- /src/object.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const memory = @import("./memory.zig"); 3 | const table = @import("./table.zig"); 4 | const common = @import("./common.zig"); 5 | const VM = @import("./vm.zig").VM; 6 | const Value = @import("./value.zig").Value; 7 | const Chunk = @import("./chunk.zig").Chunk; 8 | const Table = table.Table; 9 | const alloc = common.alloc; 10 | 11 | var vm: *VM = undefined; 12 | 13 | pub fn initGC(_vm: *VM) void { 14 | vm = _vm; 15 | } 16 | 17 | pub const ObjType = enum(u8) { obj_bound_method, obj_string, obj_function, obj_native, obj_closure, obj_upvalue, obj_class, obj_instance }; 18 | 19 | pub const Obj = extern struct { 20 | const Self = @This(); 21 | t: ObjType, 22 | is_marked: bool, 23 | next: ?*Obj = null, 24 | 25 | pub fn asString(self: *Self) *ObjString { 26 | return @ptrCast(*ObjString, @alignCast(@alignOf(ObjString), self)); 27 | } 28 | 29 | pub fn asFunction(self: *Self) *ObjFunction { 30 | return @ptrCast(*ObjFunction, @alignCast(@alignOf(ObjFunction), self)); 31 | } 32 | 33 | pub fn asNative(self: *Self) *ObjNative { 34 | return @ptrCast(*ObjNative, @alignCast(@alignOf(ObjNative), self)); 35 | } 36 | 37 | pub fn asClosure(self: *Self) *ObjClosure { 38 | return @ptrCast(*ObjClosure, @alignCast(@alignOf(ObjClosure), self)); 39 | } 40 | 41 | pub fn asUpvalue(self: *Self) *ObjUpvalue { 42 | return @ptrCast(*ObjUpvalue, @alignCast(@alignOf(ObjUpvalue), self)); 43 | } 44 | 45 | pub fn asClass(self: *Self) *ObjClass { 46 | return @ptrCast(*ObjClass, @alignCast(@alignOf(ObjClass), self)); 47 | } 48 | 49 | pub fn asInstance(self: *Self) *ObjInstance { 50 | return @ptrCast(*ObjInstance, @alignCast(@alignOf(ObjInstance), self)); 51 | } 52 | 53 | pub fn asBoundMethod(self: *Self) *ObjBoundMethod { 54 | return @ptrCast(*ObjBoundMethod, @alignCast(@alignOf(ObjBoundMethod), self)); 55 | } 56 | 57 | pub fn print(self: *Self, writer: anytype) void { 58 | switch (self.t) { 59 | .obj_string => { 60 | writer.print("{s}", .{self.asString().chars[0..self.asString().length]}) catch unreachable; 61 | }, 62 | .obj_function => { 63 | printFunction(self.asFunction(), writer); 64 | }, 65 | .obj_native => { 66 | writer.print("", .{}) catch unreachable; 67 | }, 68 | .obj_closure => { 69 | printFunction(self.asClosure().function, writer); 70 | }, 71 | .obj_upvalue => { 72 | writer.print("upvalue", .{}) catch unreachable; 73 | }, 74 | .obj_class => { 75 | writer.print("{s}", .{self.asClass().name.chars[0..self.asClass().name.length]}) catch unreachable; 76 | }, 77 | .obj_instance => { 78 | writer.print("{s} instance", .{self.asInstance().class.name.chars[0..self.asInstance().class.name.length]}) catch unreachable; 79 | }, 80 | .obj_bound_method => { 81 | printFunction(self.asBoundMethod().method.function, writer); 82 | }, 83 | } 84 | } 85 | }; 86 | 87 | pub const ObjFunction = extern struct { 88 | obj: Obj, 89 | arity: usize, 90 | _chunk: [@sizeOf(Chunk)]u8 align(@alignOf(Chunk)) = undefined, 91 | name: ?*ObjString, 92 | upvalue_count: u8, 93 | pub inline fn chunk(self: *ObjFunction) *Chunk { 94 | return @ptrCast(*Chunk, &self._chunk); 95 | } 96 | }; 97 | 98 | pub const ObjClosure = extern struct { 99 | obj: Obj, 100 | function: *ObjFunction, 101 | upvalues: [*]?*ObjUpvalue, 102 | upvalue_count: usize, 103 | }; 104 | 105 | pub const ObjClass = extern struct { 106 | obj: Obj, 107 | name: *ObjString, 108 | _methods: [@sizeOf(Table)]u8 align(@alignOf(Table)), 109 | 110 | pub inline fn methods(self: *ObjClass) *Table { 111 | return @ptrCast(*Table, &self._methods); 112 | } 113 | }; 114 | 115 | pub const ObjInstance = extern struct { 116 | obj: Obj, 117 | class: *ObjClass, 118 | _fields: [@sizeOf(Table)]u8 align(@alignOf(Table)), 119 | 120 | pub inline fn fields(self: *ObjInstance) *Table { 121 | return @ptrCast(*Table, &self._fields); 122 | } 123 | }; 124 | 125 | pub const ObjBoundMethod = extern struct { 126 | obj: Obj, 127 | _receiver: [@sizeOf(Value)]u8 align(@alignOf(Value)), 128 | method: *ObjClosure, 129 | 130 | pub inline fn receiver(self: *ObjBoundMethod) *Value { 131 | return @ptrCast(*Value, &self._receiver); 132 | } 133 | }; 134 | 135 | pub const NativeFn = fn (arg_count: usize, args: [*]Value) Value; 136 | 137 | pub const ObjNative = extern struct { 138 | obj: Obj, 139 | _function: [@sizeOf(NativeFn)]u8 align(@alignOf(NativeFn)) = undefined, 140 | 141 | pub inline fn function(self: *ObjNative) *NativeFn { 142 | return @ptrCast(*NativeFn, &self._function); 143 | } 144 | }; 145 | 146 | pub const ObjString = extern struct { 147 | obj: Obj, 148 | length: usize, 149 | chars: [*]u8, 150 | hash: u32, 151 | }; 152 | 153 | pub const ObjUpvalue = extern struct { 154 | obj: Obj, 155 | location: *Value, 156 | next: ?*ObjUpvalue, 157 | _closed: [@sizeOf(Value)]u8 align(@alignOf(Value)), 158 | 159 | pub inline fn closed(self: *ObjUpvalue) *Value { 160 | return @ptrCast(*Value, &self._closed); 161 | } 162 | }; 163 | 164 | pub fn newInstance(class: *ObjClass) *Obj { 165 | const instance = allocateObject(ObjInstance, .obj_instance); 166 | instance.class = class; 167 | instance.fields().* = Table.initTable(); 168 | return @ptrCast(*Obj, instance); 169 | } 170 | 171 | pub fn newBoundMethod(receiver: Value, method: *ObjClosure) *Obj { 172 | const bound = allocateObject(ObjBoundMethod, .obj_bound_method); 173 | bound.receiver().* = receiver; 174 | bound.method = method; 175 | return @ptrCast(*Obj, bound); 176 | } 177 | 178 | pub fn newClass(name: *ObjString) *Obj { 179 | const class = allocateObject(ObjClass, .obj_class); 180 | class.name = name; 181 | class.methods().* = Table.initTable(); 182 | return @ptrCast(*Obj, class); 183 | } 184 | 185 | pub fn newClosure(function: *ObjFunction) *ObjClosure { 186 | const upvalues = memory.allocate(?*ObjUpvalue, function.upvalue_count); 187 | for (upvalues[0..function.upvalue_count]) |*upvalue| { 188 | upvalue.* = null; 189 | } 190 | var closure = allocateObject(ObjClosure, .obj_closure); 191 | closure.upvalues = upvalues; 192 | closure.upvalue_count = function.upvalue_count; 193 | closure.function = function; 194 | return closure; 195 | } 196 | 197 | pub fn newFunction() *ObjFunction { 198 | var function = allocateObject(ObjFunction, .obj_function); 199 | function.arity = 0; 200 | function.name = null; 201 | function.chunk().* = Chunk.init(alloc); 202 | function.upvalue_count = 0; 203 | return function; 204 | } 205 | 206 | pub fn newNative(function: NativeFn) *Obj { 207 | var native = allocateObject(ObjNative, .obj_native); 208 | native.function().* = function; 209 | return @ptrCast(*Obj, native); 210 | } 211 | 212 | pub fn newUpvalue(slot: *Value) *ObjUpvalue { 213 | var upvalue = allocateObject(ObjUpvalue, .obj_upvalue); 214 | upvalue.location = slot; 215 | upvalue.next = null; 216 | upvalue.closed().* = Value.Nil(); 217 | return upvalue; 218 | } 219 | 220 | pub fn copyString(chars: [*]const u8, length: usize) *Obj { 221 | const hash = hashString(chars, length); 222 | const interned = VM.strings.findString(chars, length, hash); 223 | var string: *ObjString = undefined; 224 | if (interned) |i| { 225 | string = i; 226 | } else { 227 | var heap_chars = memory.allocate(u8, length + 1)[0 .. length + 1]; 228 | std.mem.copy(u8, heap_chars, chars[0..length]); 229 | heap_chars[length] = 0; 230 | string = allocateString(heap_chars[0..length], hash); 231 | } 232 | 233 | return @ptrCast(*Obj, string); 234 | } 235 | 236 | pub fn takeString(chars: [*]u8, length: usize) *Obj { 237 | const hash = hashString(chars, length); 238 | const interned = VM.strings.findString(chars, length, hash); 239 | var string: *ObjString = undefined; 240 | if (interned) |i| { 241 | memory.freeArray(u8, chars, length + 1); 242 | string = i; 243 | } else { 244 | string = allocateString(chars[0..length], hash); 245 | } 246 | return @ptrCast(*Obj, string); 247 | } 248 | 249 | fn printFunction(function: *ObjFunction, writer: anytype) void { 250 | if (function.name) |name| { 251 | writer.print("", .{name.chars[0..name.length]}) catch unreachable; 252 | } else { 253 | writer.print("