feat: added Ir.Inst.Ref, global constant pool, lazy lowering in Sema

This commit is contained in:
Brett Broadhurst 2026-03-11 20:15:52 -06:00
parent ce5385ebac
commit e5e2b7c559
Failed to generate hash of commit
6 changed files with 615 additions and 534 deletions

View file

@ -105,7 +105,8 @@ const GenIr = struct {
) []Ir.Inst.Index {
return if (self.instructions_top == unstacked_top)
&[0]Ir.Inst.Index{}
else if (self.instructions == stacked_block.instructions and stacked_block.instructions_top != unstacked_top)
else if (self.instructions == stacked_block.instructions and
stacked_block.instructions_top != unstacked_top)
self.instructions.items[self.instructions_top..stacked_block.instructions_top]
else
self.instructions.items[self.instructions_top..];
@ -127,24 +128,28 @@ const GenIr = struct {
};
}
fn add(gi: *GenIr, inst: Ir.Inst) !Ir.Inst.Index {
fn add(gi: *GenIr, inst: Ir.Inst) !Ir.Inst.Ref {
return (try gi.addAsIndex(inst)).toRef();
}
fn addAsIndex(gi: *GenIr, inst: Ir.Inst) !Ir.Inst.Index {
const gpa = gi.astgen.gpa;
try gi.instructions.ensureUnusedCapacity(gpa, 1);
try gi.astgen.instructions.ensureUnusedCapacity(gpa, 1);
const inst_index: Ir.Inst.Index = @enumFromInt(gi.astgen.instructions.items.len);
const new_index: Ir.Inst.Index = @enumFromInt(gi.astgen.instructions.items.len);
gi.astgen.instructions.appendAssumeCapacity(inst);
gi.instructions.appendAssumeCapacity(inst_index);
return inst_index;
gi.instructions.appendAssumeCapacity(new_index);
return new_index;
}
fn addInt(gi: *GenIr, value: u64) !Ir.Inst.Index {
fn addInt(gi: *GenIr, value: u64) !Ir.Inst.Ref {
return add(gi, .{ .tag = .integer, .data = .{
.integer = .{ .value = value },
} });
}
fn addUnaryNode(gi: *GenIr, tag: Ir.Inst.Tag, arg: Ir.Inst.Index) !Ir.Inst.Index {
fn addUnaryNode(gi: *GenIr, tag: Ir.Inst.Tag, arg: Ir.Inst.Ref) !Ir.Inst.Ref {
return add(gi, .{ .tag = tag, .data = .{
.un = .{ .lhs = arg },
} });
@ -153,15 +158,15 @@ const GenIr = struct {
fn addBinaryNode(
gi: *GenIr,
tag: Ir.Inst.Tag,
lhs: Ir.Inst.Index,
rhs: Ir.Inst.Index,
) !Ir.Inst.Index {
lhs: Ir.Inst.Ref,
rhs: Ir.Inst.Ref,
) !Ir.Inst.Ref {
return add(gi, .{ .tag = tag, .data = .{
.bin = .{ .lhs = lhs, .rhs = rhs },
} });
}
fn addDeclRef(gi: *GenIr, decl_ref: Ir.NullTerminatedString) !Ir.Inst.Index {
fn addDeclRef(gi: *GenIr, decl_ref: Ir.NullTerminatedString) !Ir.Inst.Ref {
return add(gi, .{ .tag = .decl_ref, .data = .{
.string = .{
.start = decl_ref,
@ -185,8 +190,16 @@ const GenIr = struct {
return makePayloadNode(gi, .declaration);
}
fn makeBlockInst(gi: *GenIr) !Ir.Inst.Index {
return makePayloadNode(gi, .block);
fn makeBlockInst(gi: *GenIr, tag: Ir.Inst.Tag) !Ir.Inst.Index {
const inst_index: Ir.Inst.Index = @enumFromInt(gi.astgen.instructions.items.len);
const gpa = gi.astgen.gpa;
try gi.astgen.instructions.append(gpa, .{
.tag = tag,
.data = .{
.payload = .{ .payload_index = undefined },
},
});
return inst_index;
}
fn addKnot(self: *GenIr) !Ir.Inst.Index {
@ -331,7 +344,7 @@ fn setDeclaration(
fn setCondBrPayload(
condbr: Ir.Inst.Index,
cond: Ir.Inst.Index,
cond: Ir.Inst.Ref,
then_block: *GenIr,
else_block: *GenIr,
) !void {
@ -374,6 +387,7 @@ fn setExtra(astgen: *AstGen, index: usize, extra: anytype) void {
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"),
};
@ -444,9 +458,9 @@ fn unaryOp(
scope: *Scope,
expr_node: *const Ast.Node,
op: Ir.Inst.Tag,
) InnerError!Ir.Inst.Index {
assert(expr_node.data.bin.lhs != null);
const lhs = try expr(gi, scope, expr_node.data.bin.lhs orelse unreachable);
) InnerError!Ir.Inst.Ref {
const data = expr_node.data.bin;
const lhs = try expr(gi, scope, data.lhs.?);
return gi.addUnaryNode(op, lhs);
}
@ -455,12 +469,11 @@ fn binaryOp(
scope: *Scope,
expr_node: *const Ast.Node,
op: Ir.Inst.Tag,
) InnerError!Ir.Inst.Index {
) InnerError!Ir.Inst.Ref {
const data = expr_node.data.bin;
assert(data.lhs != null and data.rhs != null);
const lhs = try expr(gi, scope, data.lhs orelse unreachable);
const rhs = try expr(gi, scope, data.rhs orelse unreachable);
const lhs = try expr(gi, scope, data.lhs.?);
const rhs = try expr(gi, scope, data.rhs.?);
return gi.addBinaryNode(op, lhs, rhs);
}
@ -490,23 +503,14 @@ fn logicalOp(
gen.setLabel(else_label);
}
fn trueLiteral(gi: *GenIr) InnerError!Ir.Inst.Index {
return gi.add(.{ .tag = .true_literal, .data = undefined });
}
fn falseLiteral(gi: *GenIr) InnerError!Ir.Inst.Index {
return gi.add(.{ .tag = .false_literal, .data = undefined });
}
fn numberLiteral(gen: *GenIr, node: *const Ast.Node) InnerError!Ir.Inst.Index {
fn numberLiteral(gen: *GenIr, node: *const Ast.Node) InnerError!Ir.Inst.Ref {
const lexeme = sliceFromNode(gen.astgen, node);
const int_value = try std.fmt.parseUnsigned(u64, lexeme, 10);
return gen.addInt(int_value);
}
fn stringLiteral(gi: *GenIr, node: *const Ast.Node) InnerError!Ir.Inst.Index {
const astgen = gi.astgen;
const str = try astgen.stringFromNode(node);
fn stringLiteral(gi: *GenIr, node: *const Ast.Node) InnerError!Ir.Inst.Ref {
const str = try gi.astgen.stringFromNode(node);
return gi.add(.{
.tag = .string,
.data = .{ .string = .{
@ -515,47 +519,46 @@ fn stringLiteral(gi: *GenIr, node: *const Ast.Node) InnerError!Ir.Inst.Index {
});
}
fn stringExpr(gen: *GenIr, expr_node: *const Ast.Node) InnerError!Ir.Inst.Index {
assert(expr_node.data.bin.lhs != null);
const first_node = expr_node.data.bin.lhs orelse unreachable;
fn stringExpr(gen: *GenIr, expr_node: *const Ast.Node) InnerError!Ir.Inst.Ref {
const first_node = expr_node.data.bin.lhs.?;
return stringLiteral(gen, first_node);
}
fn identifier(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Ir.Inst.Index {
fn identifier(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Ir.Inst.Ref {
const astgen = gi.astgen;
const str = try astgen.stringFromNode(node);
if (scope.lookup(str)) |decl| {
return gi.addUnaryNode(.load_local, decl.inst_index);
return gi.addUnaryNode(.load, decl.inst_index.toRef());
}
return gi.addDeclRef(str);
}
fn expr(block: *GenIr, scope: *Scope, optional_expr: ?*const Ast.Node) InnerError!Ir.Inst.Index {
const expr_node = optional_expr orelse unreachable;
fn expr(gi: *GenIr, scope: *Scope, optional_expr: ?*const Ast.Node) InnerError!Ir.Inst.Ref {
const expr_node = optional_expr.?;
switch (expr_node.tag) {
.file => unreachable,
.true_literal => return trueLiteral(block),
.false_literal => return falseLiteral(block),
.number_literal => return numberLiteral(block, expr_node),
.string_literal => return stringLiteral(block, expr_node),
.string_expr => return stringExpr(block, expr_node),
.empty_string => return stringLiteral(block, expr_node),
.identifier => return identifier(block, scope, expr_node),
.add_expr => return binaryOp(block, scope, expr_node, .add),
.subtract_expr => return binaryOp(block, scope, expr_node, .sub),
.multiply_expr => return binaryOp(block, scope, expr_node, .mul),
.divide_expr => return binaryOp(block, scope, expr_node, .div),
.mod_expr => return binaryOp(block, scope, expr_node, .mod),
.negate_expr => return unaryOp(block, scope, expr_node, .neg),
.true_literal => return .bool_true,
.false_literal => return .bool_false,
.number_literal => return numberLiteral(gi, expr_node),
.string_literal => return stringLiteral(gi, expr_node),
.string_expr => return stringExpr(gi, expr_node),
.empty_string => return stringLiteral(gi, expr_node),
.identifier => return identifier(gi, scope, expr_node),
.add_expr => return binaryOp(gi, scope, expr_node, .add),
.subtract_expr => return binaryOp(gi, scope, expr_node, .sub),
.multiply_expr => return binaryOp(gi, scope, expr_node, .mul),
.divide_expr => return binaryOp(gi, scope, expr_node, .div),
.mod_expr => return binaryOp(gi, scope, expr_node, .mod),
.negate_expr => return unaryOp(gi, scope, expr_node, .neg),
.logical_and_expr => unreachable,
.logical_or_expr => unreachable,
.logical_not_expr => return unaryOp(block, scope, expr_node, .not),
.logical_equality_expr => return binaryOp(block, scope, expr_node, .cmp_eq),
.logical_inequality_expr => return binaryOp(block, scope, expr_node, .cmp_neq),
.logical_greater_expr => return binaryOp(block, scope, expr_node, .cmp_gt),
.logical_greater_or_equal_expr => return binaryOp(block, scope, expr_node, .cmp_gte),
.logical_lesser_expr => return binaryOp(block, scope, expr_node, .cmp_lt),
.logical_lesser_or_equal_expr => return binaryOp(block, scope, expr_node, .cmp_lte),
.logical_not_expr => return unaryOp(gi, scope, expr_node, .not),
.logical_equality_expr => return binaryOp(gi, scope, expr_node, .cmp_eq),
.logical_inequality_expr => return binaryOp(gi, scope, expr_node, .cmp_neq),
.logical_greater_expr => return binaryOp(gi, scope, expr_node, .cmp_gt),
.logical_greater_or_equal_expr => return binaryOp(gi, scope, expr_node, .cmp_gte),
.logical_lesser_expr => return binaryOp(gi, scope, expr_node, .cmp_lt),
.logical_lesser_or_equal_expr => return binaryOp(gi, scope, expr_node, .cmp_lte),
.call_expr => unreachable,
.choice_expr => unreachable,
.choice_start_expr => unreachable,
@ -588,22 +591,28 @@ fn expr(block: *GenIr, scope: *Scope, optional_expr: ?*const Ast.Node) InnerErro
.ref_parameter_decl => unreachable,
.argument_list => unreachable,
.parameter_list => unreachable,
.switch_stmt => unreachable,
.switch_case => unreachable,
.if_stmt => unreachable,
.multi_if_stmt => unreachable,
.if_branch => unreachable,
.else_branch => unreachable,
.switch_stmt => unreachable, // Handled in switchStmt
.switch_case => unreachable, // Handled in switchStmt
.if_stmt => unreachable, // Handled in ifStmt
.multi_if_stmt => unreachable, // Handled in multiIfStmt
.if_branch => unreachable, // Handled in ifStmt and multiIfStmt
.else_branch => unreachable, // Handled in switchStmt, multiIfStmt, and ifStmt
.content => unreachable,
.inline_logic_expr => unreachable,
.invalid => unreachable,
}
}
fn exprStmt(gen: *GenIr, scope: *Scope, stmt_node: *const Ast.Node) InnerError!Ir.Inst.Index {
fn exprStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Ir.Inst.Ref {
// TODO: Maybe we should introduce a unary node type to avoid optional checks?
const expr_node = stmt_node.data.bin.lhs.?;
return expr(gen, scope, expr_node);
const expr_node = node.data.bin.lhs.?;
return expr(gi, scope, expr_node);
}
fn inlineLogicExpr(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Ir.Inst.Ref {
// TODO: Maybe we should introduce a unary node type to avoid optional checks?
const main_node = node.data.bin.lhs.?;
return expr(gi, scope, main_node);
}
fn validateSwitchProngs(gen: *GenIr, stmt_node: *const Ast.Node) InnerError!void {
@ -633,96 +642,11 @@ fn validateSwitchProngs(gen: *GenIr, stmt_node: *const Ast.Node) InnerError!void
}
}
fn switchStmt(
gen: *GenIr,
parent_scope: *Scope,
stmt_node: *const Ast.Node,
) InnerError!void {
const gpa = gen.astgen.gpa;
var child_scope = try gen.astgen.createScope(parent_scope);
defer child_scope.deinit();
const label_index = try gen.makeLabel();
gen.setExit(label_index);
const eval_expr = stmt_node.data.switch_stmt.condition_expr;
const case_list = stmt_node.data.switch_stmt.cases;
// NOTE: We're going to create an array of label indexes here, since we
// may create additional labels while traversing nested expressions.
var label_list: std.ArrayList(usize) = .empty;
defer label_list.deinit(gpa);
try label_list.ensureUnusedCapacity(gpa, case_list.len);
const stack_slot = try gen.makeStackSlot();
try expr(gen, child_scope, eval_expr);
try gen.emitConstInst(.store, stack_slot);
try gen.emitSimpleInst(.pop);
for (case_list) |case_stmt| {
const case_label_index = try gen.makeLabel();
label_list.appendAssumeCapacity(case_label_index);
switch (case_stmt.tag) {
.switch_case => {
const case_eval_expr = case_stmt.data.bin.lhs orelse unreachable;
switch (case_eval_expr.tag) {
.number_literal, .true_literal, .false_literal => {},
else => return gen.fail(.invalid_switch_case, case_stmt),
}
try gen.emitConstInst(.load, stack_slot);
try expr(gen, child_scope, case_eval_expr);
try gen.emitSimpleInst(.cmp_eq);
const fixup_offset = try gen.emitJumpInst(.jmp_t);
_ = try gen.makeFixup(.{
.mode = .relative,
.label_index = case_label_index,
.code_offset = fixup_offset,
});
try gen.emitSimpleInst(.pop);
},
.else_branch => {
const fixup_offset = try gen.emitJumpInst(.jmp);
_ = try gen.makeFixup(.{
.mode = .relative,
.label_index = case_label_index,
.code_offset = fixup_offset,
});
},
else => unreachable,
}
}
for (case_list, label_list.items) |case_stmt, case_label_index| {
gen.setLabel(case_label_index);
switch (case_stmt.tag) {
.switch_case => {
try gen.emitSimpleInst(.pop);
},
.else_branch => {},
else => unreachable,
}
const block_stmt = case_stmt.data.bin.rhs;
try blockStmt(gen, child_scope, block_stmt);
const fixup_offset = try gen.emitJumpInst(.jmp);
_ = try gen.makeFixup(.{
.mode = .relative,
.label_index = gen.exit_label,
.code_offset = fixup_offset,
});
}
gen.setLabel(gen.exit_label);
}
fn ifStmt(
parent_block: *GenIr,
scope: *Scope,
stmt_node: *const Ast.Node,
) InnerError!Ir.Inst.Index {
) InnerError!Ir.Inst.Ref {
const astgen = parent_block.astgen;
const cond_expr = stmt_node.data.switch_stmt.condition_expr.?;
try validateSwitchProngs(parent_block, stmt_node);
@ -736,7 +660,7 @@ fn ifStmt(
const cond_inst = try expr(&block_scope, scope, cond_expr);
const condbr = try block_scope.addCondBr(.condbr);
const block = try parent_block.makeBlockInst();
const block = try parent_block.makeBlockInst(.block);
try block_scope.setBlockBody(block); // unstacks block
try parent_block.instructions.append(astgen.gpa, block);
@ -757,14 +681,14 @@ fn ifStmt(
}
try setCondBrPayload(condbr, cond_inst, &then_block, &else_block);
return @enumFromInt(0);
return condbr.toRef();
}
fn ifChain(
parent_block: *GenIr,
scope: *Scope,
branch_list: []const *Ast.Node,
) InnerError!Ir.Inst.Index {
) InnerError!Ir.Inst.Ref {
const gpa = parent_block.astgen.gpa;
if (branch_list.len == 0) return @enumFromInt(0);
if (branch_list[0].data.bin.lhs == null) {
@ -781,7 +705,7 @@ fn ifChain(
const body_node = branch.data.bin.rhs.?;
const cond_inst = try expr(&block_scope, scope, cond_expr);
const condbr = try block_scope.addCondBr(.condbr);
const block_inst = try parent_block.makeBlockInst();
const block_inst = try parent_block.makeBlockInst(.block);
try block_scope.setBlockBody(block_inst);
try parent_block.instructions.append(gpa, block_inst);
@ -803,7 +727,7 @@ fn multiIfStmt(
parent_block: *GenIr,
scope: *Scope,
stmt_node: *const Ast.Node,
) InnerError!Ir.Inst.Index {
) InnerError!Ir.Inst.Ref {
try validateSwitchProngs(parent_block, stmt_node);
const branch_list = stmt_node.data.switch_stmt.cases;
@ -817,19 +741,9 @@ fn multiIfStmt(
return @enumFromInt(0);
}
fn inlineLogicExpr(
gen: *GenIr,
scope: *Scope,
expr_node: *const Ast.Node,
) InnerError!Ir.Inst.Index {
const main_node = expr_node.data.bin.lhs;
assert(main_node != null);
return expr(gen, scope, main_node);
}
fn contentExpr(block: *GenIr, scope: *Scope, expr_node: *const Ast.Node) InnerError!Ir.Inst.Index {
fn contentExpr(block: *GenIr, scope: *Scope, expr_node: *const Ast.Node) InnerError!Ir.Inst.Ref {
// FIXME: This is a placeholder until we figure out what this function should be returning.
var last_inst: Ir.Inst.Index = undefined;
var last_inst: Ir.Inst.Ref = undefined;
// TODO: Make sure that this is not nullable.
const node_list = expr_node.data.list.items.?;
for (node_list) |child_node| {
@ -838,29 +752,30 @@ fn contentExpr(block: *GenIr, scope: *Scope, expr_node: *const Ast.Node) InnerEr
.inline_logic_expr => try inlineLogicExpr(block, scope, child_node),
.if_stmt => try ifStmt(block, scope, child_node),
.multi_if_stmt => try multiIfStmt(block, scope, child_node),
//.switch_stmt => try switchStmt(gen, scope, child_node),
//.switch_stmt => try switchStmt(block, scope, child_node),
else => unreachable,
};
last_inst = try block.add(.{ .tag = .content_push, .data = undefined });
last_inst = try block.addUnaryNode(.content_push, last_inst);
}
return last_inst;
}
fn contentStmt(gen: *GenIr, scope: *Scope, stmt_node: *const Ast.Node) InnerError!Ir.Inst.Index {
const expr_node = stmt_node.data.bin.lhs orelse unreachable;
fn contentStmt(gen: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!Ir.Inst.Ref {
const expr_node = node.data.bin.lhs.?;
const expr_ref = try contentExpr(gen, scope, expr_node);
return gen.addUnaryNode(.content_flush, expr_ref);
}
fn assignStmt(gi: *GenIr, scope: *Scope, stmt_node: *const Ast.Node) InnerError!void {
fn assignStmt(gi: *GenIr, scope: *Scope, node: *const Ast.Node) InnerError!void {
const astgen = gi.astgen;
const identifier_node = stmt_node.data.bin.lhs orelse unreachable;
const expr_node = stmt_node.data.bin.rhs orelse unreachable;
const identifier_node = node.data.bin.lhs.?;
const expr_node = node.data.bin.rhs.?;
const name_ref = try astgen.stringFromNode(identifier_node);
// TODO: Support globals as well
if (scope.lookup(name_ref)) |decl| {
const expr_result = try expr(gi, scope, expr_node);
_ = try gi.addBinaryNode(.store_local, decl.inst_index, expr_result);
_ = try gi.addBinaryNode(.store, decl.inst_index.toRef(), expr_result);
return;
}
return gi.fail(.unknown_identifier, identifier_node);
@ -949,13 +864,13 @@ fn tempDecl(gi: *GenIr, scope: *Scope, decl_node: *const Ast.Node) !void {
return gi.fail(.redefined_identifier, decl_node);
}
const alloc_inst = try gi.add(.{ .tag = .alloc_local, .data = undefined });
const alloc_inst = try gi.add(.{ .tag = .alloc, .data = undefined });
const expr_result = try expr(gi, scope, expr_node);
_ = try gi.addBinaryNode(.store_local, alloc_inst, expr_result);
_ = try gi.addBinaryNode(.store, alloc_inst, expr_result);
return scope.insert(name_ref, .{
.decl_node = decl_node,
.inst_index = alloc_inst,
.inst_index = alloc_inst.toIndex().?,
});
}

View file

@ -1,5 +1,6 @@
const std = @import("std");
const Ast = @import("Ast.zig");
const assert = std.debug.assert;
const Ir = @This();
string_bytes: []u8,
@ -13,8 +14,35 @@ pub const Inst = struct {
data: Data,
pub const Index = enum(u32) {
file_inst = 0,
file_inst,
ref_start_index = 32,
_,
pub fn toRef(i: Index) Inst.Ref {
return @enumFromInt(@intFromEnum(Index.ref_start_index) + @intFromEnum(i));
}
};
pub const Ref = enum(u32) {
bool_true,
bool_false,
none = std.math.maxInt(u32),
_,
pub fn toIndex(inst: Ref) ?Index {
assert(inst != .none);
const ref_int = @intFromEnum(inst);
if (ref_int >= @intFromEnum(Index.ref_start_index)) {
return @enumFromInt(ref_int - @intFromEnum(Index.ref_start_index));
} else {
return null;
}
}
pub fn toIndexAllowNone(inst: Ref) ?Index {
if (inst == .none) return null;
return toIndex(inst);
}
};
pub const Tag = enum {
@ -23,12 +51,9 @@ pub const Inst = struct {
decl_knot,
decl_var,
decl_ref,
block,
condbr,
@"break",
alloc_local,
load_local,
store_local,
alloc,
load,
store,
add,
sub,
mul,
@ -42,10 +67,11 @@ pub const Inst = struct {
cmp_gte,
cmp_lt,
cmp_lte,
true_literal,
false_literal,
integer,
string,
block,
condbr,
@"break",
content_push,
content_flush,
};
@ -55,11 +81,11 @@ pub const Inst = struct {
payload_index: u32,
},
un: struct {
lhs: Index,
lhs: Ref,
},
bin: struct {
lhs: Index,
rhs: Index,
lhs: Ref,
rhs: Ref,
},
integer: struct {
value: u64,
@ -96,7 +122,7 @@ pub const Inst = struct {
};
pub const CondBr = struct {
condition: Index,
condition: Ref,
then_body_len: u32,
else_body_len: u32,
};
@ -165,6 +191,22 @@ const Render = struct {
}
};
fn renderInstIndex(r: *Render, index: Inst.Index) !void {
const io_w = r.writer;
return io_w.print("%{d}", .{@intFromEnum(index)});
}
fn renderInstRef(r: *Render, ref: Inst.Ref) !void {
const io_w = r.writer;
if (ref == .none) {
return io_w.writeAll(".none");
} else if (ref.toIndex()) |i| {
return r.renderInstIndex(i);
} else {
return io_w.print("@{s}", .{@tagName(ref)});
}
}
fn renderSimple(r: *Render, inst: Inst) Error!void {
const io_w = r.writer;
return io_w.print("{s}(?)", .{@tagName(inst.tag)});
@ -172,12 +214,22 @@ const Render = struct {
fn renderUnary(r: *Render, inst: Inst) Error!void {
const io_w = r.writer;
return io_w.print("{s}(%{d})", .{ @tagName(inst.tag), inst.data.un.lhs });
const data = inst.data.un;
try io_w.print("{s}(", .{@tagName(inst.tag)});
const lhs = data.lhs;
try renderInstRef(r, lhs);
return io_w.writeAll(")");
}
fn renderBinary(r: *Render, inst: Inst) Error!void {
const io_w = r.writer;
return io_w.print("{s}(%{d}, %{d})", .{ @tagName(inst.tag), inst.data.bin.lhs, inst.data.bin.rhs });
const data = inst.data.bin;
try io_w.print("{s}(", .{@tagName(inst.tag)});
try renderInstRef(r, data.lhs);
try io_w.writeAll(", ");
try renderInstRef(r, data.rhs);
return io_w.writeAll(")");
}
fn renderBodyInner(r: *Render, ir: Ir, body_list: []const Inst.Index) Error!void {
@ -216,7 +268,9 @@ const Render = struct {
const then_body = ir.bodySlice(extra.end, extra.data.then_body_len);
const else_body = ir.bodySlice(extra.end + then_body.len, extra.data.else_body_len);
try io_w.print("{s}(%{d}, ", .{ @tagName(inst.tag), extra.data.condition });
try io_w.print("{s}(", .{@tagName(inst.tag)});
try renderInstRef(r, extra.data.condition);
try io_w.writeAll(", ");
try renderBodyInner(r, ir, then_body);
try io_w.writeAll(", ");
try renderBodyInner(r, ir, else_body);
@ -286,9 +340,9 @@ const Render = struct {
},
.condbr => try r.renderCondbr(ir, inst),
.@"break" => try r.renderBreak(ir, inst),
.alloc_local => try r.renderSimple(inst),
.load_local => try r.renderUnary(inst),
.store_local => try r.renderBinary(inst),
.alloc => try r.renderSimple(inst),
.load => try r.renderUnary(inst),
.store => try r.renderBinary(inst),
.block => try r.renderBlock(ir, inst),
.add => try r.renderBinary(inst),
.sub => try r.renderBinary(inst),
@ -303,12 +357,6 @@ const Render = struct {
.cmp_gte => try r.renderBinary(inst),
.cmp_lt => try r.renderBinary(inst),
.cmp_lte => try r.renderBinary(inst),
.true_literal => {
try io_w.print("{s}", .{@tagName(inst.tag)});
},
.false_literal => {
try io_w.print("{s}", .{@tagName(inst.tag)});
},
.integer => {
const value = inst.data.integer.value;
try io_w.print("{s}({d})", .{ @tagName(inst.tag), value });
@ -317,8 +365,8 @@ const Render = struct {
const str_bytes = inst.data.string.get(ir);
try io_w.print("{s}(\"{s}\")", .{ @tagName(inst.tag), str_bytes });
},
.content_push => try r.renderSimple(inst),
.content_flush => try r.renderUnary(inst),
.content_push => try r.renderUnary(inst),
.content_flush => try r.renderSimple(inst),
}
try io_w.writeAll("\n");
}
@ -329,7 +377,7 @@ pub fn render(ir: Ir, gpa: std.mem.Allocator, writer: *std.Io.Writer) !void {
var r = Render{ .gpa = gpa, .writer = writer, .prefix = .{} };
defer r.prefix.deinit(gpa);
try r.renderInst(ir, .file_inst);
try r.renderInst(ir, @enumFromInt(0));
return writer.flush();
}
@ -367,6 +415,7 @@ pub fn extraData(ir: Ir, comptime T: type, index: usize) ExtraData(T) {
@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"),
};

View file

@ -7,8 +7,9 @@ const Sema = @This();
gpa: std.mem.Allocator,
ir: *const Ir,
bytecode: std.ArrayListUnmanaged(u8) = .empty,
constants: std.ArrayListUnmanaged(CompiledStory.Constant) = .empty,
constant_map: std.AutoHashMapUnmanaged(CompiledStory.Constant, u32) = .empty,
globals: std.ArrayListUnmanaged(u32) = .empty,
knots: std.ArrayListUnmanaged(CompiledStory.Knot) = .empty,
const InnerError = error{
@ -17,10 +18,21 @@ const InnerError = error{
InvalidJump,
};
const Ref = union(enum) {
bool_true,
bool_false,
none,
index: u32,
constant: u32,
global: u32,
local: u32,
};
fn deinit(sema: *Sema) void {
const gpa = sema.gpa;
sema.bytecode.deinit(gpa);
sema.constants.deinit(gpa);
sema.constant_map.deinit(gpa);
sema.globals.deinit(gpa);
sema.knots.deinit(gpa);
sema.* = undefined;
}
@ -29,51 +41,290 @@ fn fail(_: *Sema, message: []const u8) InnerError {
@panic(message);
}
fn resolveIndex(sema: *Sema, index: Ir.Inst.Index) Ir.Inst {
return sema.ir.instructions[@intFromEnum(index)];
}
fn emitByte(sema: *Sema, byte: u8) !void {
fn getConstant(sema: *Sema, data: CompiledStory.Constant) !Ref {
const gpa = sema.gpa;
return sema.bytecode.append(gpa, byte);
}
fn emitByteOp(sema: *Sema, op: Story.Opcode) !void {
return emitByte(sema, @intFromEnum(op));
}
fn emitConstOp(sema: *Sema, op: Story.Opcode, arg: u8) !void {
const gpa = sema.gpa;
if (arg >= std.math.maxInt(u8)) return error.TooManyConstants;
try sema.bytecode.ensureUnusedCapacity(gpa, 2);
sema.bytecode.appendAssumeCapacity(@intFromEnum(op));
sema.bytecode.appendAssumeCapacity(arg);
}
fn emitJumpOp(sema: *Sema, op: Story.Opcode) error{OutOfMemory}!usize {
const gpa = sema.gpa;
try sema.bytecode.ensureUnusedCapacity(gpa, 3);
sema.bytecode.appendAssumeCapacity(@intFromEnum(op));
sema.bytecode.appendAssumeCapacity(0xff);
sema.bytecode.appendAssumeCapacity(0xff);
return sema.bytecode.items.len - 2;
}
fn makeConstant(sema: *Sema, data: CompiledStory.Constant) !usize {
const gpa = sema.gpa;
const const_index = sema.constants.items.len;
if (sema.constant_map.get(data)) |index| {
return .{ .constant = index };
} else {
const index = sema.constants.items.len;
try sema.constants.append(gpa, data);
return const_index;
try sema.constant_map.put(gpa, data, @intCast(index));
return .{ .constant = @intCast(index) };
}
}
fn addGlobal(sema: *Sema, name: Ir.NullTerminatedString) !Ref {
const gpa = sema.gpa;
const interned = try sema.getConstant(.{ .string = name });
try sema.globals.append(gpa, interned.constant);
return .{ .global = interned.constant };
}
fn getGlobal(sema: *Sema, name: Ir.NullTerminatedString) !Ref {
const interned = try sema.getConstant(.{ .string = name });
for (sema.ir.globals) |global| {
if (name == global.name) {
return .{ .global = interned.constant };
}
}
return sema.fail("unknown global variable");
}
fn irInteger(sema: *Sema, inst: Ir.Inst.Index) InnerError!Ref {
const data = sema.ir.instructions[@intFromEnum(inst)].data.integer;
return sema.getConstant(.{ .integer = data.value });
}
fn irString(sema: *Sema, inst: Ir.Inst.Index) InnerError!Ref {
const data = sema.ir.instructions[@intFromEnum(inst)].data.string;
return sema.getConstant(.{ .string = data.start });
}
fn irUnary(
sema: *Sema,
chunk: *Chunk,
inst: Ir.Inst.Index,
op: Story.Opcode,
) InnerError!Ref {
const data = sema.ir.instructions[@intFromEnum(inst)].data.un;
const lhs = chunk.resolveInst(data.lhs);
_ = try chunk.doLoad(lhs);
return chunk.addByteOp(op);
}
fn irBinary(
sema: *Sema,
chunk: *Chunk,
inst: Ir.Inst.Index,
op: Story.Opcode,
) InnerError!Ref {
const data = sema.ir.instructions[@intFromEnum(inst)].data.bin;
const lhs = chunk.resolveInst(data.lhs);
const rhs = chunk.resolveInst(data.rhs);
_ = try chunk.doLoad(lhs);
_ = try chunk.doLoad(rhs);
return chunk.addByteOp(op);
}
fn irAlloc(_: *Sema, chunk: *Chunk, _: Ir.Inst.Index) InnerError!Ref {
const local_index = chunk.knot.stack_size;
// TODO: Add constraints on how many temporaries we can have.
// max(u8) or max(u16) are most likey appropriate.
chunk.knot.stack_size += 1;
return .{ .local = local_index };
}
fn irStore(sema: *Sema, chunk: *Chunk, inst: Ir.Inst.Index) InnerError!void {
const data = sema.ir.instructions[@intFromEnum(inst)].data.bin;
const lhs = chunk.resolveInst(data.lhs);
const rhs = chunk.resolveInst(data.rhs);
_ = try chunk.doLoad(rhs);
switch (lhs) {
.bool_true, .bool_false => unreachable, // TODO: "Cannot assign to boolean"
.none => unreachable,
.constant => |_| unreachable, // TODO: "Cannot assign to constant"
.global => |id| _ = try chunk.addConstOp(.store_global, @intCast(id)),
.local => |id| _ = try chunk.addConstOp(.store, @intCast(id)),
.index => unreachable,
}
_ = try chunk.addByteOp(.pop);
}
// TODO: Check what the target is!
fn irLoad(sema: *Sema, chunk: *Chunk, inst: Ir.Inst.Index) InnerError!Ref {
const data = sema.ir.instructions[@intFromEnum(inst)].data.un;
const lhs = chunk.resolveInst(data.lhs);
return chunk.doLoad(lhs);
}
fn irCondBr(sema: *Sema, chunk: *Chunk, inst: Ir.Inst.Index) InnerError!Ref {
const data = sema.ir.instructions[@intFromEnum(inst)].data.payload;
const extra = sema.ir.extraData(Ir.Inst.CondBr, data.payload_index);
const then_body = sema.ir.bodySlice(extra.end, extra.data.then_body_len);
const else_body = sema.ir.bodySlice(extra.end + then_body.len, extra.data.else_body_len);
const else_label = try chunk.addLabel();
const end_label = try chunk.addLabel();
const condition = chunk.resolveInst(extra.data.condition);
_ = try chunk.doLoad(condition);
try chunk.addFixup(.jmp_f, else_label);
_ = try chunk.addByteOp(.pop);
try blockBodyInner(sema, chunk, then_body);
try chunk.addFixup(.jmp, end_label);
chunk.setLabel(else_label);
_ = try chunk.addByteOp(.pop);
try blockBodyInner(sema, chunk, else_body);
chunk.setLabel(end_label);
return .none;
}
fn irBreak(sema: *Sema, inst: Ir.Inst.Index) InnerError!void {
_ = sema;
_ = inst;
}
fn irBlock(sema: *Sema, chunk: *Chunk, inst: Ir.Inst.Index) InnerError!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);
return blockBodyInner(sema, chunk, body);
}
fn irContentPush(sema: *Sema, chunk: *Chunk, inst: Ir.Inst.Index) InnerError!Ref {
const data = sema.ir.instructions[@intFromEnum(inst)].data.un;
const lhs = chunk.resolveInst(data.lhs);
_ = try chunk.doLoad(lhs);
return chunk.addByteOp(.stream_push);
}
fn irContentFlush(_: *Sema, chunk: *Chunk, _: Ir.Inst.Index) InnerError!Ref {
return chunk.addByteOp(.stream_flush);
}
fn irDeclRef(sema: *Sema, _: *Chunk, inst: Ir.Inst.Index) InnerError!Ref {
const data = sema.ir.instructions[@intFromEnum(inst)].data.string;
return sema.getGlobal(data.start);
}
fn irDeclVar(
sema: *Sema,
chunk: *Chunk,
name: Ir.NullTerminatedString,
inst: Ir.Inst.Index,
) InnerError!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);
try blockBodyInner(sema, chunk, body);
// FIXME: hack
{
const last_inst = body[body.len - 1].toRef();
const val = chunk.resolveInst(last_inst);
_ = try chunk.doLoad(val);
}
const global = try sema.addGlobal(name);
_ = try chunk.addConstOp(.store_global, @intCast(global.global));
_ = try chunk.addByteOp(.pop);
}
fn irDeclKnot(
sema: *Sema,
name_ref: Ir.NullTerminatedString,
inst: Ir.Inst.Index,
) InnerError!void {
const gpa = sema.gpa;
const data = sema.ir.instructions[@intFromEnum(inst)].data.payload;
const extra = sema.ir.extraData(Ir.Inst.Knot, data.payload_index);
var knot: CompiledStory.Knot = .{
.name = name_ref,
.arity = 0,
.stack_size = 0,
};
var chunk: Chunk = .{
.sema = sema,
.knot = &knot,
};
defer chunk.fixups.deinit(gpa);
defer chunk.labels.deinit(gpa);
defer chunk.inst_map.deinit(gpa);
const body = sema.ir.bodySlice(extra.end, extra.data.body_len);
try blockBodyInner(sema, &chunk, body);
_ = try chunk.addByteOp(.exit);
try chunk.resolveLabels();
try sema.knots.append(gpa, knot);
}
fn irDeclaration(sema: *Sema, parent_chunk: ?*Chunk, inst: Ir.Inst.Index) !void {
const data = sema.ir.instructions[@intFromEnum(inst)].data.payload;
const extra = sema.ir.extraData(Ir.Inst.Declaration, data.payload_index).data;
const value_data = sema.ir.instructions[@intFromEnum(extra.value)];
switch (value_data.tag) {
.decl_var => try irDeclVar(sema, parent_chunk.?, extra.name, extra.value),
.decl_knot => try irDeclKnot(sema, extra.name, extra.value),
else => unreachable,
}
}
fn blockBodyInner(sema: *Sema, chunk: *Chunk, body: []const Ir.Inst.Index) InnerError!void {
const gpa = sema.gpa;
for (body) |inst| {
const data = sema.ir.instructions[@intFromEnum(inst)];
const ref: Ref = switch (data.tag) {
.file => unreachable,
.declaration => {
try irDeclaration(sema, chunk, inst);
continue;
},
.decl_var => unreachable, // handled in declaration()
.decl_knot => unreachable, // handled in declaration()
.alloc => try irAlloc(sema, chunk, inst),
.store => {
try irStore(sema, chunk, inst);
continue;
},
.load => try irLoad(sema, chunk, inst),
.add => try irBinary(sema, chunk, inst, .add),
.sub => try irBinary(sema, chunk, inst, .sub),
.mul => try irBinary(sema, chunk, inst, .mul),
.div => try irBinary(sema, chunk, inst, .div),
.mod => try irBinary(sema, chunk, inst, .mod),
.neg => try irUnary(sema, chunk, inst, .neg),
.not => try irUnary(sema, chunk, inst, .not),
.cmp_eq => try irBinary(sema, chunk, inst, .cmp_eq),
.cmp_neq => blk: {
_ = try irBinary(sema, chunk, inst, .cmp_eq);
const tmp = try chunk.addByteOp(.not);
break :blk tmp;
},
.cmp_lt => try irBinary(sema, chunk, inst, .cmp_lt),
.cmp_lte => try irBinary(sema, chunk, inst, .cmp_lte),
.cmp_gt => try irBinary(sema, chunk, inst, .cmp_gt),
.cmp_gte => try irBinary(sema, chunk, inst, .cmp_gte),
.decl_ref => try irDeclRef(sema, chunk, inst),
.integer => try irInteger(sema, inst),
.string => try irString(sema, inst),
.condbr => try irCondBr(sema, chunk, inst),
.@"break" => {
try irBreak(sema, inst);
continue;
},
.block => {
try irBlock(sema, chunk, inst);
continue;
},
.content_push => try irContentPush(sema, chunk, inst),
.content_flush => try irContentFlush(sema, chunk, inst),
};
try chunk.inst_map.put(gpa, inst, ref);
}
}
fn file(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);
// FIXME: We are going to get burned by this if we don't formalize it.
// Adding common constants to the constant pool.
_ = try sema.getConstant(.{ .integer = 0 });
_ = try sema.getConstant(.{ .integer = 1 });
for (body) |body_index| try irDeclaration(sema, null, body_index);
}
const Chunk = struct {
sema: *Sema,
name: Ir.NullTerminatedString,
arity: u32,
stack_size: u32,
stack_map: std.AutoHashMapUnmanaged(Ir.Inst.Index, u32),
labels: std.ArrayListUnmanaged(Label),
fixups: std.ArrayListUnmanaged(Fixup),
knot: *CompiledStory.Knot,
labels: std.ArrayListUnmanaged(Label) = .empty,
fixups: std.ArrayListUnmanaged(Fixup) = .empty,
inst_map: std.AutoHashMapUnmanaged(Ir.Inst.Index, Ref) = .empty,
const dummy_address = 0xffffffff;
@ -86,15 +337,55 @@ const Chunk = struct {
relative,
absolute,
},
label_index: usize,
code_offset: usize,
label_index: u32,
code_offset: u32,
};
fn addByteOp(chunk: *Chunk, op: Story.Opcode) error{OutOfMemory}!Ref {
const gpa = chunk.sema.gpa;
const bytecode = &chunk.knot.bytecode;
const byte_index = bytecode.items.len;
try bytecode.append(gpa, @intFromEnum(op));
return .{ .index = @intCast(byte_index) };
}
fn addConstOp(chunk: *Chunk, op: Story.Opcode, arg: u8) error{OutOfMemory}!Ref {
const gpa = chunk.sema.gpa;
const bytecode = &chunk.knot.bytecode;
const byte_index = bytecode.items.len;
try bytecode.ensureUnusedCapacity(gpa, 2);
bytecode.appendAssumeCapacity(@intFromEnum(op));
bytecode.appendAssumeCapacity(arg);
return .{ .index = @intCast(byte_index) };
}
fn addJumpOp(chunk: *Chunk, op: Story.Opcode) error{OutOfMemory}!Ref {
const gpa = chunk.sema.gpa;
const bytecode = &chunk.knot.bytecode;
try bytecode.ensureUnusedCapacity(gpa, 3);
bytecode.appendAssumeCapacity(@intFromEnum(op));
bytecode.appendAssumeCapacity(0xff);
bytecode.appendAssumeCapacity(0xff);
return .{ .index = @intCast(bytecode.items.len - 2) };
}
fn resolveInst(chunk: *Chunk, ref: Ir.Inst.Ref) Ref {
if (ref.toIndex()) |index| {
return chunk.inst_map.get(index).?;
}
switch (ref) {
.bool_true => return .bool_true,
.bool_false => return .bool_false,
else => return .{ .constant = @intFromEnum(ref) },
}
}
fn addFixup(chunk: *Chunk, op: Story.Opcode, label: usize) !void {
const code_ref = try chunk.addJumpOp(op);
return chunk.fixups.append(chunk.sema.gpa, .{
.mode = .relative,
.label_index = label,
.code_offset = try chunk.sema.emitJumpOp(op),
.label_index = @intCast(label),
.code_offset = code_ref.index,
});
}
@ -107,7 +398,7 @@ const Chunk = struct {
}
fn setLabel(chunk: *Chunk, label_index: usize) void {
const code_offset = chunk.sema.bytecode.items.len;
const code_offset = chunk.knot.bytecode.items.len;
assert(label_index <= chunk.labels.items.len);
const label_data = &chunk.labels.items[label_index];
@ -117,7 +408,7 @@ const Chunk = struct {
fn resolveLabels(chunk: *Chunk) !void {
const start_index = 0;
const end_index = chunk.fixups.items.len;
const bytecode = &chunk.sema.bytecode;
const bytecode = &chunk.knot.bytecode;
for (chunk.fixups.items[start_index..end_index]) |fixup| {
const label = chunk.labels.items[fixup.label_index];
@ -135,244 +426,44 @@ const Chunk = struct {
bytecode.items[fixup.code_offset + 1] = @intCast(target_offset & 0xff);
}
}
fn doLoad(chunk: *Chunk, ref: Ref) InnerError!Ref {
const gpa = chunk.sema.gpa;
switch (ref) {
.bool_true => return chunk.addByteOp(.true),
.bool_false => return chunk.addByteOp(.false),
.none => return ref,
.constant => |id| {
// TODO: This isn't great. New constant indexes are
// created each time.
const ref_const = chunk.knot.constants.items.len;
try chunk.knot.constants.append(gpa, id);
return chunk.addConstOp(.load_const, @intCast(ref_const));
},
.global => |id| {
// TODO: This isn't great. New constant indexes are
// created each time.
const ref_const = chunk.knot.constants.items.len;
try chunk.knot.constants.append(gpa, id);
return chunk.addConstOp(.load_global, @intCast(ref_const));
},
.local => |id| return chunk.addConstOp(.load, @intCast(id)),
.index => return ref,
}
}
};
fn integerInst(sema: *Sema, inst: Ir.Inst) InnerError!void {
const int_const = try sema.makeConstant(.{
.integer = inst.data.integer.value,
});
return emitConstOp(sema, .load_const, @intCast(int_const));
}
fn stringInst(sema: *Sema, inst: Ir.Inst) InnerError!void {
const str_const = try sema.makeConstant(.{
.string = inst.data.string.start,
});
return emitConstOp(sema, .load_const, @intCast(str_const));
}
fn unaryInst(sema: *Sema, _: Ir.Inst, op: Story.Opcode) InnerError!void {
return emitByteOp(sema, op);
}
fn binaryInst(sema: *Sema, _: Ir.Inst, op: Story.Opcode) InnerError!void {
return emitByteOp(sema, op);
}
fn irContentPush(sema: *Sema, _: Ir.Inst) InnerError!void {
return emitByteOp(sema, .stream_push);
}
fn irContentFlush(sema: *Sema, _: Ir.Inst) InnerError!void {
return emitByteOp(sema, .stream_flush);
}
fn allocLocal(sema: *Sema, chunk: *Chunk, inst: Ir.Inst.Index) InnerError!void {
const gpa = sema.gpa;
// TODO: Add constraints on how many temporaries we can have.
// max(u8) or max(u16) are most likey appropriate.
try chunk.stack_map.put(gpa, inst, chunk.stack_size);
chunk.stack_size += 1;
}
fn storeLocal(sema: *Sema, chunk: *Chunk, inst: Ir.Inst) InnerError!void {
const temp_inst = inst.data.bin.lhs;
const stack_offset = chunk.stack_map.get(temp_inst).?;
try emitConstOp(sema, .store, @intCast(stack_offset));
try emitByteOp(sema, .pop);
}
fn loadLocal(sema: *Sema, chunk: *Chunk, inst: Ir.Inst) InnerError!void {
const temp_inst = inst.data.un.lhs;
const stack_offset = chunk.stack_map.get(temp_inst).?;
try emitConstOp(sema, .load, @intCast(stack_offset));
}
fn irCondBr(sema: *Sema, chunk: *Chunk, inst: Ir.Inst) InnerError!void {
const payload_node = inst.data.payload;
const extra = sema.ir.extraData(Ir.Inst.CondBr, payload_node.payload_index);
const then_body = sema.ir.bodySlice(extra.end, extra.data.then_body_len);
const else_body = sema.ir.bodySlice(extra.end + then_body.len, extra.data.else_body_len);
const else_label = try chunk.addLabel();
const end_label = try chunk.addLabel();
try chunk.addFixup(.jmp_f, else_label);
try emitByteOp(sema, .pop);
for (then_body) |body_index| try compileInst(sema, chunk, body_index);
try chunk.addFixup(.jmp, end_label);
chunk.setLabel(else_label);
try emitByteOp(sema, .pop);
for (else_body) |body_index| try compileInst(sema, chunk, body_index);
chunk.setLabel(end_label);
}
fn irBreak(sema: *Sema, inst: Ir.Inst) InnerError!void {
_ = sema;
_ = inst;
}
fn irBlock(sema: *Sema, chunk: *Chunk, block_inst: Ir.Inst) InnerError!void {
const payload_node = block_inst.data.payload;
const extra = sema.ir.extraData(Ir.Inst.Block, payload_node.payload_index);
const body = sema.ir.bodySlice(extra.end, extra.data.body_len);
for (body) |body_index| try compileInst(sema, chunk, body_index);
}
fn declRef(sema: *Sema, inst: Ir.Inst) InnerError!void {
const ir = sema.ir;
const str = inst.data.string.start;
for (ir.globals) |global| {
if (str == global.name) {
const constant_index = try sema.makeConstant(.{ .string = str });
return emitConstOp(sema, .load_global, @intCast(constant_index));
}
}
return sema.fail("unknown global variable");
}
fn declVar(
sema: *Sema,
chunk: *Chunk,
name: Ir.NullTerminatedString,
inst: Ir.Inst,
) InnerError!void {
const ir = sema.ir;
const payload_index = inst.data.payload.payload_index;
const extra = ir.extraData(Ir.Inst.Block, payload_index);
const body = ir.bodySlice(extra.end, extra.data.body_len);
for (body) |body_index| try compileInst(sema, chunk, body_index);
for (ir.globals) |global| {
if (name == global.name) {
const constant_index = try sema.makeConstant(.{ .string = name });
return emitConstOp(sema, .store_global, @intCast(constant_index));
}
}
return sema.fail("unknown global variable");
}
fn declKnot(sema: *Sema, name_ref: Ir.NullTerminatedString, inst: Ir.Inst) InnerError!void {
const gpa = sema.gpa;
const ir = sema.ir;
const extra = ir.extraData(Ir.Inst.Knot, inst.data.payload.payload_index);
const bytecode_start = sema.bytecode.items.len;
const const_start = sema.constants.items.len;
var chunk: Chunk = .{
.sema = sema,
.name = name_ref,
.arity = 0,
.stack_size = 0,
.stack_map = .empty,
.fixups = .empty,
.labels = .empty,
};
defer chunk.stack_map.deinit(gpa);
defer chunk.fixups.deinit(gpa);
defer chunk.labels.deinit(gpa);
const body_slice = ir.bodySlice(extra.end, extra.data.body_len);
for (body_slice) |body_inst| try compileInst(sema, &chunk, body_inst);
try emitByteOp(sema, .exit);
try chunk.resolveLabels();
try sema.knots.append(gpa, .{
.name_ref = name_ref,
.arity = chunk.arity,
.stack_size = chunk.stack_size,
.bytecode = .{
.start = @intCast(bytecode_start),
.len = @intCast(sema.bytecode.items.len - bytecode_start),
},
.constants = .{
.start = @intCast(const_start),
.len = @intCast(sema.constants.items.len - const_start),
},
});
}
fn declaration(sema: *Sema, parent_chunk: ?*Chunk, inst: Ir.Inst) !void {
const ir = sema.ir;
const extra = ir.extraData(Ir.Inst.Declaration, inst.data.payload.payload_index);
const value_inst = sema.resolveIndex(extra.data.value);
const str = extra.data.name;
switch (value_inst.tag) {
.decl_var => try declVar(sema, parent_chunk.?, str, value_inst),
.decl_knot => try declKnot(sema, str, value_inst),
else => unreachable,
}
}
fn compileInst(sema: *Sema, chunk: *Chunk, index: Ir.Inst.Index) InnerError!void {
const inst = sema.resolveIndex(index);
switch (inst.tag) {
.file => unreachable,
.declaration => try declaration(sema, chunk, inst),
.decl_var => unreachable, // handled in declaration()
.decl_knot => unreachable, // handled in declaration()
.decl_ref => try declRef(sema, inst),
.condbr => try irCondBr(sema, chunk, inst),
.@"break" => try irBreak(sema, inst),
.block => try irBlock(sema, chunk, inst),
.alloc_local => try allocLocal(sema, chunk, index),
.store_local => try storeLocal(sema, chunk, inst),
.load_local => try loadLocal(sema, chunk, inst),
.true_literal => unreachable, // TODO
.false_literal => unreachable, // TODO
.add => try binaryInst(sema, inst, .add),
.sub => try binaryInst(sema, inst, .sub),
.mul => try binaryInst(sema, inst, .mul),
.div => try binaryInst(sema, inst, .div),
.mod => try binaryInst(sema, inst, .mod),
.neg => try unaryInst(sema, inst, .neg),
.not => try unaryInst(sema, inst, .not),
.cmp_eq => try unaryInst(sema, inst, .cmp_eq),
.cmp_neq => {
try unaryInst(sema, inst, .cmp_eq);
try emitByteOp(sema, .not);
},
.cmp_lt => try unaryInst(sema, inst, .cmp_lt),
.cmp_lte => try unaryInst(sema, inst, .cmp_lte),
.cmp_gt => try unaryInst(sema, inst, .cmp_gt),
.cmp_gte => try unaryInst(sema, inst, .cmp_gte),
.content_push => try irContentPush(sema, inst),
.content_flush => try irContentFlush(sema, inst),
.string => try stringInst(sema, inst),
.integer => try integerInst(sema, inst),
}
}
fn file(sema: *Sema, inst: Ir.Inst) !void {
const extra = sema.ir.extraData(Ir.Inst.Block, inst.data.payload.payload_index);
const body = sema.ir.bodySlice(extra.end, extra.data.body_len);
for (body) |body_index| {
const body_inst = sema.resolveIndex(body_index);
assert(body_inst.tag == .declaration);
try declaration(sema, null, body_inst);
}
}
pub const CompiledStory = struct {
knots: []Knot,
constants: []Constant,
bytecode: []u8,
globals: []u32,
pub const Knot = struct {
name_ref: Ir.NullTerminatedString,
name: Ir.NullTerminatedString,
arity: u32,
stack_size: u32,
constants: struct {
start: u32,
len: u32,
},
bytecode: struct {
start: u32,
len: u32,
},
constants: std.ArrayListUnmanaged(u32) = .empty,
bytecode: std.ArrayListUnmanaged(u8) = .empty,
};
pub const Constant = union(enum) {
@ -381,58 +472,57 @@ pub const CompiledStory = struct {
};
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.bytecode);
gpa.free(self.globals);
gpa.free(self.constants);
self.* = undefined;
}
fn bytecodeSlice(self: CompiledStory, knot: *const Knot) []const u8 {
return self.bytecode[knot.bytecode.start..][0..knot.bytecode.len];
}
fn constantsSlice(self: CompiledStory, knot: *const Knot) []const Constant {
return self.constants[knot.constants.start..][0..knot.constants.len];
}
pub fn buildRuntime(
self: *CompiledStory,
gpa: std.mem.Allocator,
ir: Ir,
story: *Story,
) !void {
for (self.knots) |compiled_knot| {
var constant_pool: std.ArrayListUnmanaged(*Object) = .empty;
try constant_pool.ensureUnusedCapacity(gpa, compiled_knot.constants.len);
defer constant_pool.deinit(gpa);
const constants_pool = &story.constants_pool;
try constants_pool.ensureUnusedCapacity(gpa, self.constants.len);
try story.paths.ensureUnusedCapacity(gpa, self.knots.len);
try story.globals.ensureUnusedCapacity(gpa, @intCast(self.globals.len));
const constants_slice = self.constantsSlice(&compiled_knot);
for (constants_slice) |constant| {
for (self.constants) |constant| {
switch (constant) {
.integer => |value| {
const object: *Object.Number = try .create(story, .{
.integer = @intCast(value),
});
constant_pool.appendAssumeCapacity(&object.base);
constants_pool.appendAssumeCapacity(&object.base);
},
.string => |ref| {
const bytes = ir.nullTerminatedString(ref);
const object: *Object.String = try .create(story, bytes);
constant_pool.appendAssumeCapacity(&object.base);
constants_pool.appendAssumeCapacity(&object.base);
},
}
}
const bytecode_slice = self.bytecodeSlice(&compiled_knot);
const chunk_name = ir.nullTerminatedString(compiled_knot.name_ref);
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 runtime_chunk: *Object.ContentPath = try .create(story, .{
.name = try .create(story, chunk_name),
.arity = @intCast(compiled_knot.arity),
.locals_count = @intCast(compiled_knot.stack_size - compiled_knot.arity),
.const_pool = try constant_pool.toOwnedSlice(gpa),
.bytes = try gpa.dupe(u8, bytecode_slice),
.name = try .create(story, ir.nullTerminatedString(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),
});
try story.paths.append(gpa, &runtime_chunk.base);
story.paths.appendAssumeCapacity(&runtime_chunk.base);
}
}
};
@ -444,10 +534,10 @@ pub fn compile(gpa: std.mem.Allocator, ir: *const Ir) !CompiledStory {
};
defer sema.deinit();
try file(&sema, ir.instructions[0]);
try file(&sema, @enumFromInt(0));
return .{
.bytecode = try sema.bytecode.toOwnedSlice(gpa),
.constants = try sema.constants.toOwnedSlice(gpa),
.globals = try sema.globals.toOwnedSlice(gpa),
.knots = try sema.knots.toOwnedSlice(gpa),
};
}

View file

@ -15,6 +15,7 @@ is_exited: bool = false,
can_advance: bool = false,
choice_index: usize = 0,
current_choices: std.ArrayListUnmanaged(Choice) = .empty,
constants_pool: std.ArrayListUnmanaged(*Object) = .empty,
globals: std.StringHashMapUnmanaged(?*Object) = .empty,
paths: std.ArrayListUnmanaged(*Object) = .empty,
stack: std.ArrayListUnmanaged(?*Object) = .empty,
@ -101,6 +102,7 @@ pub fn deinit(story: *Story) void {
}
story.current_choices.deinit(gpa);
story.constants_pool.deinit(gpa);
story.globals.deinit(gpa);
story.paths.deinit(gpa);
story.stack.deinit(gpa);
@ -109,6 +111,24 @@ pub fn deinit(story: *Story) void {
pub fn dump(story: *Story, writer: *std.Io.Writer) !void {
const story_dumper: Dumper = .{ .story = story, .writer = writer };
try writer.writeAll("=== Constants ===\n");
for (story.constants_pool.items) |global_constant| {
try story_dumper.dumpObject(global_constant);
try writer.writeAll("\n");
}
try writer.writeAll("\n");
try writer.writeAll("=== Globals ===\n");
var global_iter = story.globals.iterator();
while (global_iter.next()) |entry| {
try writer.print("{s} => ...\n", .{entry.key_ptr.*});
}
try writer.writeAll("\n");
try writer.writeAll("=== Knots ===\n");
for (story.paths.items) |path_object| {
try story_dumper.dump(@ptrCast(path_object));
}
@ -152,8 +172,7 @@ fn currentFrame(vm: *Story) *CallFrame {
fn peekStack(vm: *Story, offset: usize) ?*Object {
const stack_top = vm.stack.items.len;
assert(stack_top > offset);
assert(stack_top != 0);
if (stack_top <= offset) return null;
return vm.stack.items[stack_top - offset - 1];
}
@ -171,10 +190,11 @@ fn popStack(vm: *Story) ?*Object {
return vm.stack.pop() orelse unreachable;
}
fn getConstant(_: *Story, frame: *CallFrame, offset: u8) !*Object {
const constant_pool = frame.callee.const_pool;
if (offset >= constant_pool.len) return error.InvalidArgument;
return constant_pool[offset];
fn getConstant(story: *Story, frame: *CallFrame, offset: u8) !*Object {
if (offset >= frame.callee.const_pool.len) return error.InvalidArgument;
const constant_index = frame.callee.const_pool[offset];
return story.constants_pool.items[constant_index];
}
fn getLocal(vm: *Story, frame: *CallFrame, offset: u8) ?*Object {
@ -380,14 +400,17 @@ fn execute(vm: *Story) !std.ArrayListUnmanaged(u8) {
frame.ip += 2;
},
.stream_push => {
const arg_object = vm.popStack();
if (arg_object) |object| {
// FIXME: This should be more strict.
// Its not because theres a bug in when these instructions are
// emitted.
if (vm.peekStack(0)) |object| {
const string_object = try Object.String.fromObject(vm, object);
const string_bytes = string_object.bytes[0..string_object.length];
try stream_writer.writer.writeAll(string_bytes);
} else {
return error.InvalidArgument;
}
_ = vm.popStack();
} //else {
//return error.InvalidArgument;
//}
frame.ip += 1;
},

View file

@ -15,8 +15,10 @@ fn dumpSimpleInst(d: Dumper, offset: usize, op: Opcode) !usize {
fn dumpByteInst(d: Dumper, context: *const Object.ContentPath, offset: usize, op: Opcode) !usize {
const arg = context.bytes[offset + 1];
if (op == .load_const) {
const constant_index = context.const_pool[arg];
const global_constant = d.story.constants_pool.items[constant_index];
try d.writer.print("{s} {d} (", .{ @tagName(op), arg });
try d.dumpObject(context.const_pool[arg]);
try d.dumpObject(global_constant);
try d.writer.print(")\n", .{});
} else {
try d.writer.print("{s} {x}\n", .{ @tagName(op), arg });
@ -31,7 +33,9 @@ fn dumpGlobalInst(
op: Opcode,
) !usize {
const arg = context.bytes[offset + 1];
const global_name: *Object.String = @ptrCast(context.const_pool[arg]);
const constant_index = context.const_pool[arg];
const global_constant = d.story.constants_pool.items[constant_index];
const global_name: *Object.String = @ptrCast(global_constant);
const name_bytes = global_name.bytes[0..global_name.length];
try d.writer.print("{s} {x} '{s}'\n", .{ @tagName(op), arg, name_bytes });

View file

@ -264,14 +264,14 @@ pub const Object = struct {
// TODO: Rename this to stack size.
locals_count: usize,
// TODO: Rename this to constant_pool.
const_pool: []*Object,
const_pool: []u32,
bytes: []const u8,
pub const CreateOptions = struct {
name: *Object.String,
arity: usize,
locals_count: usize,
const_pool: []*Object,
const_pool: []u32,
bytes: []const u8,
};