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

@ -1,14 +1,17 @@
//! Abstract Syntax Tree for Ink source code.
const std = @import("std"); const std = @import("std");
const ink = @import("root.zig"); const ink = @import("root.zig");
const Tokenizer = @import("tokenizer.zig").Tokenizer; const Tokenizer = @import("tokenizer.zig").Tokenizer;
const Render = @import("Ast/Render.zig"); const Render = @import("Ast/Render.zig");
const Parse = @import("Parse.zig"); const Parse = @import("Parse.zig");
const Ast = @This(); const Ast = @This();
const assert = std.debug.assert;
filename: []const u8, filename: []const u8,
source: []const u8, source: []const u8,
// TODO: Make this non-nullable. Empty files are valid.
root: ?*Node = null, root: ?*Node = null,
errors: []Error, errors: []const Error,
pub const Node = struct { pub const Node = struct {
tag: Tag, tag: Tag,
@ -215,24 +218,22 @@ pub const Error = struct {
}; };
pub const Tag = enum { pub const Tag = enum {
panic, expected_token,
unexpected_token,
unknown_identifier,
redefined_identifier,
assignment_to_const,
expected_newline, expected_newline,
expected_expression,
expected_double_quote, expected_double_quote,
expected_identifier, expected_identifier,
expected_expression,
invalid_expression, invalid_expression,
invalid_lvalue, invalid_lvalue,
too_many_arguments, too_many_arguments,
too_many_parameters, too_many_parameters,
unexpected_token,
invalid_else_stmt,
unexpected_else_stmt,
invalid_switch_case,
}; };
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( pub fn parse(
@ -280,16 +281,35 @@ pub fn render(
return Render.renderTree(gpa, writer, &ast, options); return Render.renderTree(gpa, writer, &ast, options);
} }
pub fn renderErrors( pub fn renderError(tree: Ast, w: *std.Io.Writer, parse_error: Error) !void {
ast: Ast, switch (parse_error.tag) {
gpa: std.mem.Allocator, .expected_token => try w.writeAll("expected token"),
writer: *std.Io.Writer, .expected_newline => try w.writeAll("expected newline"),
options: Render.Options, .expected_double_quote => try w.writeAll("unterminated string, expected closing quote"),
) !void { .expected_identifier => try w.writeAll("expected identifier"),
return Render.renderErrors(gpa, writer, &ast, options); .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 { pub fn errorSlice(tree: Ast, parse_error: Ast.Error) []const u8 {
gpa.free(ast.errors); return tree.source[parse_error.loc.start..parse_error.loc.end];
ast.* = undefined; }
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;
} }

View file

@ -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 { const Prefix = struct {
buf: std.ArrayListUnmanaged(u8) = .empty, 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 { 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 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); try r.tty_config.setColor(writer, .reset);
} }
@ -549,23 +491,3 @@ pub fn renderTree(
} }
try writer.flush(); 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();
}

View file

@ -16,7 +16,7 @@ globals: std.ArrayListUnmanaged(Ir.Global) = .empty,
global_ref_table: std.AutoHashMapUnmanaged(Ir.NullTerminatedString, usize) = .empty, global_ref_table: std.AutoHashMapUnmanaged(Ir.NullTerminatedString, usize) = .empty,
extra: std.ArrayListUnmanaged(u32) = .empty, extra: std.ArrayListUnmanaged(u32) = .empty,
scratch: 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{ pub const InnerError = error{
OutOfMemory, OutOfMemory,
@ -25,7 +25,239 @@ pub const InnerError = error{
Overflow, 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; const gpa = astgen.gpa;
astgen.string_table.deinit(gpa); astgen.string_table.deinit(gpa);
astgen.string_bytes.deinit(gpa); astgen.string_bytes.deinit(gpa);
@ -34,54 +266,9 @@ pub fn deinit(astgen: *AstGen) void {
astgen.instructions.deinit(gpa); astgen.instructions.deinit(gpa);
astgen.extra.deinit(gpa); astgen.extra.deinit(gpa);
astgen.scratch.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 { const GenIr = struct {
astgen: *AstGen, astgen: *AstGen,
instructions: *std.ArrayListUnmanaged(Ir.Inst.Index), instructions: *std.ArrayListUnmanaged(Ir.Inst.Index),
@ -121,14 +308,6 @@ const GenIr = struct {
self.instructions.items[self.instructions_top..]; 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 { fn makeSubBlock(self: *GenIr) GenIr {
return .{ return .{
.astgen = self.astgen, .astgen = self.astgen,
@ -326,50 +505,50 @@ const GenIr = struct {
} }
}; };
/// Splat an IR data struct into the `extra` array. const Scope = struct {
fn addExtra(astgen: *AstGen, extra: anytype) !u32 { parent: ?*Scope,
const fields = std.meta.fields(@TypeOf(extra)); astgen: *AstGen,
try astgen.extra.ensureUnusedCapacity(astgen.gpa, fields.len); namespace_prefix: Ir.NullTerminatedString,
return addExtraAssumeCapacity(astgen, extra); decls: std.AutoHashMapUnmanaged(Ir.NullTerminatedString, Decl),
}
/// Splat an IR data struct into the `extra` array. const Decl = struct {
fn addExtraAssumeCapacity(astgen: *AstGen, extra: anytype) u32 { decl_node: *const Ast.Node,
const fields = std.meta.fields(@TypeOf(extra)); inst_index: Ir.Inst.Index,
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 { fn deinit(self: *Scope) void {
return appendBlockBodyArrayList(astgen, &astgen.extra, body); const gpa = self.astgen.gpa;
} self.decls.deinit(gpa);
fn appendBlockBodyArrayList(
_: *AstGen,
list: *std.ArrayListUnmanaged(u32),
body: []const Ir.Inst.Index,
) void {
for (body) |inst_index| {
list.appendAssumeCapacity(@intFromEnum(inst_index));
} }
}
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;
}
};
fn setDeclaration( fn setDeclaration(
decl_index: Ir.Inst.Index, decl_index: Ir.Inst.Index,
@ -392,7 +571,7 @@ fn setDeclaration(
try astgen.global_ref_table.ensureUnusedCapacity(gpa, 1); try astgen.global_ref_table.ensureUnusedCapacity(gpa, 1);
if (astgen.global_ref_table.get(args.name)) |_| { 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; const inst_data = &astgen.instructions.items[@intFromEnum(decl_index)].data;
@ -439,98 +618,6 @@ fn setCondBrPayload(
astgen.appendBlockBody(else_body); 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( fn unaryOp(
gi: *GenIr, gi: *GenIr,
scope: *Scope, scope: *Scope,
@ -581,10 +668,10 @@ fn logicalOp(
gen.setLabel(else_label); gen.setLabel(else_label);
} }
fn numberLiteral(gen: *GenIr, node: *const Ast.Node) InnerError!Ir.Inst.Ref { fn numberLiteral(block: *GenIr, node: *const Ast.Node) InnerError!Ir.Inst.Ref {
const lexeme = sliceFromNode(gen.astgen, node); const lexeme = block.astgen.tree.nodeSlice(node);
const int_value = try std.fmt.parseUnsigned(u64, lexeme, 10); 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 { 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 { fn validateSwitchProngs(gen: *GenIr, stmt_node: *const Ast.Node) InnerError!void {
const astgen = gen.astgen;
var stmt_has_block: bool = false; var stmt_has_block: bool = false;
var stmt_has_else: bool = false; var stmt_has_else: bool = false;
const case_list = stmt_node.data.switch_stmt.cases; 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 => { .else_branch => {
if (case_stmt != last_prong) { 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) { if (stmt_has_else) {
return gen.fail(.unexpected_else_stmt, case_stmt); return fail(astgen, case_stmt, "duplicate else stmt", .{});
} }
stmt_has_else = true; stmt_has_else = true;
}, },
@ -845,7 +933,7 @@ fn switchStmt(
.number_literal => try numberLiteral(parent_block, case_expr), .number_literal => try numberLiteral(parent_block, case_expr),
.true_literal => .bool_true, .true_literal => .bool_true,
.false_literal => .bool_false, .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(); var case_block = parent_block.makeSubBlock();
defer case_block.unstack(); 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); _ = try gi.addBinaryNode(.store, decl.inst_index.toRef(), expr_result);
return; 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 { 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 { 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 identifier_node = decl_node.data.bin.lhs.?;
const expr_node = decl_node.data.bin.rhs.?; 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)) |_| { 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 }); 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(.{ const file_inst = try gi.addAsIndex(.{
.tag = .file, .tag = .file,
.data = .{ .data = .{
.payload = .{ .payload = .{ .payload_index = undefined },
.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); 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),
};
}

View file

@ -4,16 +4,122 @@ const Writer = @import("print_ir.zig").Writer;
const assert = std.debug.assert; const assert = std.debug.assert;
const Ir = @This(); const Ir = @This();
string_bytes: []u8, /// List of IR instructions for the translation unit.
instructions: []Inst, 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, 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 { pub const Inst = struct {
tag: Tag, tag: Tag,
data: Data, data: Data,
/// An index to an IR instruction. Some values are reserved.
pub const Index = enum(u32) { pub const Index = enum(u32) {
file_inst, file_inst,
ref_start_index = 32, 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) { pub const Ref = enum(u32) {
bool_true, bool_true,
bool_false, bool_false,
@ -174,93 +289,13 @@ pub const Inst = struct {
obj_ptr: Ref, obj_ptr: Ref,
field_name_start: NullTerminatedString, field_name_start: NullTerminatedString,
}; };
};
pub const Global = struct { pub const CompileErrors = struct {
tag: Tag, items_len: u32,
name: Ir.NullTerminatedString,
is_constant: bool,
pub const Tag = enum { pub const Item = struct {
knot, msg: NullTerminatedString,
variable, byte_offset: u32,
};
}; };
}; };
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"),
};
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]);
}

View file

@ -1,16 +1,16 @@
const std = @import("std"); const std = @import("std");
const Ir = @import("Ir.zig"); const Ir = @import("Ir.zig");
const Story = @import("Story.zig"); const Story = @import("Story.zig");
const Object = Story.Object; const Compilation = @import("compile.zig").Compilation;
const assert = std.debug.assert; const assert = std.debug.assert;
const Sema = @This(); const Sema = @This();
gpa: std.mem.Allocator, gpa: std.mem.Allocator,
ir: *const Ir, ir: *const Ir,
constants: std.ArrayListUnmanaged(CompiledStory.Constant) = .empty, constants: std.ArrayListUnmanaged(Compilation.Constant) = .empty,
constant_map: std.AutoHashMapUnmanaged(CompiledStory.Constant, u32) = .empty, constant_map: std.AutoHashMapUnmanaged(Compilation.Constant, u32) = .empty,
knots: std.ArrayListUnmanaged(Compilation.Knot) = .empty,
globals: std.ArrayListUnmanaged(u32) = .empty, globals: std.ArrayListUnmanaged(u32) = .empty,
knots: std.ArrayListUnmanaged(CompiledStory.Knot) = .empty,
const InnerError = error{ const InnerError = error{
OutOfMemory, OutOfMemory,
@ -28,7 +28,7 @@ const Ref = union(enum) {
local: u32, local: u32,
}; };
fn deinit(sema: *Sema) void { pub fn deinit(sema: *Sema) void {
const gpa = sema.gpa; const gpa = sema.gpa;
sema.constants.deinit(gpa); sema.constants.deinit(gpa);
sema.constant_map.deinit(gpa); sema.constant_map.deinit(gpa);
@ -41,7 +41,7 @@ fn fail(_: *Sema, message: []const u8) InnerError {
@panic(message); @panic(message);
} }
fn getConstant(sema: *Sema, data: CompiledStory.Constant) !Ref { fn getConstant(sema: *Sema, data: Compilation.Constant) !Ref {
const gpa = sema.gpa; const gpa = sema.gpa;
if (sema.constant_map.get(data)) |index| { if (sema.constant_map.get(data)) |index| {
@ -343,7 +343,7 @@ fn irDeclKnot(
const data = sema.ir.instructions[@intFromEnum(inst)].data.payload; const data = sema.ir.instructions[@intFromEnum(inst)].data.payload;
const extra = sema.ir.extraData(Ir.Inst.Knot, data.payload_index); const extra = sema.ir.extraData(Ir.Inst.Knot, data.payload_index);
var knot: CompiledStory.Knot = .{ var knot: Compilation.Knot = .{
.name = name_ref, .name = name_ref,
.arity = 0, .arity = 0,
.stack_size = 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 data = sema.ir.instructions[@intFromEnum(inst)].data.payload;
const extra = sema.ir.extraData(Ir.Inst.Block, data.payload_index); const extra = sema.ir.extraData(Ir.Inst.Block, data.payload_index);
const body = sema.ir.bodySlice(extra.end, extra.data.body_len); 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 { const Chunk = struct {
sema: *Sema, sema: *Sema,
knot: *CompiledStory.Knot, knot: *Compilation.Knot,
labels: std.ArrayListUnmanaged(Label) = .empty, labels: std.ArrayListUnmanaged(Label) = .empty,
fixups: std.ArrayListUnmanaged(Fixup) = .empty, fixups: std.ArrayListUnmanaged(Fixup) = .empty,
inst_map: std.AutoHashMapUnmanaged(Ir.Inst.Index, Ref) = .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),
};
}

View file

@ -1,5 +1,6 @@
//! Virtual machine state for story execution. //! Virtual machine state for story execution.
const std = @import("std"); const std = @import("std");
const Compilation = @import("compile.zig").Compilation;
const tokenizer = @import("tokenizer.zig"); const tokenizer = @import("tokenizer.zig");
const Ast = @import("Ast.zig"); const Ast = @import("Ast.zig");
const AstGen = @import("AstGen.zig"); const AstGen = @import("AstGen.zig");
@ -521,7 +522,7 @@ fn divert(vm: *Story, knot_name: []const u8) !void {
pub const LoadOptions = struct { pub const LoadOptions = struct {
dump_writer: ?*std.Io.Writer = null, dump_writer: ?*std.Io.Writer = null,
stderr_writer: *std.Io.Writer, error_writer: *std.Io.Writer,
use_color: bool = true, use_color: bool = true,
dump_ast: bool = false, dump_ast: bool = false,
dump_ir: bool = false, dump_ir: bool = false,
@ -538,46 +539,22 @@ pub fn loadFromString(
source_bytes: [:0]const u8, source_bytes: [:0]const u8,
options: LoadOptions, options: LoadOptions,
) !Story { ) !Story {
var arena_allocator = std.heap.ArenaAllocator.init(gpa); var comp = try Compilation.compile(gpa, .{
defer arena_allocator.deinit(); .source_bytes = source_bytes,
const arena = arena_allocator.allocator(); .filename = "<STDIN>",
const ast = try Ast.parse(gpa, arena, source_bytes, "<STDIN>", 0); .dump_writer = options.dump_writer,
.dump_use_color = options.use_color,
if (options.dump_ast) { .dump_ast = options.dump_ast,
if (options.dump_writer) |w| { .dump_ir = options.dump_ir,
try w.writeAll("=== AST ===\n");
try ast.render(gpa, w, .{
.use_color = options.use_color,
}); });
} defer comp.deinit();
}
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); if (comp.errors.len > 0) {
defer sem_ir.deinit(gpa); for (comp.errors) |err| {
try comp.renderError(options.error_writer, err);
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.Fail;
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 = .{ var story: Story = .{
.allocator = gpa, .allocator = gpa,
@ -585,9 +562,7 @@ pub fn loadFromString(
.dump_writer = options.dump_writer, .dump_writer = options.dump_writer,
}; };
errdefer story.deinit(); errdefer story.deinit();
try comp.setupStoryRuntime(gpa, &story);
try compiled.buildRuntime(gpa, &sem_ir, &story);
if (story.getKnot(Story.default_knot_name)) |knot| { if (story.getKnot(Story.default_knot_name)) |knot| {
try story.divertToKnot(knot); try story.divertToKnot(knot);
story.can_advance = true; story.can_advance = true;

View file

@ -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_r = options.input_reader;
const io_w = options.transcript_writer; const io_w = options.transcript_writer;
var story = try ink.Story.loadFromString(gpa, source_bytes, .{ var story = try ink.Story.loadFromString(gpa, source_bytes, .{
.stderr_writer = options.error_writer, .error_writer = options.error_writer,
}); });
defer story.deinit(); defer story.deinit();

229
src/compile.zig Normal file
View file

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

37
src/error_tests.zig Normal file
View file

@ -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
,
\\<STDIN>: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 = "<STDIN>",
.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());
}

View file

@ -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(); const stdout = std.fs.File.stdout();
var stdout_writer = stdout.writer(&stdout_buffer); var stdout_writer = stdout.writer(&stdout_buffer);
const stderr = std.fs.File.stderr(); const stderr = std.fs.File.stderr();
var stderr_writer = stderr.writer(&stderr_buffer); var stderr_writer = stderr.writer(&stderr_buffer);
var story = try ink.Story.loadFromString(gpa, source_bytes, .{ var story = ink.Story.loadFromString(gpa, source_bytes, .{
.stderr_writer = &stderr_writer.interface, .error_writer = &stderr_writer.interface,
.dump_writer = &stdout_writer.interface, .dump_writer = &stdout_writer.interface,
.use_color = use_color, .use_color = use_color,
.dump_ast = dump_ast, .dump_ast = dump_ast,
.dump_ir = dump_ir, .dump_ir = dump_ir,
}); }) catch |err| switch (err) {
error.Fail => std.process.exit(1),
else => |e| return e,
};
defer story.deinit(); defer story.deinit();
if (dump_story) { if (dump_story) {
try story.dump(&stderr_writer.interface); 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.is_exited and story.can_advance) {
while (story.can_advance) { while (story.can_advance) {

View file

@ -7,5 +7,6 @@ test {
_ = Ast; _ = Ast;
_ = Story; _ = Story;
_ = @import("parser_tests.zig"); _ = @import("parser_tests.zig");
_ = @import("error_tests.zig");
_ = @import("Story/runtime_tests.zig"); _ = @import("Story/runtime_tests.zig");
} }