From c940374f27cb5a6d793aa5e258d64fea0686a30c Mon Sep 17 00:00:00 2001 From: Brett Broadhurst Date: Wed, 18 Mar 2026 17:52:07 -0600 Subject: [PATCH] refactor: new direction for error reporting --- src/Ast.zig | 62 +++-- src/Ast/Render.zig | 80 +----- src/AstGen.zig | 528 +++++++++++++++++++----------------- src/Ir.zig | 215 +++++++++------ src/Sema.zig | 110 +------- src/Story.zig | 57 ++-- src/Story/runtime_tests.zig | 2 +- src/compile.zig | 229 ++++++++++++++++ src/error_tests.zig | 37 +++ src/main.zig | 19 +- src/root.zig | 1 + 11 files changed, 758 insertions(+), 582 deletions(-) create mode 100644 src/compile.zig create mode 100644 src/error_tests.zig diff --git a/src/Ast.zig b/src/Ast.zig index 1331952..47629b2 100644 --- a/src/Ast.zig +++ b/src/Ast.zig @@ -1,14 +1,17 @@ +//! Abstract Syntax Tree for Ink source code. const std = @import("std"); const ink = @import("root.zig"); const Tokenizer = @import("tokenizer.zig").Tokenizer; const Render = @import("Ast/Render.zig"); const Parse = @import("Parse.zig"); const Ast = @This(); +const assert = std.debug.assert; filename: []const u8, source: []const u8, +// TODO: Make this non-nullable. Empty files are valid. root: ?*Node = null, -errors: []Error, +errors: []const Error, pub const Node = struct { tag: Tag, @@ -215,24 +218,22 @@ pub const Error = struct { }; pub const Tag = enum { - panic, - unexpected_token, - unknown_identifier, - redefined_identifier, - assignment_to_const, + expected_token, expected_newline, - expected_expression, expected_double_quote, expected_identifier, + expected_expression, invalid_expression, invalid_lvalue, too_many_arguments, too_many_parameters, - - invalid_else_stmt, - unexpected_else_stmt, - invalid_switch_case, + unexpected_token, }; + + pub fn tokenSlice(err: Error, tree: Ast) []const u8 { + assert(err.loc.start <= err.loc.end); + return tree.source[err.loc.start..err.loc.end]; + } }; pub fn parse( @@ -280,16 +281,35 @@ pub fn render( return Render.renderTree(gpa, writer, &ast, options); } -pub fn renderErrors( - ast: Ast, - gpa: std.mem.Allocator, - writer: *std.Io.Writer, - options: Render.Options, -) !void { - return Render.renderErrors(gpa, writer, &ast, options); +pub fn renderError(tree: Ast, w: *std.Io.Writer, parse_error: Error) !void { + switch (parse_error.tag) { + .expected_token => try w.writeAll("expected token"), + .expected_newline => try w.writeAll("expected newline"), + .expected_double_quote => try w.writeAll("unterminated string, expected closing quote"), + .expected_identifier => try w.writeAll("expected identifier"), + .expected_expression => try w.writeAll("expected expression"), + .unexpected_token => try w.writeAll("unexpected token"), + .invalid_expression => try w.writeAll("invalid expression"), + .invalid_lvalue => try w.writeAll("invalid lvalue for assignment"), + .too_many_arguments => try w.print("too many arguments to '{s}'", .{ + errorSlice(tree, parse_error), + }), + .too_many_parameters => try w.print("too many parameters defined for '{s}'", .{ + errorSlice(tree, parse_error), + }), + } } -pub fn deinit(ast: *Ast, gpa: std.mem.Allocator) void { - gpa.free(ast.errors); - ast.* = undefined; +pub fn errorSlice(tree: Ast, parse_error: Ast.Error) []const u8 { + return tree.source[parse_error.loc.start..parse_error.loc.end]; +} + +pub fn nodeSlice(tree: Ast, node: *const Ast.Node) []const u8 { + assert(node.loc.start <= node.loc.end); + return tree.source[node.loc.start..node.loc.end]; +} + +pub fn deinit(tree: *Ast, gpa: std.mem.Allocator) void { + gpa.free(tree.errors); + tree.* = undefined; } diff --git a/src/Ast/Render.zig b/src/Ast/Render.zig index 6acdaff..a9127f7 100644 --- a/src/Ast/Render.zig +++ b/src/Ast/Render.zig @@ -123,62 +123,6 @@ fn nodeTagToString(tag: Ast.Node.Tag) []const u8 { }; } -fn makeErrorInfo(r: *Render, err: Ast.Error, message: []const u8) ErrorInfo { - const line_index = r.lines.calculateLine(err.loc.start); - const snippet_range = r.lines.ranges.items[line_index]; - const column = err.loc.start - snippet_range.start; - const snippet = r.tree.source[snippet_range.start..snippet_range.end]; - - return ErrorInfo{ - .filename = r.tree.filename, - .message = message, - .line = line_index, - .column = column, - .snippet = snippet, - }; -} - -fn renderErrorf(r: *Render, writer: *std.Io.Writer, err: Ast.Error, message: []const u8) !void { - const error_info = makeErrorInfo(r, err, message); - const filename = error_info.filename; - const line = error_info.line + 1; - const col = error_info.column + 1; - const snippet = error_info.message; - - try writer.print("{s}:{d}:{d}: error: {s}\n", .{ filename, line, col, snippet }); - try writer.print("{d:<4} | {s}\n", .{ line, error_info.snippet }); - try writer.writeAll(" | "); - - var i: usize = 0; - while (i < col) : (i += 1) { - try writer.writeByte(' '); - } - - try writer.writeByte('^'); - try writer.writeAll("\n\n"); -} - -fn renderError(r: *Render, writer: *std.Io.Writer, err: Ast.Error) !void { - switch (err.tag) { - .panic => try renderErrorf(r, writer, err, "parser panicked"), - .unknown_identifier => try renderErrorf(r, writer, err, "unknown identifier"), - .redefined_identifier => try renderErrorf(r, writer, err, "redefined identifier"), - .assignment_to_const => try renderErrorf(r, writer, err, "assignment to constant value"), - .unexpected_token => try renderErrorf(r, writer, err, "unexpected token"), - .expected_newline => try renderErrorf(r, writer, err, "expected newline"), - .expected_double_quote => try renderErrorf(r, writer, err, "unterminated string, expected closing quote"), - .expected_identifier => try renderErrorf(r, writer, err, "expected identifier"), - .expected_expression => try renderErrorf(r, writer, err, "expected expression"), - .invalid_expression => try renderErrorf(r, writer, err, "invalid expression"), - .invalid_lvalue => try renderErrorf(r, writer, err, "invalid lvalue for assignment"), - .too_many_arguments => try renderErrorf(r, writer, err, "too many arguments to '{s}'"), - .too_many_parameters => try renderErrorf(r, writer, err, "too many parameters defined for '{s}'"), - .unexpected_else_stmt => try renderErrorf(r, writer, err, "unexpected else stmt"), - .invalid_else_stmt => try renderErrorf(r, writer, err, "invalid else stmt"), - .invalid_switch_case => try renderErrorf(r, writer, err, "invalid switch case expression"), - } -} - const Prefix = struct { buf: std.ArrayListUnmanaged(u8) = .empty, @@ -211,10 +155,8 @@ fn writeType(r: *Render, writer: *std.Io.Writer, node: *const Ast.Node) !void { } fn writeLexeme(r: *Render, writer: *std.Io.Writer, node: *const Ast.Node) !void { - const bytes = r.tree.source; - const lexeme = bytes[node.loc.start..node.loc.end]; try r.tty_config.setColor(writer, .yellow); - try writer.print("`{s}`", .{lexeme}); + try writer.print("`{s}`", .{r.tree.nodeSlice(node)}); try r.tty_config.setColor(writer, .reset); } @@ -549,23 +491,3 @@ pub fn renderTree( } try writer.flush(); } - -pub fn renderErrors( - gpa: std.mem.Allocator, - writer: *std.Io.Writer, - ast: *const Ast, - options: Options, -) !void { - var r: Render = .{ - .gpa = gpa, - .tree = ast, - .tty_config = if (options.use_color) .escape_codes else .no_color, - .prefix = .{}, - .lines = try LineCache.build(gpa, ast.source), - }; - defer r.prefix.deinit(gpa); - defer r.lines.deinit(gpa); - - for (ast.errors) |err| try r.renderError(writer, err); - try writer.flush(); -} diff --git a/src/AstGen.zig b/src/AstGen.zig index af51193..e31fa7d 100644 --- a/src/AstGen.zig +++ b/src/AstGen.zig @@ -16,7 +16,7 @@ globals: std.ArrayListUnmanaged(Ir.Global) = .empty, global_ref_table: std.AutoHashMapUnmanaged(Ir.NullTerminatedString, usize) = .empty, extra: std.ArrayListUnmanaged(u32) = .empty, scratch: std.ArrayListUnmanaged(u32) = .empty, -errors: std.ArrayListUnmanaged(Ast.Error) = .empty, +compile_errors: std.ArrayListUnmanaged(Ir.Inst.CompileErrors.Item) = .empty, pub const InnerError = error{ OutOfMemory, @@ -25,7 +25,239 @@ pub const InnerError = error{ Overflow, }; -pub fn deinit(astgen: *AstGen) void { +/// Splat an IR data struct into the `extra` array. +fn addExtra(astgen: *AstGen, extra: anytype) !u32 { + const fields = std.meta.fields(@TypeOf(extra)); + try astgen.extra.ensureUnusedCapacity(astgen.gpa, fields.len); + return addExtraAssumeCapacity(astgen, extra); +} + +/// Splat an IR data struct into the `extra` array. +fn addExtraAssumeCapacity(astgen: *AstGen, extra: anytype) u32 { + const fields = std.meta.fields(@TypeOf(extra)); + const extra_index: u32 = @intCast(astgen.extra.items.len); + astgen.extra.items.len += fields.len; + setExtra(astgen, extra_index, extra); + return extra_index; +} + +fn setExtra(astgen: *AstGen, index: usize, extra: anytype) void { + const fields = std.meta.fields(@TypeOf(extra)); + var i = index; + inline for (fields) |field| { + astgen.extra.items[i] = switch (field.type) { + u32 => @field(extra, field.name), + Ir.Inst.Index => @intFromEnum(@field(extra, field.name)), + Ir.Inst.Ref => @intFromEnum(@field(extra, field.name)), + Ir.NullTerminatedString => @intFromEnum(@field(extra, field.name)), + else => @compileError("bad field type"), + }; + i += 1; + } +} + +fn appendBlockBody(astgen: *AstGen, body: []const Ir.Inst.Index) void { + return appendBlockBodyArrayList(astgen, &astgen.extra, body); +} + +fn appendBlockBodyArrayList( + _: *AstGen, + list: *std.ArrayListUnmanaged(u32), + body: []const Ir.Inst.Index, +) void { + for (body) |inst_index| { + list.appendAssumeCapacity(@intFromEnum(inst_index)); + } +} +fn appendErrorNode( + astgen: *AstGen, + node: *const Ast.Node, + comptime format: []const u8, + args: anytype, +) error{OutOfMemory}!void { + return appendErrorToken(astgen, @intCast(node.loc.start), format, args); +} + +fn appendErrorToken( + astgen: *AstGen, + byte_offset: u32, + comptime format: []const u8, + args: anytype, +) error{OutOfMemory}!void { + const gpa = astgen.gpa; + const string_bytes = &astgen.string_bytes; + const msg: Ir.NullTerminatedString = @enumFromInt(string_bytes.items.len); + try string_bytes.print(gpa, format ++ "\x00", args); + + try astgen.compile_errors.append(gpa, .{ + .msg = msg, + .byte_offset = byte_offset, + }); +} + +fn fail( + astgen: *AstGen, + node: *const Ast.Node, + comptime format: []const u8, + args: anytype, +) error{ SemanticError, OutOfMemory } { + try appendErrorNode(astgen, node, format, args); + return error.SemanticError; +} + +fn lowerAstErrors(astgen: *AstGen) error{OutOfMemory}!void { + const gpa = astgen.gpa; + var msg: std.Io.Writer.Allocating = .init(gpa); + defer msg.deinit(); + const w = &msg.writer; + + for (astgen.tree.errors) |err| { + astgen.tree.renderError(w, err) catch return error.OutOfMemory; + try appendErrorToken( + astgen, + @intCast(err.loc.start), + "{s}", + .{msg.written()}, + ); + msg.clearRetainingCapacity(); + } +} + +fn nullTerminatedString(astgen: *AstGen, str: Ir.NullTerminatedString) [:0]const u8 { + const slice = astgen.string_bytes.items[@intFromEnum(str)..]; + return slice[0..std.mem.indexOfScalar(u8, slice, 0).? :0]; +} + +fn stringFromBytes(astgen: *AstGen, bytes: []const u8) error{OutOfMemory}!Ir.NullTerminatedString { + const gpa = astgen.gpa; + const string_bytes = &astgen.string_bytes; + const str_index: u32 = @intCast(string_bytes.items.len); + try string_bytes.appendSlice(gpa, bytes); + + const key: []const u8 = string_bytes.items[str_index..]; + const gop = try astgen.string_table.getOrPutContextAdapted(gpa, key, StringIndexAdapter{ + .bytes = string_bytes, + }, StringIndexContext{ + .bytes = string_bytes, + }); + if (gop.found_existing) { + string_bytes.shrinkRetainingCapacity(str_index); + return @enumFromInt(gop.key_ptr.*); + } else { + gop.key_ptr.* = str_index; + try string_bytes.append(gpa, 0); + return @enumFromInt(str_index); + } +} + +fn stringFromNode(astgen: *AstGen, node: *const Ast.Node) !Ir.NullTerminatedString { + const name_bytes = astgen.tree.nodeSlice(node); + return astgen.stringFromBytes(name_bytes); +} + +fn qualifiedString( + astgen: *AstGen, + prefix: Ir.NullTerminatedString, + relative: Ir.NullTerminatedString, +) !Ir.NullTerminatedString { + const gpa = astgen.gpa; + const string_bytes = &astgen.string_bytes; + const string_table = &astgen.string_table; + const str_index: u32 = @intCast(string_bytes.items.len); + + switch (prefix) { + .empty => return relative, + else => |prev| { + try string_bytes.appendSlice(gpa, nullTerminatedString(astgen, prev)); + try string_bytes.append(gpa, '.'); + try string_bytes.appendSlice(gpa, nullTerminatedString(astgen, relative)); + + const key: []const u8 = string_bytes.items[str_index..]; + const gop = try string_table.getOrPutContextAdapted(gpa, key, StringIndexAdapter{ + .bytes = string_bytes, + }, StringIndexContext{ + .bytes = string_bytes, + }); + if (gop.found_existing) { + string_bytes.shrinkRetainingCapacity(str_index); + return @enumFromInt(gop.key_ptr.*); + } else { + gop.key_ptr.* = str_index; + try string_bytes.append(gpa, 0); + return @enumFromInt(str_index); + } + }, + } +} + +/// Perform IR code generation via tree-walk. +pub fn generate(gpa: std.mem.Allocator, tree: *const Ast) !Ir { + var astgen: AstGen = .{ + .gpa = gpa, + .tree = tree, + }; + defer astgen.deinit(); + + // First entry is reserved for Ir.NullTerminatedString.empty. + try astgen.string_bytes.append(gpa, 0); + + var instructions: std.ArrayListUnmanaged(Ir.Inst.Index) = .empty; + defer instructions.deinit(gpa); + var file_scope: Scope = .{ + .parent = null, + .decls = .empty, + .namespace_prefix = .empty, + .astgen = &astgen, + }; + var block: GenIr = .{ + .astgen = &astgen, + .instructions = &instructions, + .instructions_top = 0, + }; + defer block.unstack(); + + const reserved_extra_count = @typeInfo(Ir.ExtraIndex).@"enum".fields.len; + try astgen.extra.ensureTotalCapacity(gpa, reserved_extra_count); + astgen.extra.items.len += reserved_extra_count; + + const fatal = if (tree.errors.len == 0) fatal: { + // TODO: Make sure this is never null. + const root_node = tree.root.?; + file(&block, &file_scope, root_node) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + error.SemanticError => break :fatal true, + else => |e| return e, + }; + break :fatal false; + } else fatal: { + try lowerAstErrors(&astgen); + break :fatal true; + }; + + const err_index = @intFromEnum(Ir.ExtraIndex.compile_errors); + if (astgen.compile_errors.items.len == 0) { + astgen.extra.items[err_index] = 0; + } else { + const extra_len = 1 + astgen.compile_errors.items.len * + @typeInfo(Ir.Inst.CompileErrors.Item).@"struct".fields.len; + try astgen.extra.ensureUnusedCapacity(gpa, extra_len); + + astgen.extra.items[err_index] = astgen.addExtraAssumeCapacity(Ir.Inst.CompileErrors{ + .items_len = @intCast(astgen.compile_errors.items.len), + }); + for (astgen.compile_errors.items) |item| { + _ = astgen.addExtraAssumeCapacity(item); + } + } + return .{ + .instructions = if (fatal) &.{} else try astgen.instructions.toOwnedSlice(gpa), + .string_bytes = try astgen.string_bytes.toOwnedSlice(gpa), + .globals = try astgen.globals.toOwnedSlice(gpa), + .extra = try astgen.extra.toOwnedSlice(gpa), + }; +} + +fn deinit(astgen: *AstGen) void { const gpa = astgen.gpa; astgen.string_table.deinit(gpa); astgen.string_bytes.deinit(gpa); @@ -34,54 +266,9 @@ pub fn deinit(astgen: *AstGen) void { astgen.instructions.deinit(gpa); astgen.extra.deinit(gpa); astgen.scratch.deinit(gpa); - astgen.errors.deinit(gpa); + astgen.compile_errors.deinit(gpa); } -const Scope = struct { - parent: ?*Scope, - astgen: *AstGen, - namespace_prefix: Ir.NullTerminatedString, - decls: std.AutoHashMapUnmanaged(Ir.NullTerminatedString, Decl), - - const Decl = struct { - decl_node: *const Ast.Node, - inst_index: Ir.Inst.Index, - }; - - fn deinit(self: *Scope) void { - const gpa = self.astgen.gpa; - self.decls.deinit(gpa); - } - - fn makeChild(parent_scope: *Scope) Scope { - return .{ - .parent = parent_scope, - .astgen = parent_scope.astgen, - .namespace_prefix = parent_scope.namespace_prefix, - .decls = .empty, - }; - } - - fn setNamespacePrefix(scope: *Scope, relative: Ir.NullTerminatedString) !void { - const astgen = scope.astgen; - scope.namespace_prefix = try astgen.qualifiedString(scope.namespace_prefix, relative); - } - - fn insert(self: *Scope, ref: Ir.NullTerminatedString, decl: Decl) !void { - const gpa = self.astgen.gpa; - return self.decls.put(gpa, ref, decl); - } - - fn lookup(self: *Scope, ref: Ir.NullTerminatedString) ?Decl { - var current_scope: ?*Scope = self; - while (current_scope) |scope| : (current_scope = scope.parent) { - const result = scope.decls.get(ref); - if (result) |symbol| return symbol; - } - return null; - } -}; - const GenIr = struct { astgen: *AstGen, instructions: *std.ArrayListUnmanaged(Ir.Inst.Index), @@ -121,14 +308,6 @@ const GenIr = struct { self.instructions.items[self.instructions_top..]; } - fn fail( - self: *GenIr, - tag: Ast.Error.Tag, - node: *const Ast.Node, - ) error{ SemanticError, OutOfMemory } { - return self.astgen.fail(tag, node); - } - fn makeSubBlock(self: *GenIr) GenIr { return .{ .astgen = self.astgen, @@ -326,50 +505,50 @@ const GenIr = struct { } }; -/// Splat an IR data struct into the `extra` array. -fn addExtra(astgen: *AstGen, extra: anytype) !u32 { - const fields = std.meta.fields(@TypeOf(extra)); - try astgen.extra.ensureUnusedCapacity(astgen.gpa, fields.len); - return addExtraAssumeCapacity(astgen, extra); -} +const Scope = struct { + parent: ?*Scope, + astgen: *AstGen, + namespace_prefix: Ir.NullTerminatedString, + decls: std.AutoHashMapUnmanaged(Ir.NullTerminatedString, Decl), -/// Splat an IR data struct into the `extra` array. -fn addExtraAssumeCapacity(astgen: *AstGen, extra: anytype) u32 { - const fields = std.meta.fields(@TypeOf(extra)); - const extra_index: u32 = @intCast(astgen.extra.items.len); - astgen.extra.items.len += fields.len; - setExtra(astgen, extra_index, extra); - return extra_index; -} + const Decl = struct { + decl_node: *const Ast.Node, + inst_index: Ir.Inst.Index, + }; -fn setExtra(astgen: *AstGen, index: usize, extra: anytype) void { - const fields = std.meta.fields(@TypeOf(extra)); - var i = index; - inline for (fields) |field| { - astgen.extra.items[i] = switch (field.type) { - u32 => @field(extra, field.name), - Ir.Inst.Index => @intFromEnum(@field(extra, field.name)), - Ir.Inst.Ref => @intFromEnum(@field(extra, field.name)), - Ir.NullTerminatedString => @intFromEnum(@field(extra, field.name)), - else => @compileError("bad field type"), + fn deinit(self: *Scope) void { + const gpa = self.astgen.gpa; + self.decls.deinit(gpa); + } + + fn makeChild(parent_scope: *Scope) Scope { + return .{ + .parent = parent_scope, + .astgen = parent_scope.astgen, + .namespace_prefix = parent_scope.namespace_prefix, + .decls = .empty, }; - i += 1; } -} -fn appendBlockBody(astgen: *AstGen, body: []const Ir.Inst.Index) void { - return appendBlockBodyArrayList(astgen, &astgen.extra, body); -} - -fn appendBlockBodyArrayList( - _: *AstGen, - list: *std.ArrayListUnmanaged(u32), - body: []const Ir.Inst.Index, -) void { - for (body) |inst_index| { - list.appendAssumeCapacity(@intFromEnum(inst_index)); + fn setNamespacePrefix(scope: *Scope, relative: Ir.NullTerminatedString) !void { + const astgen = scope.astgen; + scope.namespace_prefix = try astgen.qualifiedString(scope.namespace_prefix, relative); } -} + + fn insert(self: *Scope, ref: Ir.NullTerminatedString, decl: Decl) !void { + const gpa = self.astgen.gpa; + return self.decls.put(gpa, ref, decl); + } + + fn lookup(self: *Scope, ref: Ir.NullTerminatedString) ?Decl { + var current_scope: ?*Scope = self; + while (current_scope) |scope| : (current_scope = scope.parent) { + const result = scope.decls.get(ref); + if (result) |symbol| return symbol; + } + return null; + } +}; fn setDeclaration( decl_index: Ir.Inst.Index, @@ -392,7 +571,7 @@ fn setDeclaration( try astgen.global_ref_table.ensureUnusedCapacity(gpa, 1); if (astgen.global_ref_table.get(args.name)) |_| { - return astgen.fail(.redefined_identifier, args.decl_node); + return fail(astgen, args.decl_node, "redefined identifier", .{}); } const inst_data = &astgen.instructions.items[@intFromEnum(decl_index)].data; @@ -439,98 +618,6 @@ fn setCondBrPayload( astgen.appendBlockBody(else_body); } -fn fail( - self: *AstGen, - tag: Ast.Error.Tag, - source_node: *const Ast.Node, -) error{ SemanticError, OutOfMemory } { - const gpa = self.gpa; - const err: Ast.Error = .{ - .tag = tag, - .loc = .{ - .start = source_node.loc.start, - .end = source_node.loc.end, - }, - }; - - try self.errors.append(gpa, err); - return error.SemanticError; -} - -fn sliceFromNode(astgen: *const AstGen, node: *const Ast.Node) []const u8 { - assert(node.loc.start <= node.loc.end); - const source_bytes = astgen.tree.source; - return source_bytes[node.loc.start..node.loc.end]; -} - -fn stringFromBytes(astgen: *AstGen, bytes: []const u8) error{OutOfMemory}!Ir.NullTerminatedString { - const gpa = astgen.gpa; - const string_bytes = &astgen.string_bytes; - const str_index: u32 = @intCast(string_bytes.items.len); - try string_bytes.appendSlice(gpa, bytes); - - const key: []const u8 = string_bytes.items[str_index..]; - const gop = try astgen.string_table.getOrPutContextAdapted(gpa, key, StringIndexAdapter{ - .bytes = string_bytes, - }, StringIndexContext{ - .bytes = string_bytes, - }); - if (gop.found_existing) { - string_bytes.shrinkRetainingCapacity(str_index); - return @enumFromInt(gop.key_ptr.*); - } else { - gop.key_ptr.* = str_index; - try string_bytes.append(gpa, 0); - return @enumFromInt(str_index); - } -} - -fn stringFromNode(astgen: *AstGen, node: *const Ast.Node) !Ir.NullTerminatedString { - const name_bytes = sliceFromNode(astgen, node); - assert(name_bytes.len > 0); - return astgen.stringFromBytes(name_bytes); -} - -fn nullTerminatedString(astgen: *AstGen, str: Ir.NullTerminatedString) [:0]const u8 { - const slice = astgen.string_bytes.items[@intFromEnum(str)..]; - return slice[0..std.mem.indexOfScalar(u8, slice, 0).? :0]; -} - -fn qualifiedString( - astgen: *AstGen, - prefix: Ir.NullTerminatedString, - relative: Ir.NullTerminatedString, -) !Ir.NullTerminatedString { - const gpa = astgen.gpa; - const string_bytes = &astgen.string_bytes; - const string_table = &astgen.string_table; - const str_index: u32 = @intCast(string_bytes.items.len); - - switch (prefix) { - .empty => return relative, - else => |prev| { - try string_bytes.appendSlice(gpa, nullTerminatedString(astgen, prev)); - try string_bytes.append(gpa, '.'); - try string_bytes.appendSlice(gpa, nullTerminatedString(astgen, relative)); - - const key: []const u8 = string_bytes.items[str_index..]; - const gop = try string_table.getOrPutContextAdapted(gpa, key, StringIndexAdapter{ - .bytes = string_bytes, - }, StringIndexContext{ - .bytes = string_bytes, - }); - if (gop.found_existing) { - string_bytes.shrinkRetainingCapacity(str_index); - return @enumFromInt(gop.key_ptr.*); - } else { - gop.key_ptr.* = str_index; - try string_bytes.append(gpa, 0); - return @enumFromInt(str_index); - } - }, - } -} - fn unaryOp( gi: *GenIr, scope: *Scope, @@ -581,10 +668,10 @@ fn logicalOp( gen.setLabel(else_label); } -fn numberLiteral(gen: *GenIr, node: *const Ast.Node) InnerError!Ir.Inst.Ref { - const lexeme = sliceFromNode(gen.astgen, node); +fn numberLiteral(block: *GenIr, node: *const Ast.Node) InnerError!Ir.Inst.Ref { + const lexeme = block.astgen.tree.nodeSlice(node); const int_value = try std.fmt.parseUnsigned(u64, lexeme, 10); - return gen.addInt(int_value); + return block.addInt(int_value); } fn stringLiteral(gi: *GenIr, node: *const Ast.Node) InnerError!Ir.Inst.Ref { @@ -694,6 +781,7 @@ fn inlineLogicExpr(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError! } fn validateSwitchProngs(gen: *GenIr, stmt_node: *const Ast.Node) InnerError!void { + const astgen = gen.astgen; var stmt_has_block: bool = false; var stmt_has_else: bool = false; const case_list = stmt_node.data.switch_stmt.cases; @@ -708,10 +796,10 @@ fn validateSwitchProngs(gen: *GenIr, stmt_node: *const Ast.Node) InnerError!void }, .else_branch => { if (case_stmt != last_prong) { - return gen.fail(.invalid_else_stmt, case_stmt); + return fail(astgen, case_stmt, "invalid else stmt", .{}); } if (stmt_has_else) { - return gen.fail(.unexpected_else_stmt, case_stmt); + return fail(astgen, case_stmt, "duplicate else stmt", .{}); } stmt_has_else = true; }, @@ -845,7 +933,7 @@ fn switchStmt( .number_literal => try numberLiteral(parent_block, case_expr), .true_literal => .bool_true, .false_literal => .bool_false, - else => return parent_block.fail(.invalid_switch_case, case_stmt), + else => return fail(astgen, case_expr, "invalid switch case", .{}), }; var case_block = parent_block.makeSubBlock(); defer case_block.unstack(); @@ -934,7 +1022,7 @@ fn assignStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!void _ = try gi.addBinaryNode(.store, decl.inst_index.toRef(), expr_result); return; } - return gi.fail(.unknown_identifier, identifier_node); + return fail(astgen, identifier_node, "unknown identifier", .{}); } fn choiceStarStmt(gi: *GenIr, _: *Scope, node: *const Ast.Node) InnerError!Ir.Inst.Ref { @@ -1174,12 +1262,13 @@ fn divertStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) !void { } fn tempDecl(gi: *GenIr, scope: *Scope, decl_node: *const Ast.Node) !void { + const astgen = gi.astgen; const identifier_node = decl_node.data.bin.lhs.?; const expr_node = decl_node.data.bin.rhs.?; - const name_ref = try gi.astgen.stringFromNode(identifier_node); + const name_ref = try astgen.stringFromNode(identifier_node); if (scope.lookup(name_ref)) |_| { - return gi.fail(.redefined_identifier, decl_node); + return fail(astgen, decl_node, "duplicate identifier", .{}); } const alloc_inst = try gi.add(.{ .tag = .alloc, .data = undefined }); @@ -1371,9 +1460,7 @@ fn file(gi: *GenIr, scope: *Scope, file_node: *const Ast.Node) InnerError!void { const file_inst = try gi.addAsIndex(.{ .tag = .file, .data = .{ - .payload = .{ - .payload_index = undefined, - }, + .payload = .{ .payload_index = undefined }, }, }); @@ -1403,46 +1490,3 @@ fn file(gi: *GenIr, scope: *Scope, file_node: *const Ast.Node) InnerError!void { } return file_scope.setBlockBody(file_inst); } - -/// Perform code generation via tree-walk. -pub fn generate(gpa: std.mem.Allocator, tree: *const Ast) !Ir { - var astgen: AstGen = .{ - .gpa = gpa, - .tree = tree, - }; - defer astgen.deinit(); - - // First entry is reserved for Ir.NullTerminatedString.empty. - try astgen.string_bytes.append(gpa, 0); - - var instructions: std.ArrayListUnmanaged(Ir.Inst.Index) = .empty; - defer instructions.deinit(gpa); - - var file_scope: Scope = .{ - .parent = null, - .decls = .empty, - .namespace_prefix = .empty, - .astgen = &astgen, - }; - var gen: GenIr = .{ - .astgen = &astgen, - .instructions = &instructions, - .instructions_top = 0, - }; - defer gen.unstack(); - - // TODO: Make sure this is never null. - const root_node = tree.root.?; - file(&gen, &file_scope, root_node) catch |err| switch (err) { - error.SemanticError => {}, - else => |e| return e, - }; - - return .{ - .string_bytes = try astgen.string_bytes.toOwnedSlice(gpa), - .instructions = try astgen.instructions.toOwnedSlice(gpa), - .globals = try astgen.globals.toOwnedSlice(gpa), - .extra = try astgen.extra.toOwnedSlice(gpa), - .errors = try astgen.errors.toOwnedSlice(gpa), - }; -} diff --git a/src/Ir.zig b/src/Ir.zig index 52452bf..06cb5cf 100644 --- a/src/Ir.zig +++ b/src/Ir.zig @@ -4,16 +4,122 @@ const Writer = @import("print_ir.zig").Writer; const assert = std.debug.assert; const Ir = @This(); -string_bytes: []u8, +/// List of IR instructions for the translation unit. instructions: []Inst, -globals: []Global, +/// Interned string bytes. All strings are null-terminated. +/// Index 0 is reserved for the empty string. +string_bytes: []u8, +/// Ancillary data for instructions. The meaning of this data is determined by +/// the value of `Inst.Tag`. See `ExtraIndex` for the values of reserved indexes. extra: []u32, -errors: []Ast.Error, +globals: []Global, + +pub const ExtraIndex = enum(u32) { + /// If this is 0, no compile errors. Otherwise there is a `CompileErrors` + /// payload at this index. + compile_errors, + _, +}; + +fn ExtraData(comptime T: type) type { + return struct { + data: T, + end: usize, + }; +} + +/// Extract a slice of `extra` into an `Inst` payload structure. +pub fn extraData(ir: Ir, comptime T: type, index: usize) ExtraData(T) { + const fields = @typeInfo(T).@"struct".fields; + var i: usize = index; + var result: T = undefined; + inline for (fields) |field| { + @field(result, field.name) = switch (field.type) { + u32 => ir.extra[i], + Inst.Index => @enumFromInt(ir.extra[i]), + Inst.Ref => @enumFromInt(ir.extra[i]), + NullTerminatedString => @enumFromInt(ir.extra[i]), + else => @compileError("bad field type"), + }; + i += 1; + } + return .{ .data = result, .end = i }; +} + +pub const NullTerminatedString = enum(u32) { + empty, + _, +}; + +pub fn nullTerminatedString(ir: Ir, index: NullTerminatedString) [:0]const u8 { + const slice = ir.string_bytes[@intFromEnum(index)..]; + return slice[0..std.mem.indexOfScalar(u8, slice, 0).? :0]; +} + +pub fn bodySlice(ir: Ir, start: usize, len: usize) []Inst.Index { + return @ptrCast(ir.extra[start..][0..len]); +} + +pub fn hasCompileErrors(ir: Ir) bool { + if (ir.extra[@intFromEnum(ExtraIndex.compile_errors)] != 0) { + return true; + } else { + assert(ir.instructions.len != 0); + return false; + } +} + +pub fn render(ir: Ir, writer: *std.Io.Writer) !void { + if (ir.instructions.len > 0) { + var w: Writer = .{ .code = ir }; + try w.writeInst(writer, .file_inst); + return writer.flush(); + } +} + +pub fn dumpInfo(ir: Ir, writer: *std.Io.Writer) !void { + const bytes = ir.string_bytes; + + var start: usize = 0; + while (start < bytes.len) { + const end = std.mem.indexOfScalarPos(u8, bytes, start, 0) orelse break; + const str = bytes[start..end]; + + try writer.print("[{d:04}] ", .{start}); + for (str) |b| try writer.print("{x:02} ", .{b}); + try writer.print("00: {s}\n", .{str}); + start = end + 1; + } + for (ir.globals) |global| { + try writer.print("{any}\n", .{global}); + } + return writer.flush(); +} + +pub fn deinit(ir: *Ir, gpa: std.mem.Allocator) void { + gpa.free(ir.string_bytes); + gpa.free(ir.instructions); + gpa.free(ir.globals); + gpa.free(ir.extra); + ir.* = undefined; +} + +pub const Global = struct { + tag: Tag, + name: Ir.NullTerminatedString, + is_constant: bool, + + pub const Tag = enum { + knot, + variable, + }; +}; pub const Inst = struct { tag: Tag, data: Data, + /// An index to an IR instruction. Some values are reserved. pub const Index = enum(u32) { file_inst, ref_start_index = 32, @@ -24,6 +130,15 @@ pub const Inst = struct { } }; + /// A reference to an IR instruction, or to an statically interned value, + /// or neither. + /// + /// If the integer tag value is < `Index.ref_start_index`, then it + /// corresponds to an interned value. Otherwise, this refers to a IR + /// instruction. + /// + /// The tag type is specified so that it is safe to bitcast between `[]u32` + /// and `[]Ref`. pub const Ref = enum(u32) { bool_true, bool_false, @@ -174,93 +289,13 @@ pub const Inst = struct { obj_ptr: Ref, field_name_start: NullTerminatedString, }; -}; -pub const Global = struct { - tag: Tag, - name: Ir.NullTerminatedString, - is_constant: bool, + pub const CompileErrors = struct { + items_len: u32, - pub const Tag = enum { - knot, - variable, - }; -}; - -pub const NullTerminatedString = enum(u32) { - empty, - _, -}; - -pub const IndexSlice = struct { - index: u32, - len: u32, -}; - -pub fn deinit(ir: *Ir, gpa: std.mem.Allocator) void { - gpa.free(ir.string_bytes); - gpa.free(ir.instructions); - gpa.free(ir.globals); - gpa.free(ir.extra); - gpa.free(ir.errors); - ir.* = undefined; -} - -pub fn render(ir: Ir, writer: *std.Io.Writer) !void { - assert(ir.instructions.len > 0); - var w: Writer = .{ .code = ir }; - try w.writeInst(writer, .file_inst); - return writer.flush(); -} - -pub fn dumpInfo(ir: Ir, writer: *std.Io.Writer) !void { - const bytes = ir.string_bytes; - - var start: usize = 0; - while (start < bytes.len) { - const end = std.mem.indexOfScalarPos(u8, bytes, start, 0) orelse break; - const str = bytes[start..end]; - - try writer.print("[{d:04}] ", .{start}); - for (str) |b| try writer.print("{x:02} ", .{b}); - try writer.print("00: {s}\n", .{str}); - start = end + 1; - } - for (ir.globals) |global| { - try writer.print("{any}\n", .{global}); - } - return writer.flush(); -} - -fn ExtraData(comptime T: type) type { - return struct { - data: T, - end: usize, - }; -} - -pub fn extraData(ir: Ir, comptime T: type, index: usize) ExtraData(T) { - const fields = @typeInfo(T).@"struct".fields; - var i: usize = index; - var result: T = undefined; - inline for (fields) |field| { - @field(result, field.name) = switch (field.type) { - u32 => ir.extra[i], - Inst.Index => @enumFromInt(ir.extra[i]), - Inst.Ref => @enumFromInt(ir.extra[i]), - NullTerminatedString => @enumFromInt(ir.extra[i]), - else => @compileError("bad field type"), + pub const Item = struct { + msg: NullTerminatedString, + byte_offset: u32, }; - i += 1; - } - return .{ .data = result, .end = i }; -} - -pub fn nullTerminatedString(ir: Ir, index: NullTerminatedString) [:0]const u8 { - const slice = ir.string_bytes[@intFromEnum(index)..]; - return slice[0..std.mem.indexOfScalar(u8, slice, 0).? :0]; -} - -pub fn bodySlice(ir: Ir, start: usize, len: usize) []Inst.Index { - return @ptrCast(ir.extra[start..][0..len]); -} + }; +}; diff --git a/src/Sema.zig b/src/Sema.zig index d8c9ed4..9dde3f4 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -1,16 +1,16 @@ const std = @import("std"); const Ir = @import("Ir.zig"); const Story = @import("Story.zig"); -const Object = Story.Object; +const Compilation = @import("compile.zig").Compilation; const assert = std.debug.assert; const Sema = @This(); gpa: std.mem.Allocator, ir: *const Ir, -constants: std.ArrayListUnmanaged(CompiledStory.Constant) = .empty, -constant_map: std.AutoHashMapUnmanaged(CompiledStory.Constant, u32) = .empty, +constants: std.ArrayListUnmanaged(Compilation.Constant) = .empty, +constant_map: std.AutoHashMapUnmanaged(Compilation.Constant, u32) = .empty, +knots: std.ArrayListUnmanaged(Compilation.Knot) = .empty, globals: std.ArrayListUnmanaged(u32) = .empty, -knots: std.ArrayListUnmanaged(CompiledStory.Knot) = .empty, const InnerError = error{ OutOfMemory, @@ -28,7 +28,7 @@ const Ref = union(enum) { local: u32, }; -fn deinit(sema: *Sema) void { +pub fn deinit(sema: *Sema) void { const gpa = sema.gpa; sema.constants.deinit(gpa); sema.constant_map.deinit(gpa); @@ -41,7 +41,7 @@ fn fail(_: *Sema, message: []const u8) InnerError { @panic(message); } -fn getConstant(sema: *Sema, data: CompiledStory.Constant) !Ref { +fn getConstant(sema: *Sema, data: Compilation.Constant) !Ref { const gpa = sema.gpa; if (sema.constant_map.get(data)) |index| { @@ -343,7 +343,7 @@ fn irDeclKnot( const data = sema.ir.instructions[@intFromEnum(inst)].data.payload; const extra = sema.ir.extraData(Ir.Inst.Knot, data.payload_index); - var knot: CompiledStory.Knot = .{ + var knot: Compilation.Knot = .{ .name = name_ref, .arity = 0, .stack_size = 0, @@ -505,7 +505,7 @@ fn blockBodyInner(sema: *Sema, chunk: *Chunk, body: []const Ir.Inst.Index) Inner } } -fn file(sema: *Sema, inst: Ir.Inst.Index) !void { +pub fn analyzeFile(sema: *Sema, inst: Ir.Inst.Index) !void { const data = sema.ir.instructions[@intFromEnum(inst)].data.payload; const extra = sema.ir.extraData(Ir.Inst.Block, data.payload_index); const body = sema.ir.bodySlice(extra.end, extra.data.body_len); @@ -520,7 +520,7 @@ fn file(sema: *Sema, inst: Ir.Inst.Index) !void { const Chunk = struct { sema: *Sema, - knot: *CompiledStory.Knot, + knot: *Compilation.Knot, labels: std.ArrayListUnmanaged(Label) = .empty, fixups: std.ArrayListUnmanaged(Fixup) = .empty, inst_map: std.AutoHashMapUnmanaged(Ir.Inst.Index, Ref) = .empty, @@ -666,95 +666,3 @@ const Chunk = struct { } } }; - -pub const CompiledStory = struct { - knots: []Knot, - constants: []Constant, - globals: []u32, - - pub const Knot = struct { - name: Ir.NullTerminatedString, - arity: u32, - stack_size: u32, - constants: std.ArrayListUnmanaged(u32) = .empty, - bytecode: std.ArrayListUnmanaged(u8) = .empty, - }; - - pub const Constant = union(enum) { - integer: u64, - string: Ir.NullTerminatedString, - }; - - pub fn deinit(self: *CompiledStory, gpa: std.mem.Allocator) void { - for (self.knots) |*knot| { - knot.constants.deinit(gpa); - knot.bytecode.deinit(gpa); - } - - gpa.free(self.knots); - gpa.free(self.globals); - gpa.free(self.constants); - self.* = undefined; - } - - pub fn buildRuntime( - self: *CompiledStory, - gpa: std.mem.Allocator, - ir: *Ir, - story: *Story, - ) !void { - const globals_len = self.globals.len + self.knots.len; - const constants_pool = &story.constants_pool; - try constants_pool.ensureUnusedCapacity(gpa, self.constants.len); - try story.globals.ensureUnusedCapacity(gpa, @intCast(globals_len)); - - for (self.constants) |constant| { - switch (constant) { - .integer => |value| { - const object: *Object.Number = try .create(story, .{ - .integer = @intCast(value), - }); - constants_pool.appendAssumeCapacity(&object.base); - }, - .string => |ref| { - const bytes = ir.nullTerminatedString(ref); - const object: *Object.String = try .create(story, bytes); - constants_pool.appendAssumeCapacity(&object.base); - }, - } - } - for (self.globals) |global_index| { - const str = self.constants[global_index]; - const name_bytes = ir.nullTerminatedString(str.string); - story.globals.putAssumeCapacity(name_bytes, null); - } - for (self.knots) |*knot| { - const knot_name = ir.nullTerminatedString(knot.name); - const runtime_chunk: *Object.ContentPath = try .create(story, .{ - .name = try .create(story, knot_name), - .arity = @intCast(knot.arity), - .locals_count = @intCast(knot.stack_size - knot.arity), - .const_pool = try knot.constants.toOwnedSlice(gpa), - .bytes = try knot.bytecode.toOwnedSlice(gpa), - }); - story.globals.putAssumeCapacity(knot_name, &runtime_chunk.base); - } - story.string_bytes = ir.string_bytes; - ir.string_bytes = &.{}; - } -}; - -pub fn compile(gpa: std.mem.Allocator, ir: *const Ir) !CompiledStory { - var sema: Sema = .{ - .gpa = gpa, - .ir = ir, - }; - defer sema.deinit(); - - try file(&sema, .file_inst); - return .{ - .constants = try sema.constants.toOwnedSlice(gpa), - .globals = try sema.globals.toOwnedSlice(gpa), - .knots = try sema.knots.toOwnedSlice(gpa), - }; -} diff --git a/src/Story.zig b/src/Story.zig index ef27685..d2db85c 100644 --- a/src/Story.zig +++ b/src/Story.zig @@ -1,5 +1,6 @@ //! Virtual machine state for story execution. const std = @import("std"); +const Compilation = @import("compile.zig").Compilation; const tokenizer = @import("tokenizer.zig"); const Ast = @import("Ast.zig"); const AstGen = @import("AstGen.zig"); @@ -521,7 +522,7 @@ fn divert(vm: *Story, knot_name: []const u8) !void { pub const LoadOptions = struct { dump_writer: ?*std.Io.Writer = null, - stderr_writer: *std.Io.Writer, + error_writer: *std.Io.Writer, use_color: bool = true, dump_ast: bool = false, dump_ir: bool = false, @@ -538,46 +539,22 @@ pub fn loadFromString( source_bytes: [:0]const u8, options: LoadOptions, ) !Story { - var arena_allocator = std.heap.ArenaAllocator.init(gpa); - defer arena_allocator.deinit(); - const arena = arena_allocator.allocator(); - const ast = try Ast.parse(gpa, arena, source_bytes, "", 0); + var comp = try Compilation.compile(gpa, .{ + .source_bytes = source_bytes, + .filename = "", + .dump_writer = options.dump_writer, + .dump_use_color = options.use_color, + .dump_ast = options.dump_ast, + .dump_ir = options.dump_ir, + }); + defer comp.deinit(); - if (options.dump_ast) { - if (options.dump_writer) |w| { - try w.writeAll("=== AST ===\n"); - try ast.render(gpa, w, .{ - .use_color = options.use_color, - }); + if (comp.errors.len > 0) { + for (comp.errors) |err| { + try comp.renderError(options.error_writer, err); } + return error.Fail; } - if (ast.errors.len > 0) { - try ast.renderErrors(gpa, options.stderr_writer, .{ - .use_color = options.use_color, - }); - return error.Invalid; - } - - var sem_ir = try AstGen.generate(gpa, &ast); - defer sem_ir.deinit(gpa); - - if (sem_ir.errors.len != 0) { - for (sem_ir.errors) |err| { - try options.stderr_writer.print("{any}\n", .{err}); - } - try options.stderr_writer.flush(); - return error.CompilationFailed; - } - if (options.dump_ir) { - if (options.dump_writer) |w| { - try w.writeAll("=== Semantic IR ===\n"); - try sem_ir.dumpInfo(w); - try sem_ir.render(w); - } - } - - var compiled = try Sema.compile(gpa, &sem_ir); - defer compiled.deinit(gpa); var story: Story = .{ .allocator = gpa, @@ -585,9 +562,7 @@ pub fn loadFromString( .dump_writer = options.dump_writer, }; errdefer story.deinit(); - - try compiled.buildRuntime(gpa, &sem_ir, &story); - + try comp.setupStoryRuntime(gpa, &story); if (story.getKnot(Story.default_knot_name)) |knot| { try story.divertToKnot(knot); story.can_advance = true; diff --git a/src/Story/runtime_tests.zig b/src/Story/runtime_tests.zig index 1ed4691..b54ad90 100644 --- a/src/Story/runtime_tests.zig +++ b/src/Story/runtime_tests.zig @@ -12,7 +12,7 @@ fn testRunner(gpa: std.mem.Allocator, source_bytes: [:0]const u8, options: Optio const io_r = options.input_reader; const io_w = options.transcript_writer; var story = try ink.Story.loadFromString(gpa, source_bytes, .{ - .stderr_writer = options.error_writer, + .error_writer = options.error_writer, }); defer story.deinit(); diff --git a/src/compile.zig b/src/compile.zig new file mode 100644 index 0000000..d99b99b --- /dev/null +++ b/src/compile.zig @@ -0,0 +1,229 @@ +const std = @import("std"); +const Ast = @import("Ast.zig"); +const AstGen = @import("AstGen.zig"); +const Sema = @import("Sema.zig"); +const Ir = @import("Ir.zig"); +const Story = @import("Story.zig"); +const Object = Story.Object; +const assert = std.debug.assert; + +pub const Compilation = struct { + gpa: std.mem.Allocator, + arena: std.heap.ArenaAllocator, + tree: Ast, + ir: Ir, + errors: []Error, + knots: []Knot, + constants: []Constant, + globals: []u32, + + pub const Error = struct { + line: usize, + column: usize, + snippet: []const u8, + message: []const u8, + }; + + pub const Knot = struct { + name: Ir.NullTerminatedString, + arity: u32, + stack_size: u32, + constants: std.ArrayListUnmanaged(u32) = .empty, + bytecode: std.ArrayListUnmanaged(u8) = .empty, + }; + + pub const Constant = union(enum) { + integer: u64, + string: Ir.NullTerminatedString, + }; + + pub fn renderError(cu: *const Compilation, w: *std.Io.Writer, compile_error: Error) !void { + const filename = cu.tree.filename; + const line = compile_error.line + 1; + const column = compile_error.column + 1; + + try w.print( + "{s}:{d}:{d}: error: {s}\n", + .{ filename, line, column, compile_error.message }, + ); + try w.print("{d:<4} | {s}\n", .{ line, compile_error.snippet }); + try w.writeAll(" | "); + + if (column > 1) { + try w.splatByteAll(' ', column - 1); + } + try w.writeAll("^\n"); + } + + pub const CompileOptions = struct { + source_bytes: [:0]const u8, + filename: [:0]const u8, + dump_writer: ?*std.Io.Writer = null, + dump_ast: bool = false, + dump_ir: bool = false, + dump_use_color: bool = false, + }; + + pub fn compile(gpa: std.mem.Allocator, options: CompileOptions) !Compilation { + var arena_allocator = std.heap.ArenaAllocator.init(gpa); + errdefer arena_allocator.deinit(); + + var errors: std.ArrayListUnmanaged(Error) = .empty; + defer errors.deinit(gpa); + + const arena = arena_allocator.allocator(); + const ast = try Ast.parse(gpa, arena, options.source_bytes, options.filename, 0); + var ir = try AstGen.generate(gpa, &ast); + errdefer ir.deinit(gpa); + + var sema: Sema = .{ + .gpa = gpa, + .ir = &ir, + }; + defer sema.deinit(); + + if (options.dump_writer) |w| { + if (options.dump_ast) { + try w.writeAll("=== AST ===\n"); + try ast.render(gpa, w, .{ + .use_color = options.dump_use_color, + }); + } + if (options.dump_ir) { + try w.writeAll("=== Semantic IR ===\n"); + try ir.dumpInfo(w); + try ir.render(w); + } + } + + const fatal = if (ir.hasCompileErrors()) fatal: { + const payload_index = ir.extra[@intFromEnum(Ir.ExtraIndex.compile_errors)]; + assert(payload_index != 0); + + const header = ir.extraData(Ir.Inst.CompileErrors, payload_index); + const items_len = header.data.items_len; + var extra_index = header.end; + + // TODO: Make an iterator for this? + for (0..items_len) |_| { + const item = ir.extraData(Ir.Inst.CompileErrors.Item, extra_index); + extra_index = item.end; + + const loc = findLineColumn(ast.source, item.data.byte_offset); + try errors.append(gpa, .{ + .line = loc.line, + .column = loc.column, + .snippet = loc.source_line, + .message = ir.nullTerminatedString(item.data.msg), + }); + } + break :fatal true; + } else fatal: { + try sema.analyzeFile(.file_inst); + break :fatal false; + }; + return .{ + .gpa = gpa, + .arena = arena_allocator, + .tree = ast, + .ir = ir, + .errors = try errors.toOwnedSlice(gpa), + .constants = if (fatal) &.{} else try sema.constants.toOwnedSlice(gpa), + .globals = if (fatal) &.{} else try sema.globals.toOwnedSlice(gpa), + .knots = if (fatal) &.{} else try sema.knots.toOwnedSlice(gpa), + }; + } + + pub fn setupStoryRuntime(cu: *Compilation, gpa: std.mem.Allocator, story: *Story) !void { + assert(cu.errors.len == 0); + + const globals_len = cu.globals.len + cu.knots.len; + const constants_pool = &story.constants_pool; + try constants_pool.ensureUnusedCapacity(gpa, cu.constants.len); + try story.globals.ensureUnusedCapacity(gpa, @intCast(globals_len)); + + for (cu.constants) |constant| { + switch (constant) { + .integer => |value| { + const object: *Object.Number = try .create(story, .{ + .integer = @intCast(value), + }); + constants_pool.appendAssumeCapacity(&object.base); + }, + .string => |ref| { + const bytes = cu.ir.nullTerminatedString(ref); + const object: *Object.String = try .create(story, bytes); + constants_pool.appendAssumeCapacity(&object.base); + }, + } + } + for (cu.globals) |global_index| { + const str = cu.constants[global_index]; + const name_bytes = cu.ir.nullTerminatedString(str.string); + story.globals.putAssumeCapacity(name_bytes, null); + } + for (cu.knots) |*knot| { + const knot_name = cu.ir.nullTerminatedString(knot.name); + const runtime_chunk: *Object.ContentPath = try .create(story, .{ + .name = try .create(story, knot_name), + .arity = @intCast(knot.arity), + .locals_count = @intCast(knot.stack_size - knot.arity), + .const_pool = try knot.constants.toOwnedSlice(gpa), + .bytes = try knot.bytecode.toOwnedSlice(gpa), + }); + story.globals.putAssumeCapacity(knot_name, &runtime_chunk.base); + } + story.string_bytes = cu.ir.string_bytes; + cu.ir.string_bytes = &.{}; + } + + pub fn deinit(cu: *Compilation) void { + const gpa = cu.gpa; + for (cu.knots) |*knot| { + knot.constants.deinit(gpa); + knot.bytecode.deinit(gpa); + } + + gpa.free(cu.knots); + gpa.free(cu.errors); + gpa.free(cu.globals); + gpa.free(cu.constants); + + cu.ir.deinit(gpa); + cu.arena.deinit(); + cu.* = undefined; + } +}; + +const Loc = struct { + line: usize, + column: usize, + source_line: []const u8, +}; + +fn findLineColumn(source: []const u8, byte_offset: usize) Loc { + var line: usize = 0; + var column: usize = 0; + var line_start: usize = 0; + var i: usize = 0; + while (i < byte_offset) : (i += 1) { + switch (source[i]) { + '\n' => { + line += 1; + column = 0; + line_start = i + 1; + }, + else => { + column += 1; + }, + } + } + while (i < source.len and source[i] != '\n') { + i += 1; + } + return .{ + .line = line, + .column = column, + .source_line = source[line_start..i], + }; +} diff --git a/src/error_tests.zig b/src/error_tests.zig new file mode 100644 index 0000000..411fcea --- /dev/null +++ b/src/error_tests.zig @@ -0,0 +1,37 @@ +const std = @import("std"); +const Compilation = @import("compile.zig").Compilation; + +test "compiler: global variable shadowing" { + try testEqual( + \\VAR a = 0 + \\VAR b = 2 + \\VAR a = 1 + , + \\:3:1: error: redefined identifier + \\3 | VAR a = 1 + \\ | ^ + \\ + , + ); +} + +fn testEqual(source_bytes: [:0]const u8, expected_error: []const u8) !void { + const gpa = std.testing.allocator; + var allocating = std.io.Writer.Allocating.init(gpa); + defer allocating.deinit(); + const io_w = &allocating.writer; + + var c = try Compilation.compile(gpa, .{ + .source_bytes = source_bytes, + .filename = "", + .dump_writer = null, + .dump_use_color = false, + .dump_ast = false, + .dump_ir = false, + }); + defer c.deinit(); + + try std.testing.expect(c.errors.len > 0); + for (c.errors) |err| try c.renderError(io_w, err); + return std.testing.expectEqualSlices(u8, expected_error, allocating.written()); +} diff --git a/src/main.zig b/src/main.zig index 53ba5c3..5b4b749 100644 --- a/src/main.zig +++ b/src/main.zig @@ -79,28 +79,33 @@ fn mainArgs( }; }; - const stdin = std.fs.File.stdin(); - var stdin_reader = stdin.reader(&stdin_buffer); - const stdout = std.fs.File.stdout(); var stdout_writer = stdout.writer(&stdout_buffer); const stderr = std.fs.File.stderr(); var stderr_writer = stderr.writer(&stderr_buffer); - var story = try ink.Story.loadFromString(gpa, source_bytes, .{ - .stderr_writer = &stderr_writer.interface, + var story = ink.Story.loadFromString(gpa, source_bytes, .{ + .error_writer = &stderr_writer.interface, .dump_writer = &stdout_writer.interface, .use_color = use_color, .dump_ast = dump_ast, .dump_ir = dump_ir, - }); + }) catch |err| switch (err) { + error.Fail => std.process.exit(1), + else => |e| return e, + }; defer story.deinit(); if (dump_story) { try story.dump(&stderr_writer.interface); } - if (compile_only) return; + return if (!compile_only) run(gpa, &story); +} + +fn run(gpa: std.mem.Allocator, story: *ink.Story) !void { + const stdin = std.fs.File.stdin(); + var stdin_reader = stdin.reader(&stdin_buffer); while (!story.is_exited and story.can_advance) { while (story.can_advance) { diff --git a/src/root.zig b/src/root.zig index 5bd2440..c70b41c 100644 --- a/src/root.zig +++ b/src/root.zig @@ -7,5 +7,6 @@ test { _ = Ast; _ = Story; _ = @import("parser_tests.zig"); + _ = @import("error_tests.zig"); _ = @import("Story/runtime_tests.zig"); }