diff --git a/README.md b/README.md index cbe04e9..515100f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ While reading [Crafting Interpreters](https://craftinginterpreters.com/), after - [x] 24 - Calls and Functions - [x] 25 - Closures - [x] 26 - Garbage Collection -- [ ] 27 - Classes and Instances +- [x] 27 - Classes and Instances - [ ] 28 - Method and Initializers - [ ] 29 - Superclasses - [ ] 30 - Optimization diff --git a/samples/ch27_classes_instances.lox b/samples/ch27_classes_instances.lox new file mode 100644 index 0000000..5ce90cf --- /dev/null +++ b/samples/ch27_classes_instances.lox @@ -0,0 +1,16 @@ +class Brioche {} +print Brioche(); + +class Toast {} +var toast = Toast(); +print toast; +print toast.jam = "grape"; +print toast.jam; + +class Pair {} +var pair = Pair(); + +pair.first = 1; +pair.second = 2; + +print pair.first + pair.second; \ No newline at end of file diff --git a/src/chunk.zig b/src/chunk.zig index 26479c6..d5282b8 100644 --- a/src/chunk.zig +++ b/src/chunk.zig @@ -148,6 +148,9 @@ pub const Chunk = struct { @intFromEnum(OpCode.OP_GET_UPVALUE) => return utils.byte_instruction("OP_GET_UPVALUE", self, offset), @intFromEnum(OpCode.OP_SET_UPVALUE) => return utils.byte_instruction("OP_SET_UPVALUE", self, offset), @intFromEnum(OpCode.OP_CLOSE_UPVALUE) => return utils.simple_instruction("OP_CLOSE_UPVALUE", offset), + @intFromEnum(OpCode.OP_CLASS) => return utils.constant_instruction("OP_CLASS", self, offset), + @intFromEnum(OpCode.OP_GET_PROPERTY) => return utils.constant_instruction("OP_GET_PROPERTY", self, offset), + @intFromEnum(OpCode.OP_SET_PROPERTY) => return utils.constant_instruction("OP_SET_PROPERTY", self, offset), else => { debug.print("unknown opcode {d}\n", .{instruction}); return offset + 1; diff --git a/src/compile.zig b/src/compile.zig index b2e744b..f9676bc 100644 --- a/src/compile.zig +++ b/src/compile.zig @@ -249,7 +249,7 @@ pub const Parser = struct { TokenType.LEFT_BRACE => ParserRule{ .prefix = null, .infix = null, .precedence = Precedence.None }, TokenType.RIGHT_BRACE => ParserRule{ .prefix = null, .infix = null, .precedence = Precedence.None }, TokenType.COMMA => ParserRule{ .prefix = null, .infix = null, .precedence = Precedence.None }, - TokenType.DOT => ParserRule{ .prefix = null, .infix = null, .precedence = Precedence.None }, + TokenType.DOT => ParserRule{ .prefix = null, .infix = dot, .precedence = Precedence.Call }, TokenType.MINUS => ParserRule{ .prefix = unary, .infix = binary, .precedence = Precedence.Term }, TokenType.PLUS => ParserRule{ .prefix = null, .infix = binary, .precedence = Precedence.Term }, TokenType.SEMICOLON => ParserRule{ .prefix = null, .infix = null, .precedence = Precedence.None }, @@ -363,7 +363,9 @@ pub const Parser = struct { } fn declaration(self: *Parser) ParsingError!void { - if (self.match(TokenType.FUN)) { + if (self.match(TokenType.CLASS)) { + try self.class_declaration(); + } else if (self.match(TokenType.FUN)) { try self.fun_declaration(); } else if (self.match(TokenType.VAR)) { try self.var_declaration(); @@ -858,6 +860,30 @@ pub const Parser = struct { try self.emit_byte(@intFromEnum(OpCode.OP_RETURN)); } } + + fn class_declaration(self: *Parser) ParsingError!void { + self.consume(TokenType.IDENTIFIER, "Expect class name."); + const name_constant = try self.identifier_constant(self.previous.?); + self.declare_variable(); + + try self.emit_bytes(@intFromEnum(OpCode.OP_CLASS), name_constant); + try self.define_variable(name_constant); + + self.consume(TokenType.LEFT_BRACE, "Expect '{' before class body."); + self.consume(TokenType.RIGHT_BRACE, "Expect '}' after class body."); + } + + fn dot(self: *Parser, can_assign: bool) ParsingError!void { + self.consume(TokenType.IDENTIFIER, "Expect property name after '.'."); + const name = try self.identifier_constant(self.previous.?); + + if (can_assign and self.match(TokenType.EQUAL)) { + try self.expression(); + try self.emit_bytes(@intFromEnum(OpCode.OP_SET_PROPERTY), name); + } else { + try self.emit_bytes(@intFromEnum(OpCode.OP_GET_PROPERTY), name); + } + } }; const FunctionType = enum { diff --git a/src/memory.zig b/src/memory.zig index 2d8247d..d29876f 100644 --- a/src/memory.zig +++ b/src/memory.zig @@ -234,6 +234,15 @@ pub const ZloxAllocator = struct { } } }, + ObjType.Class => { + const class: *Obj.Class = obj.as_class(); + self.mark_object(&class.name.obj); + }, + ObjType.Instance => { + const instance: *Obj.Instance = obj.as_instance(); + self.mark_object(&instance.class.obj); + self.mark_table(&instance.fields); + }, } } diff --git a/src/object.zig b/src/object.zig index e711f5a..8653bd7 100644 --- a/src/object.zig +++ b/src/object.zig @@ -3,6 +3,7 @@ const debug = std.debug; const Allocator = std.mem.Allocator; const Chunk = @import("./chunk.zig").Chunk; +const Table = @import("./table.zig").Table; const Value = @import("./values.zig").Value; const VM = @import("./vm.zig").VM; @@ -14,6 +15,8 @@ pub const ObjType = enum { Native, Closure, Upvalue, + Class, + Instance, }; pub const NativeFn = *const fn (vm: *VM, arg_count: usize, args: []Value) Value; @@ -46,6 +49,8 @@ pub const Obj = struct { ObjType.Native => self.as_native().destroy(), ObjType.Closure => self.as_closure().destroy(), ObjType.Upvalue => self.as_upvalue().destroy(), + ObjType.Class => self.as_class().destroy(), + ObjType.Instance => self.as_instance().destroy(), } } @@ -158,6 +163,43 @@ pub const Obj = struct { } }; + pub const Class = struct { + obj: Obj, + name: *Obj.String, + + pub fn new(vm: *VM, name: *Obj.String) *Class { + const class_obj = Obj.new(Class, vm, ObjType.Class); + + class_obj.name = name; + + return class_obj; + } + + pub fn destroy(self: *Class) void { + self.obj.allocator.destroy(self); + } + }; + + pub const Instance = struct { + obj: Obj, + class: *Obj.Class, + fields: Table, + + pub fn new(vm: *VM, class: *Obj.Class) *Instance { + const instance_obj = Obj.new(Instance, vm, ObjType.Instance); + + instance_obj.class = class; + instance_obj.fields = Table.new(vm.allocator); + + return instance_obj; + } + + pub fn destroy(self: *Instance) void { + self.fields.destroy(); + self.obj.allocator.destroy(self); + } + }; + pub fn is_type(self: *Obj, kind: ObjType) bool { return self.kind == kind; } @@ -182,6 +224,14 @@ pub const Obj = struct { return self.is_type(ObjType.Upvalue); } + pub fn is_class(self: *Obj) bool { + return self.is_type(ObjType.Class); + } + + pub fn is_instance(self: *Obj) bool { + return self.is_type(ObjType.Instance); + } + pub fn print(self: *Obj) void { switch (self.kind) { ObjType.String => { @@ -206,6 +256,14 @@ pub const Obj = struct { ObjType.Upvalue => { debug.print("upvalue", .{}); }, + ObjType.Class => { + const obj = self.as_class(); + debug.print("{s}", .{obj.name.chars}); + }, + ObjType.Instance => { + const obj = self.as_instance(); + debug.print("{s} instance", .{obj.class.name.chars}); + }, } } @@ -233,4 +291,14 @@ pub const Obj = struct { std.debug.assert(self.kind == ObjType.Upvalue); return @fieldParentPtr("obj", self); } + + pub fn as_class(self: *Obj) *Class { + std.debug.assert(self.kind == ObjType.Class); + return @fieldParentPtr("obj", self); + } + + pub fn as_instance(self: *Obj) *Instance { + std.debug.assert(self.kind == ObjType.Instance); + return @fieldParentPtr("obj", self); + } }; diff --git a/src/opcode.zig b/src/opcode.zig index 07bbb5d..96dce07 100644 --- a/src/opcode.zig +++ b/src/opcode.zig @@ -11,6 +11,8 @@ pub const OpCode = enum(u8) { OP_SET_LOCAL, OP_GET_UPVALUE, OP_SET_UPVALUE, + OP_GET_PROPERTY, + OP_SET_PROPERTY, OP_EQUAL, OP_GREATER, OP_LESS, @@ -28,4 +30,5 @@ pub const OpCode = enum(u8) { OP_CLOSURE, OP_CLOSE_UPVALUE, OP_RETURN, + OP_CLASS, }; diff --git a/src/scanner.zig b/src/scanner.zig index 581df37..f60d114 100644 --- a/src/scanner.zig +++ b/src/scanner.zig @@ -316,7 +316,7 @@ pub const Scanner = struct { pub fn identifier_type(self: *Scanner) TokenType { return switch (self.source[self.start]) { 'a' => self.check_keyword(1, 2, "nd", TokenType.AND), - 'c' => self.check_keyword(1, 4, "class", TokenType.CLASS), + 'c' => self.check_keyword(1, 4, "lass", TokenType.CLASS), 'e' => self.check_keyword(1, 3, "lse", TokenType.ELSE), 'f' => if (self.current - self.start > 1) { return switch (self.source[self.start + 1]) { diff --git a/src/vm.zig b/src/vm.zig index d61e0e7..d75126d 100644 --- a/src/vm.zig +++ b/src/vm.zig @@ -290,6 +290,42 @@ pub const VM = struct { self.close_upvalues(&self.stack[self.stack_top - 1]); _ = self.pop(); }, + @intFromEnum(OpCode.OP_CLASS) => { + const name: *Obj.String = self.read_constant().as_string(); + _ = try self.push(Value.obj_val(&Obj.Class.new(self, name).obj)); + }, + @intFromEnum(OpCode.OP_GET_PROPERTY) => { + if (!self.peek(0).is_obj() or !self.peek(0).as_obj().is_instance()) { + self.runtime_error("Only instances have properties."); + return InterpretResult.RUNTIME_ERROR; + } + + const instance = self.peek(0).as_obj().as_instance(); + const name = self.read_constant().as_string(); + + var value = Value.nil_val(); + + if (instance.fields.get(name, &value)) { + _ = self.pop(); + _ = try self.push(value); + continue; + } + + self.runtime_error("Undefined property"); // XXX to complete with name.chars + return InterpretResult.RUNTIME_ERROR; + }, + @intFromEnum(OpCode.OP_SET_PROPERTY) => { + if (!self.peek(1).is_obj() or !self.peek(1).as_obj().is_instance()) { + self.runtime_error("Only instances have fields."); + return InterpretResult.RUNTIME_ERROR; + } + const instance = self.peek(1).as_obj().as_instance(); + _ = instance.fields.set(self.read_constant().as_string(), self.peek(0)); + + const value = self.pop(); + _ = self.pop(); + _ = try self.push(value); + }, else => { debug.print("Invalid instruction: {d}\n", .{instruction}); return InterpretResult.RUNTIME_ERROR; @@ -456,6 +492,12 @@ pub const VM = struct { ObjType.Closure => { return self.call(callee.as_obj().as_closure(), arg_count); }, + ObjType.Class => { + const class = callee.as_obj().as_class(); + self.stack[self.stack_top - arg_count - 1] = Value.obj_val(&Obj.Instance.new(self, class).obj); + + return true; + }, else => {}, } }