Files

789 lines
23 KiB
Plaintext

// Expect retcode = 0
// VM 字节码容器:内存中的 Prog 与 .sir 序列化(反序列化在 C 侧 spl_cli)。
// 字符串用 ByteView 借用、不拥有;gdata 条目为 ByteBuffer,由本模块拥有释放。
type .{ .Error, .Status, .make } = @import("utils/error.spl");
type .{ .Vec } = @import("utils/vec.spl");
type .{ .ByteView } = @import("utils/byte_view.spl");
type .{ .ByteBuffer } = @import("utils/byte_buffer.spl");
type .{ .append_usize, .append_i64 } = @import("utils/fmt.spl");
type Mem = @import("utils/memory.spl");
type .{ .open_cstr, .write_all, .read_all, .File } = @import("utils/io.spl");
type Op = enum {
Error,
Push,
Dup,
Drop,
Swap,
Pick,
Rot,
Add,
Sub,
Mul,
DivS,
DivU,
RemS,
RemU,
Neg,
And,
Or,
Xor,
Not,
Shl,
ShrU,
ShrS,
Eq,
Ne,
Slt,
Sle,
Ult,
Ule,
Sgt,
Sge,
Ugt,
Uge,
Trunc,
Sext,
Zext,
Jmp,
Bz,
Bnz,
Halt,
Call,
Calli,
Ret,
Alloc,
Laddr,
Gaddr,
Load,
Store,
Memcpy,
Ncall,
Nlib,
Bk,
Dbg,
}
type VmKind = enum {
Void,
Bool,
I8,
U8,
I16,
U16,
I32,
U32,
I64,
U64,
Isize,
Usize,
F32,
F64,
Ptr,
}
type VmIns = struct {
opcode: Op,
ty: VmKind,
imm: usize,
}
type VmFunc = struct {
name: ByteView,
idx_of_strtab: usize,
nargs: usize,
ninsns: usize,
address: usize,
}
type VmNative = struct {
name: ByteView,
idx_of_strtab: usize,
}
type Prog = @self;
insns: Vec, // VmIns
funcs: Vec, // VmFunc
natives: Vec, // VmNative
strtab: Vec, // ByteView
gdata: Vec, // ByteBuffer(索引 0 为占位项)
type ProgResult = enum {
Ok: Prog,
Err: Error,
}
fn op_name(op: Op): []u8 {
match op {
.Error => { ret "error"; },
.Push => { ret "push"; },
.Dup => { ret "dup"; },
.Drop => { ret "drop"; },
.Swap => { ret "swap"; },
.Pick => { ret "pick"; },
.Rot => { ret "rot"; },
.Add => { ret "add"; },
.Sub => { ret "sub"; },
.Mul => { ret "mul"; },
.DivS => { ret "div_s"; },
.DivU => { ret "div_u"; },
.RemS => { ret "rem_s"; },
.RemU => { ret "rem_u"; },
.Neg => { ret "neg"; },
.And => { ret "and"; },
.Or => { ret "or"; },
.Xor => { ret "xor"; },
.Not => { ret "not"; },
.Shl => { ret "shl"; },
.ShrU => { ret "shr_u"; },
.ShrS => { ret "shr_s"; },
.Eq => { ret "eq"; },
.Ne => { ret "ne"; },
.Slt => { ret "slt"; },
.Sle => { ret "sle"; },
.Ult => { ret "ult"; },
.Ule => { ret "ule"; },
.Sgt => { ret "sgt"; },
.Sge => { ret "sge"; },
.Ugt => { ret "ugt"; },
.Uge => { ret "uge"; },
.Trunc => { ret "trunc"; },
.Sext => { ret "sext"; },
.Zext => { ret "zext"; },
.Jmp => { ret "jmp"; },
.Bz => { ret "bz"; },
.Bnz => { ret "bnz"; },
.Halt => { ret "halt"; },
.Call => { ret "call"; },
.Calli => { ret "calli"; },
.Ret => { ret "ret"; },
.Alloc => { ret "alloc"; },
.Laddr => { ret "laddr"; },
.Gaddr => { ret "gaddr"; },
.Load => { ret "load"; },
.Store => { ret "store"; },
.Memcpy => { ret "memcpy"; },
.Ncall => { ret "ncall"; },
.Nlib => { ret "nlib"; },
.Bk => { ret "breakpoint"; },
.Dbg => { ret "dbg"; },
}
ret "error";
}
fn kind_name(k: VmKind): []u8 {
match k {
.Void => { ret "void"; },
.Bool => { ret "bool"; },
.I8 => { ret "i8"; },
.U8 => { ret "u8"; },
.I16 => { ret "i16"; },
.U16 => { ret "u16"; },
.I32 => { ret "i32"; },
.U32 => { ret "u32"; },
.I64 => { ret "i64"; },
.U64 => { ret "u64"; },
.Isize => { ret "isize"; },
.Usize => { ret "usize"; },
.F32 => { ret "f32"; },
.F64 => { ret "f64"; },
.Ptr => { ret "ptr"; },
}
ret "???";
}
fn new(): ProgResult {
var prog = Prog {
.insns = Vec.blank(@sizeof(VmIns), @alignof(VmIns)),
.funcs = Vec.blank(@sizeof(VmFunc), @alignof(VmFunc)),
.natives = Vec.blank(@sizeof(VmNative), @alignof(VmNative)),
.strtab = Vec.blank(@sizeof(ByteView), @alignof(ByteView)),
.gdata = Vec.blank(@sizeof(ByteBuffer), @alignof(ByteBuffer)),
};
// 索引 0 占位,使真实数据地址非零。
var placeholder = ByteBuffer.new();
match Vec.push(&prog.gdata, &placeholder as *u8) {
.Ok => {},
.Err(error) => {
Vec.drop(&prog.gdata);
Vec.drop(&prog.strtab);
Vec.drop(&prog.natives);
Vec.drop(&prog.funcs);
Vec.drop(&prog.insns);
ret .Err(error);
},
}
ret .Ok(prog);
}
fn gdata_at(self: *Prog, i: usize): *ByteBuffer {
match Vec.get(&self.gdata, i) {
.Ok(ptr) => { ret ptr as *ByteBuffer; },
.Err(error) => { ret null; },
}
ret null;
}
fn native_at(self: *Prog, i: usize): *VmNative {
match Vec.get(&self.natives, i) {
.Ok(ptr) => { ret ptr as *VmNative; },
.Err(error) => { ret null; },
}
ret null;
}
fn drop(self: *Prog) {
var i: usize = 0;
while i < self.gdata.len {
var e = gdata_at(self, i);
if e != null { ByteBuffer.drop(e); }
i = i + 1;
}
Vec.drop(&self.gdata);
Vec.drop(&self.strtab);
Vec.drop(&self.natives);
Vec.drop(&self.funcs);
Vec.drop(&self.insns);
}
fn add_instr(self: *Prog, op: Op, ty: VmKind, imm: usize): usize {
var i = self.insns.len;
if i < self.insns.cap {
var p = (self.insns.data as *VmIns) + i;
p.opcode = op;
p.ty = ty;
p.imm = imm;
self.insns.len = i + 1;
ret i + 1;
}
var ins = VmIns { .opcode = op, .ty = ty, .imm = imm };
if !Vec.push_raw(&self.insns, &ins as *u8) { ret 0; }
ret self.insns.len;
}
fn add_func(self: *Prog, name: ByteView, nargs: usize, address: usize): usize {
var f = VmFunc {
.name = name,
.idx_of_strtab = 0,
.nargs = nargs,
.ninsns = 0,
.address = address,
};
match Vec.push(&self.funcs, &f as *u8) {
.Ok => {},
.Err(error) => { ret 0; },
}
ret self.funcs.len;
}
// 复制 size 字节到新的 gdata 条目;返回其索引(0 表示失败)。
// ptr 是上层(Ast/Ast2Ir)拥有的稳定字节,调用期间保持有效。
fn add_data(self: *Prog, ptr: usize, size: usize): usize {
var entry = ByteBuffer.new();
match ByteBuffer.reserve(&entry, size) {
.Ok => {},
.Err(error) => { ByteBuffer.drop(&entry); ret 0; },
}
if size != 0 {
if ptr == 0 { ByteBuffer.drop(&entry); ret 0; }
Mem.copy(entry.storage.data as *any, ptr as *any, size);
entry.storage.len = size;
}
match Vec.push(&self.gdata, &entry as *u8) {
.Ok => {},
.Err(error) => { ByteBuffer.drop(&entry); ret 0; },
}
ret self.gdata.len - 1;
}
// 追加一条 size 字节的零初始化全局数据(BSS 语义);返回其索引。
fn add_zero_data(self: *Prog, size: usize): usize {
var entry = ByteBuffer.new();
match ByteBuffer.reserve(&entry, size) {
.Ok => {},
.Err(error) => { ByteBuffer.drop(&entry); ret 0; },
}
if size != 0 {
entry.storage.len = size;
var p = entry.storage.data as *u8;
var i: usize = 0;
while i < size { p[i] = 0; i = i + 1; }
}
match Vec.push(&self.gdata, &entry as *u8) {
.Ok => {},
.Err(error) => { ByteBuffer.drop(&entry); ret 0; },
}
ret self.gdata.len - 1;
}
// 返回 name 对应的 native 索引;不存在则登记。name 为空指针时返回 -1。
fn get_or_add_native(self: *Prog, name: ByteView): isize {
if name.ptr == null { ret -1; }
var i: usize = 0;
while i < self.natives.len {
var n = native_at(self, i);
if n != null && ByteView.eq(n.name, name) { ret i as isize; }
i = i + 1;
}
var nat = VmNative { .name = name, .idx_of_strtab = 0 };
match Vec.push(&self.natives, &nat as *u8) {
.Ok => {},
.Err(error) => { ret -1; },
}
ret (self.natives.len - 1) as isize;
}
fn out_s(out: *ByteBuffer, s: []u8): bool {
match ByteBuffer.append(out, s) { .Ok => { ret true; }, .Err(error) => { ret false; } }
ret false;
}
fn out_u(out: *ByteBuffer, v: usize): bool {
match append_usize(out, v, 10, false) { .Ok => { ret true; }, .Err(error) => { ret false; } }
ret false;
}
fn out_i(out: *ByteBuffer, v: i64): bool {
match append_i64(out, v, 10, false) { .Ok => { ret true; }, .Err(error) => { ret false; } }
ret false;
}
// %4zd:右对齐、最小宽度 4 的有符号十进制。
fn out_width4_i(out: *ByteBuffer, v: i64): bool {
var width: usize = 0;
if v < 0 {
if v >= -9 { width = 2; }
else if v >= -99 { width = 3; }
else { width = 4; }
} else {
if v < 10 { width = 1; }
else if v < 100 { width = 2; }
else if v < 1000 { width = 3; }
else { width = 4; }
}
var pad: usize = 0;
if width < 4 { pad = 4 - width; }
if !out_s(out, " "[0..pad]) { ret false; }
ret out_i(out, v);
}
fn dump(self: *Prog, out: *ByteBuffer): bool {
if !out_s(out, ";; SPL bytecode dump: \n") { ret false; }
if !out_s(out, ";; ninsns=") { ret false; }
if !out_u(out, self.insns.len) { ret false; }
if !out_s(out, " nfuncs=") { ret false; }
if !out_u(out, self.funcs.len) { ret false; }
if !out_s(out, " nnatives=") { ret false; }
if !out_u(out, self.natives.len) { ret false; }
if !out_s(out, " nstrs=") { ret false; }
if !out_u(out, self.strtab.len) { ret false; }
if !out_s(out, "\n\n") { ret false; }
if self.funcs.len > 0 {
if !out_s(out, ";; --- functions ---\n") { ret false; }
var i: usize = 0;
while i < self.funcs.len {
var f = func_at(self, i);
if f == null {
@assert(false, "func missing in dump");
ret false;
}
if !out_s(out, " ") { ret false; }
if !out_s(out, ByteView.as_slice(f.name)) { ret false; }
if !out_s(out, " nargs=") { ret false; }
if !out_u(out, f.nargs) { ret false; }
if !out_s(out, " ninsns=") { ret false; }
if !out_u(out, f.ninsns) { ret false; }
if !out_s(out, " addr=") { ret false; }
if !out_u(out, f.address) { ret false; }
if !out_s(out, "\n") { ret false; }
i = i + 1;
}
if !out_s(out, "\n") { ret false; }
}
if self.natives.len > 0 {
if !out_s(out, ";; --- natives ---\n") { ret false; }
var i: usize = 0;
while i < self.natives.len {
var n = native_at(self, i);
if n == null {
@assert(false, "native missing in dump");
ret false;
}
if !out_s(out, " ") { ret false; }
if !out_s(out, ByteView.as_slice(n.name)) { ret false; }
if !out_s(out, "\n") { ret false; }
i = i + 1;
}
if !out_s(out, "\n") { ret false; }
}
if self.strtab.len > 0 {
if !out_s(out, ";; --- string table ---\n") { ret false; }
var i: usize = 0;
while i < self.strtab.len {
var s = str_at(self, i);
if !out_s(out, " ") { ret false; }
if !out_i(out, i as i64) { ret false; }
if !out_s(out, ": \"") { ret false; }
if !out_s(out, ByteView.as_slice(s)) { ret false; }
if !out_s(out, "\"\n") { ret false; }
i = i + 1;
}
if !out_s(out, "\n") { ret false; }
}
if !out_s(out, ";; --- instructions ---\n") { ret false; }
var i: usize = 0;
while i < self.insns.len {
var ins = ins_at(self, i);
if !out_width4_i(out, i as i64) { ret false; }
if !out_s(out, ": ") { ret false; }
if !out_s(out, op_name(ins.opcode)) { ret false; }
if @enum2int(ins.ty) != @enum2int(VmKind.Void) {
if !out_s(out, " ") { ret false; }
if !out_s(out, kind_name(ins.ty)) { ret false; }
}
if !out_s(out, " ") { ret false; }
if !out_i(out, ins.imm as i64) { ret false; }
match ins.opcode {
.Jmp, .Bz, .Bnz => {
if !out_s(out, " ; -> ") { ret false; }
if !out_u(out, i + 1 + ins.imm) { ret false; }
},
.Call => {
if !out_s(out, " ; nargs=") { ret false; }
if !out_u(out, ins.imm) { ret false; }
if !out_s(out, ", from stack") { ret false; }
},
.Calli => { if !out_s(out, " ; indirect call") { ret false; } },
.Gaddr => {
if !out_s(out, " ; gdata[") { ret false; }
if !out_u(out, ins.imm) { ret false; }
if !out_s(out, "]") { ret false; }
},
_ => {},
}
if !out_s(out, "\n") { ret false; }
i = i + 1;
}
ret true;
}
fn ins_at(self: *Prog, i: usize): VmIns {
if i >= self.insns.len { ret VmIns { .opcode = .Error, .ty = .Void, .imm = 0 }; }
ret ((self.insns.data as *VmIns) + i).*;
}
fn func_at(self: *Prog, i: usize): *VmFunc {
match Vec.get(&self.funcs, i) {
.Ok(ptr) => { ret ptr as *VmFunc; },
.Err(error) => { ret null; },
}
ret null;
}
fn str_at(self: *Prog, i: usize): ByteView {
match Vec.get(&self.strtab, i) {
.Ok(ptr) => { ret (ptr as *ByteView).*; },
.Err(error) => { ret ByteView.empty(); },
}
ret ByteView.empty();
}
fn align8(n: usize): usize {
ret (n + 7) & (~(7 as usize));
}
// put_* 直接写入已预留容量的 ByteBuffer 存储(store 已按精确总大小 reserve),跳过
// append 的校验/拷贝;容量不足会越界,故 store 先校验容量。
fn put_u8(out: *ByteBuffer, b: u8): bool {
out.storage.data[out.storage.len] = b;
out.storage.len = out.storage.len + 1;
ret true;
}
// 直接按小端整型写(x86 小端;VM 的 STORE 用 memcpy,未对齐也安全)。
fn put_u16(out: *ByteBuffer, v: usize): bool {
var d = out.storage.data + out.storage.len;
(d as *u16).* = v as u16;
out.storage.len = out.storage.len + 2;
ret true;
}
fn put_u64(out: *ByteBuffer, v: usize): bool {
var d = out.storage.data + out.storage.len;
(d as *u64).* = v as u64;
out.storage.len = out.storage.len + 8;
ret true;
}
fn put_zeros(out: *ByteBuffer, n: usize): bool {
if n != 0 {
Mem.set((out.storage.data + out.storage.len) as *any, 0, n);
out.storage.len = out.storage.len + n;
}
ret true;
}
fn put_view(out: *ByteBuffer, v: ByteView): bool {
if v.len == 0 { ret true; }
Mem.copy((out.storage.data + out.storage.len) as *any, v.ptr as *any, v.len);
out.storage.len = out.storage.len + v.len;
ret true;
}
// 写入「带结尾 NUL、再按 8 对齐」的名称字段(对应 C 侧 nlen = strlen+1)。
fn put_name_field(out: *ByteBuffer, name: ByteView): bool {
var nlen = name.len + 1;
if !put_u64(out, nlen) { ret false; }
if !put_view(out, name) { ret false; }
if !put_u8(out, 0) { ret false; }
ret put_zeros(out, align8(nlen) - nlen);
}
// 把 prog 序列化为二进制写入 out(不落盘,供内嵌 VM 直接用)。
fn encode(self: *Prog, out: *ByteBuffer): Status {
// 精确预估输出大小并一次性预留,避免大文件逐字节增长导致的重复拷贝。
var total: usize = 48;
var fi: usize = 0;
var func_base = self.funcs.data as *VmFunc;
var nat_base = self.natives.data as *VmNative;
var str_base = self.strtab.data as *ByteView;
var gd_base = self.gdata.data as *ByteBuffer;
while fi < self.funcs.len {
var f = func_base + fi;
var nlen = f.name.len + 1;
total = total + 8 + align8(nlen) + 32;
fi = fi + 1;
}
total = total + self.insns.len * 12;
fi = 0;
while fi < self.natives.len {
var n = nat_base + fi;
var nlen = n.name.len + 1;
total = total + 8 + align8(nlen) + 8;
fi = fi + 1;
}
fi = 0;
while fi < self.strtab.len {
var s = str_base + fi;
total = total + 8 + align8(s.len);
fi = fi + 1;
}
fi = 1;
while fi < self.gdata.len {
var e = gd_base + fi;
total = total + 8 + align8(e.storage.len);
fi = fi + 1;
}
ByteBuffer.reserve(out, total);
if out.storage.cap < total || out.storage.data == null { ret .Err(make(.Io)); }
if !put_view(out, ByteView.from_slice("SPLBIN")) { ret .Err(make(.Io)); }
if !put_zeros(out, 2) { ret .Err(make(.Io)); }
var nfuncs = self.funcs.len;
var ninsns = self.insns.len;
var nnatives = self.natives.len;
var nstrs = self.strtab.len;
var ndata: usize = 0;
if self.gdata.len > 0 { ndata = self.gdata.len - 1; }
if !put_u64(out, nfuncs) { ret .Err(make(.Io)); }
if !put_u64(out, ninsns) { ret .Err(make(.Io)); }
if !put_u64(out, nnatives) { ret .Err(make(.Io)); }
if !put_u64(out, nstrs) { ret .Err(make(.Io)); }
if !put_u64(out, ndata) { ret .Err(make(.Io)); }
var i: usize = 0;
while i < nfuncs {
var f = func_base + i;
if !put_name_field(out, f.name) { ret .Err(make(.Io)); }
if !put_u64(out, f.idx_of_strtab) { ret .Err(make(.Io)); }
if !put_u64(out, f.nargs) { ret .Err(make(.Io)); }
if !put_u64(out, f.ninsns) { ret .Err(make(.Io)); }
if !put_u64(out, f.address) { ret .Err(make(.Io)); }
i = i + 1;
}
i = 0;
var ins_base = self.insns.data as *VmIns;
var out_base = out.storage.data;
var out_len = out.storage.len;
while i < ninsns {
var p = ins_base + i;
var d = out_base + out_len;
(d as *u16).* = @enum2int(p.opcode) as u16;
((d + 2) as *u16).* = @enum2int(p.ty) as u16;
((d + 4) as *u64).* = p.imm as u64;
out_len = out_len + 12;
i = i + 1;
}
out.storage.len = out_len;
i = 0;
while i < nnatives {
var n = nat_base + i;
if !put_name_field(out, n.name) { ret .Err(make(.Io)); }
if !put_u64(out, n.idx_of_strtab) { ret .Err(make(.Io)); }
i = i + 1;
}
i = 0;
while i < nstrs {
var s = str_base + i;
if !put_u64(out, s.len) { ret .Err(make(.Io)); }
if !put_view(out, s.*) { ret .Err(make(.Io)); }
if !put_zeros(out, align8(s.len) - s.len) { ret .Err(make(.Io)); }
i = i + 1;
}
i = 1;
while i < self.gdata.len {
var e = gd_base + i;
var dsize = e.storage.len;
if !put_u64(out, dsize) { ret .Err(make(.Io)); }
if !put_view(out, ByteView.from_slice(ByteBuffer.as_slice(e.*))) { ret .Err(make(.Io)); }
if !put_zeros(out, align8(dsize) - dsize) { ret .Err(make(.Io)); }
i = i + 1;
}
ret .Ok;
}
fn store(self: *Prog, path: ByteView): Status {
var out = ByteBuffer.new();
defer { ByteBuffer.drop(&out); }
match encode(self, &out) {
.Ok => {},
.Err(error) => { ret .Err(error); },
}
var f = open_cstr(path, ByteView.from_slice("wb"));
if !File.valid(&f) { ret .Err(make(.Io)); }
var w = write_all(&f, ByteBuffer.as_slice(out));
var closed = File.close(&f);
match w {
.Ok => {},
.Err(error) => { ret .Err(error); },
}
if closed != 0 { ret .Err(make(.Io)); }
ret .Ok;
}
// 序列化到内存缓冲(供内嵌 VM 直接装载)。
fn serialize(self: *Prog, out: *ByteBuffer): Status {
ret encode(self, out);
}
fn test_dump_basic() {
var pr: Prog = undefined;
match new() {
.Ok(v) => { pr = v; },
.Err(error) => { @assert(false, "prog init"); ret; },
}
defer { drop(&pr); }
add_instr(&pr, .Halt, .Void, 0);
var out = ByteBuffer.new();
defer { ByteBuffer.drop(&out); }
if !dump(&pr, &out) { @assert(false, "dump"); ret; }
var expected: [69]u8 = .[
';', ';', ' ', 'S', 'P', 'L', ' ', 'b',
'y', 't', 'e', 'c', 'o', 'd', 'e', ' ',
'd', 'u', 'm', 'p', ':', ' ', 10,
';', ';', ' ', 'n', 'i', 'n', 's', 'n',
's', '=', '1', ' ', 'n', 'f', 'u', 'n',
'c', 's', '=', '0', ' ', 'n', 'n', 'a',
't', 'i', 'v', 'e', 's', '=', '0', ' ',
'n', 's', 't', 'r', 's', '=', '0', 10, 10,
';', ';', ' ', '-', '-', '-',
];
var got = ByteBuffer.as_slice(out);
@assert(got.len > expected.len, "dump longer than header prefix");
var i: usize = 0;
while i < expected.len {
@assert(got.ptr[i] == expected[i], "dump header bytes");
i = i + 1;
}
}
fn test_native_dedup() {
var pr: Prog = undefined;
match new() {
.Ok(v) => { pr = v; },
.Err(error) => { @assert(false, "prog init"); ret; },
}
defer { drop(&pr); }
var vm = ByteView.from_slice("vm_printf");
@assert(get_or_add_native(&pr, vm) == 0, "first native idx");
@assert(get_or_add_native(&pr, vm) == 0, "dedup native idx");
@assert(pr.natives.len == 1, "native count");
}
fn test_store_roundtrip() {
var pr: Prog = undefined;
match new() {
.Ok(v) => { pr = v; },
.Err(error) => { @assert(false, "prog init"); ret; },
}
defer { drop(&pr); }
add_func(&pr, ByteView.from_slice("main"), 0, 0);
add_instr(&pr, .Halt, .Void, 0);
var path = ByteView.from_slice("build/_mcode_roundtrip.tmp");
match store(&pr, path) {
.Ok => {},
.Err(error) => { @assert(false, "store to file"); ret; },
}
var data = ByteBuffer.new();
defer { ByteBuffer.drop(&data); }
match read_all(path, &data) {
.Ok => {},
.Err(error) => { @assert(false, "read back"); ret; },
}
var b = ByteBuffer.as_slice(data);
// header(48) + 一个函数项(8 + 8 + 4*8 = 48) + 一条指令(12) = 108
@assert(b.len == 108, "serialized size");
@assert(b.ptr[0] == 'S' && b.ptr[1] == 'P' && b.ptr[2] == 'L' && b.ptr[3] == 'B' &&
b.ptr[4] == 'I' && b.ptr[5] == 'N' && b.ptr[6] == 0 && b.ptr[7] == 0,
"magic");
@assert(b.ptr[8] == 1, "nfuncs");
@assert(b.ptr[16] == 1, "ninsns");
@assert(b.ptr[24] == 0 && b.ptr[32] == 0 && b.ptr[40] == 0, "nnatives/nstrs/ndata");
// 函数项起始:name_len(5) + "main\0" + pad(3)
@assert(b.ptr[48] == 5, "name_len");
@assert(b.ptr[56] == 'm' && b.ptr[57] == 'a' && b.ptr[58] == 'i' && b.ptr[59] == 'n' &&
b.ptr[60] == 0, "name bytes");
@assert(b.ptr[61] == 0 && b.ptr[62] == 0 && b.ptr[63] == 0, "name padding");
// 指令:opcode(2)=halt(38) type(2)=void(0) imm(8)=0
var ins_off: usize = 96;
@assert(b.ptr[ins_off + 0] == 38 && b.ptr[ins_off + 1] == 0, "opcode bytes");
@assert(b.ptr[ins_off + 2] == 0 && b.ptr[ins_off + 3] == 0, "type bytes");
@assert(b.ptr[ins_off + 4] == 0 && b.ptr[ins_off + 11] == 0, "imm bytes");
}
fn main() {
test_dump_basic();
test_native_dedup();
test_store_roundtrip();
}