feat: ir for declarations and semantic analyzer start

This commit is contained in:
Brett Broadhurst 2026-03-09 05:57:25 -06:00
parent f16162b5bb
commit 197a37ebe7
Failed to generate hash of commit
4 changed files with 453 additions and 145 deletions

261
src/Sema.zig Normal file
View file

@ -0,0 +1,261 @@
const std = @import("std");
const Ir = @import("Ir.zig");
const Story = @import("Story.zig");
const Object = Story.Object;
const Sema = @This();
gpa: std.mem.Allocator,
ir: *const Ir,
bytecode: std.ArrayListUnmanaged(u8) = .empty,
constants: std.ArrayListUnmanaged(CompiledStory.Constant) = .empty,
knots: std.ArrayListUnmanaged(CompiledStory.Knot) = .empty,
const InnerError = error{
OutOfMemory,
TooManyConstants,
};
fn deinit(sema: *Sema) void {
const gpa = sema.gpa;
sema.bytecode.deinit(gpa);
sema.constants.deinit(gpa);
sema.knots.deinit(gpa);
sema.* = undefined;
}
fn resolveIndex(sema: *Sema, index: Ir.Inst.Index) Ir.Inst {
return sema.ir.instructions[@intFromEnum(index)];
}
fn emitByte(sema: *Sema, byte: u8) !void {
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: usize) !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(@intCast(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;
try sema.constants.append(gpa, data);
return const_index;
}
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 contentInst(sema: *Sema, _: Ir.Inst) InnerError!void {
return emitByteOp(sema, .stream_flush);
}
fn blockInst(sema: *Sema, block_inst: Ir.Inst) InnerError!void {
const ir = sema.ir;
const extra = ir.extraData(Ir.Inst.Block, block_inst.data.payload.payload_index);
const body = ir.bodySlice(extra.end, extra.data.body_len);
for (body) |inst_index| {
const body_inst = resolveIndex(sema, inst_index);
try compileInst(sema, body_inst);
}
}
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, 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, str_const);
}
fn compileInst(sema: *Sema, inst: Ir.Inst) InnerError!void {
switch (inst.tag) {
.block => try blockInst(sema, inst),
.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),
.content => try contentInst(sema, inst),
.string => try stringInst(sema, inst),
.integer => try integerInst(sema, inst),
else => unreachable,
}
}
fn innerDecl(sema: *Sema, inst: Ir.Inst) !void {
const ir = sema.ir;
const extra = ir.extraData(Ir.Inst.Knot, inst.data.payload.payload_index);
const body_slice = ir.bodySlice(extra.end, extra.data.body_len);
for (body_slice) |inst_index| {
const body_inst = resolveIndex(sema, inst_index);
try compileInst(sema, body_inst);
}
}
fn declaration(sema: *Sema, inst: Ir.Inst) !void {
const gpa = sema.gpa;
const ir = sema.ir;
const byte_index = sema.bytecode.items.len;
const const_index = sema.constants.items.len;
const extra = ir.extraData(Ir.Inst.Declaration, inst.data.payload.payload_index);
const value_inst = sema.resolveIndex(extra.data.value);
switch (value_inst.tag) {
.decl_var => try innerDecl(sema, value_inst),
.decl_knot => try innerDecl(sema, value_inst),
else => unreachable,
}
const name_ref = extra.data.name;
try sema.knots.append(gpa, .{
.name_ref = name_ref,
.arity = 0,
.stack_size = 0,
.bytecode = .{
.start = @intCast(byte_index),
.len = @intCast(sema.bytecode.items.len - byte_index),
},
.constants = .{
.start = @intCast(const_index),
.len = @intCast(sema.constants.items.len - const_index),
},
});
}
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) |inst_index| {
const body_inst = sema.resolveIndex(inst_index);
switch (body_inst.tag) {
.declaration => try declaration(sema, body_inst),
else => unreachable,
}
}
}
pub const CompiledStory = struct {
knots: []Knot,
constants: []Constant,
bytecode: []u8,
pub const Knot = struct {
name_ref: Ir.NullTerminatedString,
arity: u32,
stack_size: u32,
constants: struct {
start: u32,
len: u32,
},
bytecode: struct {
start: u32,
len: u32,
},
};
pub const Constant = union(enum) {
integer: u64,
string: Ir.NullTerminatedString,
};
pub fn deinit(self: *CompiledStory, gpa: std.mem.Allocator) void {
gpa.free(self.knots);
gpa.free(self.bytecode);
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_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);
},
}
}
const bytecode_slice = self.bytecodeSlice(&compiled_knot);
const chunk_name = ir.nullTerminatedString(compiled_knot.name_ref);
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),
});
try story.paths.append(gpa, &runtime_chunk.base);
}
}
};
pub fn compile(gpa: std.mem.Allocator, ir: *const Ir) !CompiledStory {
var sema: Sema = .{
.gpa = gpa,
.ir = ir,
};
defer sema.deinit();
try file(&sema, ir.instructions[0]);
return .{
.bytecode = try sema.bytecode.toOwnedSlice(gpa),
.constants = try sema.constants.toOwnedSlice(gpa),
.knots = try sema.knots.toOwnedSlice(gpa),
};
}