refactor: new direction for error reporting

This commit is contained in:
Brett Broadhurst 2026-03-18 17:52:07 -06:00
parent 72b686d750
commit c940374f27
Failed to generate hash of commit
11 changed files with 758 additions and 582 deletions

View file

@ -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),
};
}