ink/src/Story/Dumper.zig
2026-03-29 06:21:53 -06:00

383 lines
13 KiB
Zig

const std = @import("std");
const Story = @import("../Story.zig");
const Value = Story.Value;
const Object = Story.Object;
const Opcode = Story.Opcode;
const assert = std.debug.assert;
const Dumper = @This();
story: *const Story,
indent_level: usize = 0,
fn dumpSimpleInst(_: *Dumper, w: *std.Io.Writer, offset: usize, op: Opcode) !usize {
try w.print("{s}\n", .{@tagName(op)});
return offset + 1;
}
fn dumpByteInst(
self: *Dumper,
w: *std.Io.Writer,
knot: *const Object.Knot,
offset: usize,
op: Opcode,
) !usize {
const code = knot.code;
const constants_pool = &self.story.constants_pool;
assert(code.bytecode.len > offset + 1);
const arg = code.bytecode[offset + 1];
if (op == .load_const) {
try w.print("{s} {d}", .{ @tagName(op), arg });
try w.writeAll(" (");
if (code.constants.len > arg) {
const constant_index = code.constants[arg];
if (constants_pool.items.len > constant_index) {
const global_constant = &constants_pool.items[constant_index];
try self.dumpValue(w, global_constant);
} else {
try w.writeAll("invalid!");
}
} else {
try w.writeAll("invalid!");
}
try w.writeAll(")\n");
} else {
try w.print("{s} {x}\n", .{ @tagName(op), arg });
}
return offset + 2;
}
fn dumpGlobalInst(
self: *Dumper,
w: *std.Io.Writer,
knot: *const Object.Knot,
offset: usize,
op: Opcode,
) !usize {
const code = knot.code;
const constants_pool = &self.story.constants_pool;
assert(code.bytecode.len > offset + 1);
const arg = code.bytecode[offset + 1];
assert(code.constants.len > arg);
const constant_index = code.constants[arg];
const global_constant = constants_pool.items[constant_index];
switch (global_constant) {
.object => |object| switch (object.tag) {
.string => {
const global_name: *Object.String = @ptrCast(object);
const name_bytes = global_name.bytes[0..global_name.length];
try w.print("{s} {x} '{s}'\n", .{ @tagName(op), arg, name_bytes });
},
else => unreachable,
},
else => unreachable,
}
return offset + 2;
}
const Jump = enum {
relative,
absolute,
};
fn dumpJumpInst(
_: *Dumper,
w: *std.Io.Writer,
knot: *const Object.Knot,
offset: usize,
op: Opcode,
mode: Jump,
) !usize {
const code = knot.code;
assert(code.bytecode.len > offset + 2);
var jump: u16 = @as(u16, code.bytecode[offset + 1]) << 8;
jump |= code.bytecode[offset + 2];
switch (mode) {
.relative => try w.print("{s} 0x{x:0>4} (0x{x:0>4} -> 0x{x:0>4})\n", .{
@tagName(op),
jump,
offset,
offset + 3 + jump,
}),
.absolute => try w.print("{s} 0x{x:0>4}\n", .{
@tagName(op),
jump,
}),
}
return offset + 3;
}
pub fn dumpInst(
self: *Dumper,
w: *std.Io.Writer,
knot: *const Object.Knot,
offset: usize,
should_prefix: bool,
) !usize {
const name_object = knot.name;
const name_bytes = name_object.bytes[0..name_object.length];
const op: Opcode = @enumFromInt(knot.code.bytecode[offset]);
try w.splatByteAll(' ', self.indent_level);
if (should_prefix) {
try w.print("<{s}>:0x{x:0>4} | ", .{ name_bytes, offset });
} else {
try w.print("0x{x:0>4} | ", .{offset});
}
switch (op) {
.exit => return self.dumpSimpleInst(w, offset, op),
.done => return self.dumpSimpleInst(w, offset, op),
.ret => return self.dumpSimpleInst(w, offset, op),
.pop => return self.dumpSimpleInst(w, offset, op),
.true => return self.dumpSimpleInst(w, offset, op),
.false => return self.dumpSimpleInst(w, offset, op),
.add => return self.dumpSimpleInst(w, offset, op),
.sub => return self.dumpSimpleInst(w, offset, op),
.mul => return self.dumpSimpleInst(w, offset, op),
.div => return self.dumpSimpleInst(w, offset, op),
.mod => return self.dumpSimpleInst(w, offset, op),
.neg => return self.dumpSimpleInst(w, offset, op),
.not => return self.dumpSimpleInst(w, offset, op),
.cmp_eq => return self.dumpSimpleInst(w, offset, op),
.cmp_lt => return self.dumpSimpleInst(w, offset, op),
.cmp_lte => return self.dumpSimpleInst(w, offset, op),
.cmp_gt => return self.dumpSimpleInst(w, offset, op),
.cmp_gte => return self.dumpSimpleInst(w, offset, op),
.load_const => return self.dumpByteInst(w, knot, offset, op),
.load => return self.dumpByteInst(w, knot, offset, op),
.store => return self.dumpByteInst(w, knot, offset, op),
.load_global => return self.dumpGlobalInst(w, knot, offset, op),
.store_global => return self.dumpGlobalInst(w, knot, offset, op),
.call => return self.dumpByteInst(w, knot, offset, op),
.divert => return self.dumpByteInst(w, knot, offset, op),
.jmp => return self.dumpJumpInst(w, knot, offset, op, .relative),
.jmp_t => return self.dumpJumpInst(w, knot, offset, op, .relative),
.jmp_f => return self.dumpJumpInst(w, knot, offset, op, .relative),
.load_attr => return self.dumpGlobalInst(w, knot, offset, op),
.store_attr => return self.dumpGlobalInst(w, knot, offset, op),
.stream_push => return self.dumpSimpleInst(w, offset, op),
.stream_flush => return self.dumpSimpleInst(w, offset, op),
.stream_line => return self.dumpSimpleInst(w, offset, op),
.stream_glue => return self.dumpSimpleInst(w, offset, op),
.br_push => return self.dumpJumpInst(w, knot, offset, op, .absolute),
.br_table => return self.dumpSimpleInst(w, offset, op),
.br_dispatch => return self.dumpSimpleInst(w, offset, op),
.br_select_index => return self.dumpSimpleInst(w, offset, op),
else => |code| {
try w.print("Unknown opcode 0x{x:0>4}\n", .{@intFromEnum(code)});
return offset + 1;
},
}
}
fn dumpKnotConstants(w: *std.Io.Writer, knot: *const Object.Knot) !void {
try w.writeAll("[");
for (knot.code.constants) |index| {
try w.print("{d},", .{index});
}
try w.writeAll("]");
try w.flush();
}
fn dumpKnotChildren(w: *std.Io.Writer, knot: *const Object.Knot) !void {
try w.writeAll("[");
var stitch_iter = knot.members.iterator();
while (stitch_iter.next()) |entry| {
try w.print("Stitch: \"{s}\",", .{entry.key_ptr.*});
}
try w.writeAll("]");
try w.flush();
}
fn dumpKnotBytecode(self: *Dumper, w: *std.Io.Writer, knot: *const Object.Knot) !void {
var index: usize = 0;
while (index < knot.code.bytecode.len) {
index = try self.dumpInst(w, knot, index, false);
try w.flush();
}
return w.flush();
}
pub fn dumpKnot(self: *Dumper, w: *std.Io.Writer, knot: *const Object.Knot) !void {
const name_object = knot.name;
const name_bytes = name_object.bytes[0..name_object.length];
try w.splatByteAll(' ', self.indent_level);
try w.print("Name: \"{s}\"\n", .{name_bytes});
self.indent_level += 1;
try w.splatByteAll(' ', self.indent_level);
try w.print("Arguments: {d}\n", .{knot.code.args_count});
try w.splatByteAll(' ', self.indent_level);
try w.print("Locals: {d}\n", .{knot.code.locals_count});
try w.splatByteAll(' ', self.indent_level);
try w.print("Stack Size: {d}\n", .{knot.code.stack_size});
try w.splatByteAll(' ', self.indent_level);
try w.writeAll("Constants: ");
try dumpKnotConstants(w, knot);
try w.writeAll("\n");
try w.splatByteAll(' ', self.indent_level);
try w.writeAll("Children: ");
try dumpKnotChildren(w, knot);
try w.writeAll("\n");
try w.splatByteAll(' ', self.indent_level);
try w.writeAll("Bytecode: \n");
self.indent_level += 2;
try self.dumpKnotBytecode(w, knot);
try w.writeAll("\n");
self.indent_level -= 2;
var stitch_iter = knot.members.iterator();
var count: usize = 0;
while (stitch_iter.next()) |entry| : (count += 1) {
const key = entry.key_ptr.*;
const stitch: *Object.Knot = @ptrCast(entry.value_ptr.*);
try w.splatByteAll(' ', self.indent_level);
try w.print("Stitch #{d}: \"{s}\"\n", .{ count, key });
self.indent_level += 2;
try w.splatByteAll(' ', self.indent_level);
try w.print("Arguments: {d}\n", .{stitch.code.args_count});
try w.splatByteAll(' ', self.indent_level);
try w.print("Locals: {d}\n", .{stitch.code.locals_count});
try w.splatByteAll(' ', self.indent_level);
try w.print("Stack Size: {d}\n", .{stitch.code.stack_size});
try w.splatByteAll(' ', self.indent_level);
try w.writeAll("Constants: ");
try dumpKnotConstants(w, stitch);
try w.writeAll("\n");
try w.splatByteAll(' ', self.indent_level);
try w.writeAll("Children: ");
try dumpKnotChildren(w, stitch);
try w.writeAll("\n");
try w.splatByteAll(' ', self.indent_level);
try w.writeAll("Bytecode: \n");
self.indent_level += 2;
try self.dumpKnotBytecode(w, stitch);
try w.writeAll("\n");
self.indent_level -= 4;
try w.flush();
}
self.indent_level = 2;
return w.flush();
}
pub fn dumpValue(_: Dumper, w: *std.Io.Writer, value: *const Value) !void {
switch (value.*) {
.bool => |_| try w.print(
"<{s} value={f}>",
.{ value.tagBytes(), value },
),
.int => |_| try w.print(
"<{s} value={f}, address={*}>",
.{ value.tagBytes(), value, value },
),
.float => |_| try w.print(
"<{s} value={f}, address={*}>",
.{ value.tagBytes(), value, value },
),
.object => |object| switch (object.tag) {
.string => {
const typed_object: *const Object.String = @ptrCast(object);
const string_bytes = typed_object.bytes[0..typed_object.length];
try w.print("<{s} value=\"{s}\", address={*}>", .{
object.tag.tagBytes(),
string_bytes,
object,
});
},
else => try w.print("<{s} address={*}>", .{ object.tag.tagBytes(), object }),
},
}
}
pub fn dump(story: *Story, writer: *std.Io.Writer) !void {
var story_dumper: Dumper = .{ .story = story };
try writer.writeAll("=== Constants ===\n");
var i: usize = 0;
while (i < story.constants_pool.items.len) : (i += 1) {
const global_constant = &story.constants_pool.items[i];
try story_dumper.dumpValue(writer, 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");
try writer.flush();
var knots_iter = story.globals.iterator();
while (knots_iter.next()) |entry| {
if (entry.value_ptr.*) |global| {
switch (global) {
.object => |object| switch (object.tag) {
.knot => {
try writer.writeAll("*");
story_dumper.indent_level += 2;
try story_dumper.dumpKnot(writer, @ptrCast(object));
story_dumper.indent_level -= 2;
},
else => {},
},
else => {},
}
}
}
}
pub fn trace(story: *Story, writer: *std.Io.Writer, frame: *Story.CallFrame) !void {
var dumper: Dumper = .{ .story = story };
const stack = &story.stack;
const stack_top = story.stack.items.len;
try writer.print("\tStack => stack_pointer={d}, objects=[", .{frame.sp});
if (stack_top > 0) {
// FIXME: There has to be a better way to do this.
if (stack_top > 1) {
var i: usize = frame.sp;
while (i < stack.items.len - 1) : (i += 1) {
if (stack.items[i]) |*value| {
try dumper.dumpValue(writer, value);
} else {
try writer.writeAll("null");
}
try writer.writeAll(", ");
}
}
if (stack.items[stack.items.len - 1]) |*object| {
try dumper.dumpValue(writer, object);
} else {
try writer.writeAll("null");
}
}
try writer.writeAll("]\n");
_ = try dumper.dumpInst(writer, frame.callee, frame.ip, true);
return writer.flush();
}