feat: code generation and execution for simple choice statements

This commit is contained in:
Brett Broadhurst 2026-03-02 21:16:24 -07:00
parent 55346fcd85
commit 889f678dd8
Failed to generate hash of commit
4 changed files with 389 additions and 179 deletions

View file

@ -18,68 +18,16 @@ jump_stack: std.ArrayListUnmanaged(Jump) = .empty,
errors: std.ArrayListUnmanaged(Ast.Error) = .empty,
default_knot_name: [:0]const u8 = "@main@",
pub const CheckError = anyerror;
pub const Symbol = union(enum) {
global_variable: struct {
is_constant: bool,
constant_slot: usize,
node: *const Ast.Node,
},
local_variable: struct {
stack_slot: usize,
node: *const Ast.Node,
},
parameter: struct {
constant_slot: usize,
stack_slot: usize,
},
pub const CheckError = error{
OutOfMemory,
CompilerBug,
SemanticError,
TooManyConstants,
InvalidCharacter,
NotImplemented,
};
pub const Jump = struct {
label_index: usize,
code_offset: usize,
};
pub const Label = struct {
code_offset: usize,
};
pub const Chunk = struct {
name: [:0]const u8,
arity: usize,
locals_count: usize,
const_pool: std.ArrayListUnmanaged(*Story.Object),
bytes: std.ArrayListUnmanaged(u8),
pub fn finalize(chunk: *Chunk, scope: *Scope) !*Story.Object.ContentPath {
const gpa = scope.global.gpa;
const story = scope.global.story;
try scope.backpatch();
const const_pool = try chunk.const_pool.toOwnedSlice(gpa);
const bytes = try chunk.bytes.toOwnedSlice(gpa);
const knot_name = try Story.Object.String.create(story, chunk.name);
const content_path = try Story.Object.ContentPath.create(
story,
knot_name,
chunk.arity,
chunk.locals_count,
const_pool,
bytes,
);
return content_path;
}
pub fn deinit(chunk: *Chunk, gpa: std.mem.Allocator) void {
chunk.const_pool.deinit(gpa);
chunk.bytes.deinit(gpa);
chunk.* = undefined;
}
};
pub const Scope = struct {
const Scope = struct {
parent: ?*Scope,
global: *AstGen,
chunk: *Chunk,
@ -122,7 +70,11 @@ pub const Scope = struct {
return null;
}
pub fn insertIdentifier(scope: *Scope, node: *const Ast.Node, symbol: Symbol) !void {
pub fn insertIdentifier(
scope: *Scope,
node: *const Ast.Node,
symbol: Symbol,
) error{OutOfMemory}!void {
const gpa = scope.global.gpa;
const symbol_table = &scope.symbol_table;
const token_bytes = scope.global.getLexemeFromNode(node);
@ -144,27 +96,7 @@ pub const Scope = struct {
};
}
pub fn makeLabel(scope: *Scope) !usize {
const gpa = scope.global.gpa;
const label_stack = &scope.global.label_stack;
const label_id = label_stack.items.len;
const label_data: Label = .{ .code_offset = 0xffffffff };
try label_stack.append(gpa, label_data);
return label_id;
}
pub fn setLabel(scope: *Scope, label_id: usize) void {
const chunk = scope.chunk;
const code_offset = chunk.bytes.items.len;
const label_stack = &scope.global.label_stack;
assert(label_id <= label_stack.items.len);
const label_data = &label_stack.items[label_id];
label_data.code_offset = code_offset;
}
pub fn makeConstant(scope: *Scope, object: *Story.Object) !usize {
pub fn makeConstant(scope: *Scope, object: *Story.Object) error{OutOfMemory}!usize {
const gpa = scope.global.gpa;
const chunk = scope.chunk;
const const_id = chunk.const_pool.items.len;
@ -178,7 +110,7 @@ pub const Scope = struct {
len: u32,
};
pub fn makeString(scope: *Scope, bytes: []const u8) !IndexSlice {
pub fn makeString(scope: *Scope, bytes: []const u8) error{OutOfMemory}!IndexSlice {
const global = scope.global;
const gpa = scope.global.gpa;
const str_index: u32 = @intCast(global.string_bytes.items.len);
@ -201,7 +133,11 @@ pub const Scope = struct {
}
}
pub fn makeGlobal(scope: *Scope, node: *const Ast.Node, symbol: Symbol) !void {
pub fn makeGlobal(
scope: *Scope,
node: *const Ast.Node,
symbol: Symbol,
) error{OutOfMemory}!void {
const gpa = scope.global.gpa;
const global_data = scope.global;
const token_bytes = global_data.getLexemeFromNode(node);
@ -209,11 +145,11 @@ pub const Scope = struct {
return scope.insertIdentifier(node, symbol);
}
pub fn emitByte(scope: *Scope, byte: u8) !void {
pub fn emitByte(scope: *Scope, byte: u8) error{OutOfMemory}!void {
return scope.chunk.bytes.append(scope.global.gpa, byte);
}
pub fn emitSimpleInst(scope: *Scope, op: Story.Opcode) !void {
pub fn emitSimpleInst(scope: *Scope, op: Story.Opcode) error{OutOfMemory}!void {
return scope.emitByte(@intFromEnum(op));
}
@ -224,7 +160,7 @@ pub const Scope = struct {
try scope.emitByte(@intCast(arg));
}
pub fn emitJumpInst(scope: *Scope, op: Story.Opcode) !void {
pub fn emitJumpInst(scope: *Scope, op: Story.Opcode) error{OutOfMemory}!usize {
const bytes = &scope.chunk.bytes;
try scope.emitSimpleInst(op);
try scope.emitByte(0xff);
@ -232,7 +168,125 @@ pub const Scope = struct {
return bytes.items.len - 2;
}
pub fn backpatch(_: *Scope) !void {}
// Create a new code label.
pub fn makeLabel(scope: *Scope) error{OutOfMemory}!usize {
const gpa = scope.global.gpa;
const dummy_address = 0xffffffff;
const label_stack = &scope.global.label_stack;
const label_index = label_stack.items.len;
const label_data: Label = .{ .code_offset = dummy_address };
try label_stack.append(gpa, label_data);
return label_index;
}
// Sets the code offset pointed to by a label.
pub fn setLabel(scope: *Scope, label_id: usize) void {
const chunk = scope.chunk;
const code_offset = chunk.bytes.items.len;
const label_stack = &scope.global.label_stack;
assert(label_id <= label_stack.items.len);
const label_data = &label_stack.items[label_id];
label_data.code_offset = code_offset;
}
pub fn makeJump(scope: *Scope, jump: Jump) !usize {
const gpa = scope.global.gpa;
const jump_stack = &scope.global.jump_stack;
const jump_index = jump_stack.items.len;
try jump_stack.append(gpa, jump);
return jump_index;
}
pub fn resolveLabels(scope: *Scope, start_index: usize, end_index: usize) !void {
assert(start_index <= end_index);
const jump_stack = &scope.global.jump_stack;
const label_stack = &scope.global.label_stack;
const chunk_bytes = &scope.chunk.bytes;
const jump_list = jump_stack.items[start_index..end_index];
for (jump_list) |jump| {
const label = label_stack.items[jump.label_index];
const jump_offset: usize = switch (jump.mode) {
.relative => label.code_offset - jump.code_offset - 2,
.absolute => label.code_offset,
};
if (jump_offset >= std.math.maxInt(u16)) {
std.debug.print("Too much code to jump over!\n", .{});
return error.CompilerBug;
}
assert(chunk_bytes.capacity >= jump.code_offset + 2);
chunk_bytes.items[jump.code_offset] = @intCast((jump_offset >> 8) & 0xff);
chunk_bytes.items[jump.code_offset + 1] = @intCast(jump_offset & 0xff);
}
}
};
pub const Symbol = union(enum) {
global_variable: struct {
is_constant: bool,
constant_slot: usize,
node: *const Ast.Node,
},
local_variable: struct {
stack_slot: usize,
node: *const Ast.Node,
},
parameter: struct {
constant_slot: usize,
stack_slot: usize,
},
};
pub const Jump = struct {
mode: enum {
relative,
absolute,
},
label_index: usize,
code_offset: usize,
};
pub const Label = struct {
code_offset: usize,
};
// Code chunk to emitting bytes and constants to during code generation.
pub const Chunk = struct {
name: [:0]const u8,
arity: usize,
locals_count: usize,
const_pool: std.ArrayListUnmanaged(*Story.Object),
bytes: std.ArrayListUnmanaged(u8),
pub fn finalize(chunk: *Chunk, scope: *Scope) !*Story.Object.ContentPath {
const gpa = scope.global.gpa;
const story = scope.global.story;
const jump_stack_top = scope.global.jump_stack.items.len;
try scope.resolveLabels(scope.jump_stack_top, jump_stack_top);
const const_pool = try chunk.const_pool.toOwnedSlice(gpa);
const bytes = try chunk.bytes.toOwnedSlice(gpa);
const knot_name = try Story.Object.String.create(story, chunk.name);
const content_path = try Story.Object.ContentPath.create(
story,
knot_name,
chunk.arity,
chunk.locals_count,
const_pool,
bytes,
);
return content_path;
}
pub fn deinit(chunk: *Chunk, gpa: std.mem.Allocator) void {
chunk.const_pool.deinit(gpa);
chunk.bytes.deinit(gpa);
chunk.* = undefined;
}
};
pub fn deinit(astgen: *AstGen) void {
@ -363,11 +417,11 @@ fn checkContentExpr(scope: *Scope, expr: *const Ast.Node) CheckError!void {
switch (child_node.tag) {
.string_literal => {
try checkStringLiteral(scope, child_node);
try scope.emitSimpleInst(.content);
try scope.emitSimpleInst(.stream_push);
},
.inline_logic_expr => {
try checkInlineLogicExpr(scope, child_node);
try scope.emitSimpleInst(.content);
try scope.emitSimpleInst(.stream_push);
},
else => return error.NotImplemented,
}
@ -379,13 +433,6 @@ fn checkContentStmt(scope: *Scope, stmt: *const Ast.Node) CheckError!void {
return checkContentExpr(scope, expr_node);
}
fn checkBlockStmt(parent_scope: *Scope, block_stmt: *const Ast.Node) CheckError!void {
var block_scope = parent_scope.makeSubBlock();
defer block_scope.deinit();
const children = block_stmt.data.list.items orelse return;
for (children) |child_stmt| try checkStmt(&block_scope, child_stmt);
}
fn checkAssignStmt(scope: *Scope, stmt: *const Ast.Node) CheckError!void {
const lhs = stmt.data.bin.lhs orelse return error.CompilerBug;
const rhs = stmt.data.bin.rhs orelse return error.CompilerBug;
@ -413,9 +460,91 @@ fn checkAssignStmt(scope: *Scope, stmt: *const Ast.Node) CheckError!void {
return scope.fail(.unknown_identifier, lhs);
}
fn checkBlockStmt(parent_scope: *Scope, block_stmt: *const Ast.Node) CheckError!void {
var block_scope = parent_scope.makeSubBlock();
defer block_scope.deinit();
const children = block_stmt.data.list.items orelse return;
for (children) |child_stmt| try checkStmt(&block_scope, child_stmt);
}
fn checkChoiceStmt(scope: *Scope, stmt_node: *const Ast.Node) CheckError!void {
const Choice = struct {
label_index: usize,
start_expression: ?*const Ast.Node,
option_expression: ?*const Ast.Node,
inner_expression: ?*const Ast.Node,
block_stmt: ?*const Ast.Node,
};
const branch_list = stmt_node.data.list.items orelse unreachable;
assert(branch_list.len != 0);
const gpa = scope.global.gpa;
var choice_list: std.ArrayListUnmanaged(Choice) = .empty;
defer choice_list.deinit(gpa);
try choice_list.ensureUnusedCapacity(gpa, branch_list.len);
for (branch_list) |branch_stmt| {
assert(branch_stmt.tag == .choice_star_stmt or branch_stmt.tag == .choice_plus_stmt);
const branch_data = branch_stmt.data.bin;
const branch_expr = branch_data.lhs orelse unreachable;
const branch_expr_data = branch_expr.data.choice_expr;
const label_index = try scope.makeLabel();
if (branch_expr_data.start_expr) |node| {
try checkStringLiteral(scope, node);
try scope.emitSimpleInst(.stream_push);
}
if (branch_expr_data.option_expr) |node| {
try checkStringLiteral(scope, node);
try scope.emitSimpleInst(.stream_push);
}
const jump_offset = try scope.emitJumpInst(.br_push);
_ = try scope.makeJump(.{
.mode = .absolute,
.label_index = label_index,
.code_offset = jump_offset,
});
choice_list.appendAssumeCapacity(.{
.label_index = label_index,
.start_expression = branch_expr_data.start_expr,
.inner_expression = branch_expr_data.inner_expr,
.option_expression = branch_expr_data.option_expr,
.block_stmt = branch_data.rhs,
});
}
try scope.emitSimpleInst(.br_table);
try scope.emitSimpleInst(.br_select_index);
try scope.emitSimpleInst(.br_dispatch);
for (choice_list.items) |choice| {
scope.setLabel(choice.label_index);
if (choice.start_expression) |expr_node| {
try checkStringLiteral(scope, expr_node);
try scope.emitSimpleInst(.stream_push);
}
if (choice.inner_expression) |expr_node| {
try checkStringLiteral(scope, expr_node);
try scope.emitSimpleInst(.stream_push);
}
try scope.emitSimpleInst(.stream_flush);
if (choice.block_stmt) |block| {
try checkBlockStmt(scope, block);
} else {
try scope.emitSimpleInst(.exit);
}
}
}
fn checkVarDecl(scope: *Scope, decl_node: *const Ast.Node) !void {
const identifier_node = decl_node.data.bin.lhs orelse return error.Fucked;
const expr_node = decl_node.data.bin.rhs orelse return error.Fucked;
const identifier_node = decl_node.data.bin.lhs orelse return error.CompilerBug;
const expr_node = decl_node.data.bin.rhs orelse return error.CompilerBug;
try checkExpr(scope, expr_node);
switch (decl_node.tag) {
@ -461,6 +590,7 @@ fn checkStmt(scope: *Scope, stmt: *const Ast.Node) CheckError!void {
.temp_decl => try checkVarDecl(scope, stmt),
.assign_stmt => try checkAssignStmt(scope, stmt),
.content_stmt => try checkContentStmt(scope, stmt),
.choice_stmt => try checkChoiceStmt(scope, stmt),
.expr_stmt => try checkExprStmt(scope, stmt),
else => return error.NotImplemented,
}
@ -522,23 +652,18 @@ fn dumpStringsWithHex(astgen: *const AstGen) void {
std.debug.print("[{d:04}] ", .{start});
for (s) |b| std.debug.print("{x:02} ", .{b});
std.debug.print("00 {s}\n", .{s});
std.debug.print("00: {s}\n", .{s});
start = end + 1;
}
}
/// Perform code generation via tree-walk.
pub fn generate(gpa: std.mem.Allocator, tree: *const Ast) !Story {
const root_node = tree.root orelse return error.Fucked;
const root_node = tree.root orelse return error.CompilerBug;
var story: Story = .{
.allocator = gpa,
.is_exited = false,
.can_advance = false,
.gc_objects = .{},
.globals = .empty,
.paths = .empty,
.stack = .empty,
.call_stack = .empty,
};
var astgen: AstGen = .{
.gpa = gpa,