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

@ -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);
if (sema.constant_map.get(data)) |index| {
return .{ .constant = index };
} else {
const index = sema.constants.items.len;
try sema.constants.append(gpa, data);
try sema.constant_map.put(gpa, data, @intCast(index));
return .{ .constant = @intCast(index) };
}
}
fn emitByteOp(sema: *Sema, op: Story.Opcode) !void {
return emitByte(sema, @intFromEnum(op));
}
fn emitConstOp(sema: *Sema, op: Story.Opcode, arg: u8) !void {
fn addGlobal(sema: *Sema, name: Ir.NullTerminatedString) !Ref {
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);
const interned = try sema.getConstant(.{ .string = name });
try sema.globals.append(gpa, interned.constant);
return .{ .global = interned.constant };
}
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 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 makeConstant(sema: *Sema, data: CompiledStory.Constant) !usize {
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 const_index = sema.constants.items.len;
try sema.constants.append(gpa, data);
return const_index;
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| {
switch (constant) {
.integer => |value| {
const object: *Object.Number = try .create(story, .{
.integer = @intCast(value),
});
constant_pool.appendAssumeCapacity(&object.base);
},
.string => |ref| {
const bytes = ir.nullTerminatedString(ref);
const object: *Object.String = try .create(story, bytes);
constant_pool.appendAssumeCapacity(&object.base);
},
}
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);
},
}
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),
};
}