diff --git a/README.md b/README.md index 2cfb93b..bc3ce7a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ While reading [Crafting Interpreters](https://craftinginterpreters.com/), after - [x] 19 - Strings - [x] 20 - Hash Tables - [x] 21 - Global Variables -- [ ] 22 - Local Variables +- [x] 22 - Local Variables - [ ] 23 - Jumping Back and Forth - [ ] 24 - Calls and Functions - [ ] 25 - Closures diff --git a/samples/ch22_local_vars1.lox b/samples/ch22_local_vars1.lox new file mode 100644 index 0000000..8680dd1 --- /dev/null +++ b/samples/ch22_local_vars1.lox @@ -0,0 +1,4 @@ +{ + var a = "first"; + var a = "second"; +} diff --git a/samples/ch22_local_vars2.lox b/samples/ch22_local_vars2.lox new file mode 100644 index 0000000..d420c1f --- /dev/null +++ b/samples/ch22_local_vars2.lox @@ -0,0 +1,20 @@ +// This is showing: +// +// global +// outer +// inner +// outer +// global + +var a = "global"; +{ + print "in outer block: " + a; + var a = "outer"; + { + print "in inner block: " + a; + var a = "inner"; + print "in inner block: " + a; + } + print "in outer block: " + a; +} +print "global: " + a; diff --git a/samples/ch22_local_vars3.lox b/samples/ch22_local_vars3.lox new file mode 100644 index 0000000..53e2c4e --- /dev/null +++ b/samples/ch22_local_vars3.lox @@ -0,0 +1,8 @@ +{ + var a = "outer"; + { + // this is reporting "Error at 'a': Can't read local variable in its own initializer." + var a = a; + print a; + } +} \ No newline at end of file diff --git a/src/chunk.zig b/src/chunk.zig index f7499ec..f3aa25f 100644 --- a/src/chunk.zig +++ b/src/chunk.zig @@ -97,6 +97,8 @@ pub const Chunk = struct { @intFromEnum(OpCode.OP_DEFINE_GLOBAL) => return utils.constant_instruction("OP_DEFINE_GLOBAL", self, offset), @intFromEnum(OpCode.OP_GET_GLOBAL) => return utils.constant_instruction("OP_GET_GLOBAL", self, offset), @intFromEnum(OpCode.OP_SET_GLOBAL) => return utils.constant_instruction("OP_SET_GLOBAL", self, offset), + @intFromEnum(OpCode.OP_GET_LOCAL) => return utils.byte_instruction("OP_GET_LOCAL", self, offset), + @intFromEnum(OpCode.OP_SET_LOCAL) => return utils.byte_instruction("OP_SET_LOCAL", self, offset), else => { debug.print("unknown opcode {d}\n", .{instruction}); return offset + 1; diff --git a/src/compile.zig b/src/compile.zig index e295a3c..e6868ea 100644 --- a/src/compile.zig +++ b/src/compile.zig @@ -15,6 +15,8 @@ const VM = @import("./vm.zig").VM; const ParsingError = @import("./errors.zig").ParsingError; +const identifiers_equals = @import("./utils.zig").identifiers_equals; + const Precedence = enum { None, Assignement, @@ -36,6 +38,7 @@ const ParserRule = struct { }; const Parser = struct { + compiler: *Compiler, current: ?Token, previous: ?Token, scanner: *Scanner, @@ -44,8 +47,9 @@ const Parser = struct { chunk: *Chunk, vm: *VM, - fn new(vm: *VM, scanner: *Scanner, chunk: *Chunk) Parser { + fn new(vm: *VM, compiler: *Compiler, scanner: *Scanner, chunk: *Chunk) Parser { return Parser{ + .compiler = compiler, .current = null, .previous = null, .scanner = scanner, @@ -318,12 +322,26 @@ const Parser = struct { } fn named_variable(self: *Parser, token: Token, can_assign: bool) ParsingError!void { - const constant = try self.identifier_constant(token); + var get_op: OpCode = OpCode.OP_GET_LOCAL; + var set_op: OpCode = OpCode.OP_SET_LOCAL; + var has_local = true; + + var constant = self.resolve_local(token) catch blk: { + has_local = false; + break :blk 0; + }; + + if (!has_local) { + constant = try self.identifier_constant(token); + get_op = OpCode.OP_GET_GLOBAL; + set_op = OpCode.OP_SET_GLOBAL; + } + if (can_assign and self.match(TokenType.EQUAL)) { try self.expression(); - try self.emit_bytes(@intFromEnum(OpCode.OP_SET_GLOBAL), constant); + try self.emit_bytes(@intFromEnum(set_op), constant); } else { - try self.emit_bytes(@intFromEnum(OpCode.OP_GET_GLOBAL), constant); + try self.emit_bytes(@intFromEnum(get_op), constant); } } @@ -342,6 +360,10 @@ const Parser = struct { fn statement(self: *Parser) ParsingError!void { if (self.match(TokenType.PRINT)) { try self.print_statement(); + } else if (self.match(TokenType.LEFT_BRACE)) { + self.begin_scope(); + try self.block(); + try self.end_scope(); } else { try self.expression_statement(); } @@ -410,6 +432,12 @@ const Parser = struct { fn parse_variable(self: *Parser, err_msg: []const u8) ParsingError!u8 { self.consume(TokenType.IDENTIFIER, err_msg); + + self.declare_variable(); + if (self.compiler.scope_depth > 0) { + return 0; + } + return self.identifier_constant(self.previous.?); } @@ -420,15 +448,138 @@ const Parser = struct { } fn define_variable(self: *Parser, global: u8) ParsingError!void { + if (self.compiler.scope_depth > 0) { + self.mark_initialized(); + return; + } + return self.emit_bytes(@intFromEnum(OpCode.OP_DEFINE_GLOBAL), global); } + + fn mark_initialized(self: *Parser) void { + self.compiler.locals[self.compiler.local_count - 1].depth = self.compiler.scope_depth; + } + + fn declare_variable(self: *Parser) void { + if (self.compiler.scope_depth == 0) { + return; + } + const name = self.previous.?; + + if (self.compiler.local_count == 0) { + self.add_local(name); + return; + } + + var idx = self.compiler.local_count - 1; + + while (idx >= 0) { + const local = &self.compiler.locals[idx]; + + if (local.depth != null and local.depth.? < self.compiler.scope_depth) { + break; + } + + if (identifiers_equals(name, local.name)) { + self.error_msg("Already a variable with this name in this scope."); + } + + if (idx == 0) { + break; + } + idx -= 1; + } + + self.add_local(name); + } + + fn block(self: *Parser) ParsingError!void { + while (!self.check(TokenType.RIGHT_BRACE) and !self.check(TokenType.EOF)) { + try self.declaration(); + } + + self.consume(TokenType.RIGHT_BRACE, "Expect '}' after block."); + } + + fn begin_scope(self: *Parser) void { + self.compiler.scope_depth += 1; + } + + fn end_scope(self: *Parser) !void { + self.compiler.scope_depth -= 1; + + while (self.compiler.local_count > 0 and self.compiler.locals[self.compiler.local_count - 1].depth.? > self.compiler.scope_depth) { + try self.emit_byte(@intFromEnum(OpCode.OP_POP)); + self.compiler.local_count -= 1; + } + } + + fn add_local(self: *Parser, token: Token) void { + if (self.compiler.local_count == 256) { + self.error_msg("Too many local variables in function."); + return; + } + + var local = &self.compiler.locals[self.compiler.local_count]; + self.compiler.local_count += 1; + + local.name = token; + local.depth = null; + } + + fn resolve_local(self: *Parser, name: Token) !u8 { + if (self.compiler.local_count == 0) { + return ParsingError.NotFound; + } + + var idx: u8 = @intCast(self.compiler.local_count - 1); + + while (idx >= 0) { + const local = &self.compiler.locals[idx]; + + if (identifiers_equals(local.name, name)) { + if (local.depth == null) { + self.error_msg("Can't read local variable in its own initializer."); + } + return idx; + } + + if (idx == 0) { + break; + } + idx -= 1; + } + + return ParsingError.NotFound; + } +}; + +const Compiler = struct { + locals: [256]Local, + local_count: usize, + scope_depth: usize, + + fn new() Compiler { + return Compiler{ + .locals = undefined, + .local_count = 0, + .scope_depth = 0, + }; + } +}; + +const Local = struct { + name: Token, + depth: ?usize, }; pub fn compile(allocator: Allocator, vm: *VM, contents: []const u8, chunk: *Chunk) !bool { _ = allocator; + var compiler = Compiler.new(); + var scanner = Scanner.init(contents); - var parser = Parser.new(vm, &scanner, chunk); + var parser = Parser.new(vm, &compiler, &scanner, chunk); parser.advance(); diff --git a/src/errors.zig b/src/errors.zig index 83f1e9f..612cfeb 100644 --- a/src/errors.zig +++ b/src/errors.zig @@ -1,5 +1,6 @@ pub const ParsingError = error{ FloatConv, OutOfMemory, + NotFound, Unknown, }; diff --git a/src/opcode.zig b/src/opcode.zig index 3119bb5..5d5a2d5 100644 --- a/src/opcode.zig +++ b/src/opcode.zig @@ -7,6 +7,8 @@ pub const OpCode = enum(u8) { OP_GET_GLOBAL, OP_DEFINE_GLOBAL, OP_SET_GLOBAL, + OP_GET_LOCAL, + OP_SET_LOCAL, OP_EQUAL, OP_GREATER, OP_LESS, diff --git a/src/utils.zig b/src/utils.zig index 0b3fced..f6d5901 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -2,6 +2,8 @@ const std = @import("std"); const debug = std.debug; const Chunk = @import("./chunk.zig").Chunk; +const Token = @import("./scanner.zig").Token; + const print_value = @import("./values.zig").print_value; pub fn grow_capacity(capacity: usize) usize { @@ -25,6 +27,13 @@ pub fn constant_instruction(opcode_name: []const u8, chunk: Chunk, offset: usize return offset + 2; } +pub fn byte_instruction(opcode_name: []const u8, chunk: Chunk, offset: usize) usize { + const slot = chunk.code[offset + 1]; + debug.print("{s:16} {d:4}\n", .{ opcode_name, slot }); + + return offset + 2; +} + pub fn compute_hash(str: []const u8) u32 { var res_hash: u32 = 2166136261; @@ -35,3 +44,11 @@ pub fn compute_hash(str: []const u8) u32 { return res_hash; } + +pub fn identifiers_equals(a: Token, b: Token) bool { + if (a.length != b.length) { + return false; + } + + return std.mem.eql(u8, a.start[0..a.length], b.start[0..b.length]); +} diff --git a/src/vm.zig b/src/vm.zig index 76e75b6..5ab7a4c 100644 --- a/src/vm.zig +++ b/src/vm.zig @@ -174,6 +174,14 @@ pub const VM = struct { return InterpretResult.RUNTIME_ERROR; } }, + @intFromEnum(OpCode.OP_GET_LOCAL) => { + const slot = self.read_byte(); + try self.push(self.stack.items[slot]); + }, + @intFromEnum(OpCode.OP_SET_LOCAL) => { + const slot = self.read_byte(); + self.stack.items[slot] = self.peek(0); + }, else => { debug.print("Invalid instruction: {d}\n", .{instruction}); return InterpretResult.RUNTIME_ERROR;