ink/src/Ir.zig
2026-03-18 19:21:56 -06:00

316 lines
7.7 KiB
Zig

const std = @import("std");
const Ast = @import("Ast.zig");
const Writer = @import("print_ir.zig").Writer;
const assert = std.debug.assert;
const Ir = @This();
/// List of IR instructions for the translation unit.
instructions: []Inst,
/// Interned string bytes. All strings are null-terminated.
/// Index 0 is reserved for the empty string.
string_bytes: []u8,
/// Ancillary data for instructions. The meaning of this data is determined by
/// the value of `Inst.Tag`. See `ExtraIndex` for the values of reserved indexes.
extra: []u32,
globals: []Global,
pub const ExtraIndex = enum(u32) {
/// If this is 0, no compile errors. Otherwise there is a `CompileErrors`
/// payload at this index.
compile_errors,
_,
};
fn ExtraData(comptime T: type) type {
return struct {
data: T,
end: usize,
};
}
/// Extract a slice of `extra` into an `Inst` payload structure.
pub fn extraData(ir: Ir, comptime T: type, index: usize) ExtraData(T) {
const fields = @typeInfo(T).@"struct".fields;
var i: usize = index;
var result: T = undefined;
inline for (fields) |field| {
@field(result, field.name) = switch (field.type) {
u32 => ir.extra[i],
Inst.Index => @enumFromInt(ir.extra[i]),
Inst.Ref => @enumFromInt(ir.extra[i]),
NullTerminatedString => @enumFromInt(ir.extra[i]),
else => @compileError("bad field type"),
};
i += 1;
}
return .{ .data = result, .end = i };
}
pub const NullTerminatedString = enum(u32) {
empty,
_,
};
pub fn nullTerminatedString(ir: Ir, index: NullTerminatedString) [:0]const u8 {
const slice = ir.string_bytes[@intFromEnum(index)..];
return slice[0..std.mem.indexOfScalar(u8, slice, 0).? :0];
}
pub fn bodySlice(ir: Ir, start: usize, len: usize) []Inst.Index {
return @ptrCast(ir.extra[start..][0..len]);
}
pub fn hasCompileErrors(ir: Ir) bool {
if (ir.extra[@intFromEnum(ExtraIndex.compile_errors)] != 0) {
return true;
} else {
assert(ir.instructions.len != 0);
return false;
}
}
pub fn render(ir: Ir, writer: *std.Io.Writer) !void {
if (ir.instructions.len > 0) {
var w: Writer = .{ .code = ir };
try w.writeInst(writer, .file_inst);
return writer.flush();
}
}
pub fn dumpInfo(ir: Ir, writer: *std.Io.Writer) !void {
const bytes = ir.string_bytes;
var start: usize = 0;
while (start < bytes.len) {
const end = std.mem.indexOfScalarPos(u8, bytes, start, 0) orelse break;
const str = bytes[start..end];
try writer.print("[{d:04}] ", .{start});
for (str) |b| try writer.print("{x:02} ", .{b});
try writer.print("00: {s}\n", .{str});
start = end + 1;
}
for (ir.globals) |global| {
try writer.print("{any}\n", .{global});
}
return writer.flush();
}
pub fn deinit(ir: *Ir, gpa: std.mem.Allocator) void {
gpa.free(ir.string_bytes);
gpa.free(ir.instructions);
gpa.free(ir.globals);
gpa.free(ir.extra);
ir.* = undefined;
}
pub const Global = struct {
tag: Tag,
name: Ir.NullTerminatedString,
is_constant: bool,
pub const Tag = enum {
knot,
variable,
};
};
pub const Inst = struct {
tag: Tag,
data: Data,
/// An index to an IR instruction. Some values are reserved.
pub const Index = enum(u32) {
file_inst,
ref_start_index = 32,
_,
pub fn toRef(i: Index) Inst.Ref {
return @enumFromInt(@intFromEnum(Index.ref_start_index) + @intFromEnum(i));
}
};
/// A reference to an IR instruction, or to an statically interned value,
/// or neither.
///
/// If the integer tag value is < `Index.ref_start_index`, then it
/// corresponds to an interned value. Otherwise, this refers to a IR
/// instruction.
///
/// The tag type is specified so that it is safe to bitcast between `[]u32`
/// and `[]Ref`.
pub const Ref = enum(u32) {
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 {
file,
declaration,
decl_knot,
decl_var,
/// Uses the `str_tok` union field.
decl_ref,
alloc,
load,
store,
/// Uses the `bin` union field.
add,
/// Uses the `bin` union field.
sub,
/// Uses the `bin` union field.
mul,
/// Uses the `bin` union field.
div,
/// Uses the `bin` union field.
mod,
/// Uses the `un` union field.
neg,
/// Uses the `un` union field.
not,
cmp_eq,
cmp_neq,
cmp_gt,
cmp_gte,
cmp_lt,
cmp_lte,
/// Uses the `int` union field.
int,
/// Uses the `str` union field.
str,
block,
condbr,
@"break",
switch_br,
content_push,
content_flush,
choice_br,
implicit_ret,
call,
divert,
field_ptr,
field_call,
field_divert,
param,
};
pub const Data = union {
payload: struct {
payload_index: u32,
},
un: struct {
lhs: Ref,
},
bin: struct {
lhs: Ref,
rhs: Ref,
},
int: u64,
str: struct {
/// Offset into `string_bytes`.
start: NullTerminatedString,
/// Number of bytes in the string.
len: u32,
pub fn get(self: @This(), code: Ir) []const u8 {
return code.string_bytes[@intFromEnum(self.start)..][0..self.len];
}
},
str_tok: struct {
/// Offset into `string_bytes`.
start: NullTerminatedString,
src_offset: u32,
},
};
pub const Declaration = struct {
name: NullTerminatedString,
value: Index,
};
pub const Knot = struct {
body_len: u32,
};
pub const Var = struct {
body_len: u32,
};
pub const Field = struct {
lhs: Ref,
field_name_start: NullTerminatedString,
};
pub const Block = struct {
body_len: u32,
};
pub const Break = struct {
block_inst: Index,
};
pub const CondBr = struct {
condition: Ref,
then_body_len: u32,
else_body_len: u32,
};
pub const SwitchBr = struct {
operand: Ref,
cases_len: u32,
else_body_len: u32,
pub const Case = struct {
operand: Ref,
body_len: u32,
};
};
pub const ChoiceBr = struct {
cases_len: u32,
pub const Case = struct {
operand_1: Ref,
operand_2: Ref,
operand_3: Ref,
body_len: u32,
};
};
pub const Call = struct {
args_len: u32,
callee: Ref,
};
pub const FieldCall = struct {
args_len: u32,
obj_ptr: Ref,
field_name_start: NullTerminatedString,
};
pub const CompileErrors = struct {
items_len: u32,
pub const Item = struct {
msg: NullTerminatedString,
byte_offset: u32,
};
};
};