Files
bear/vm/vm.v
T
2026-08-25 18:03:03 +02:00

1067 lines
27 KiB
V

// vm.v — the VuurRaaf runtime: a small stack-based virtual machine.
//
// Call convention: CALL pushes a frame (retaddr, old bp, argc) and copies the
// arguments into the callee's local slots; the callee reserves extra locals
// with `enter n` and cleans up with `ret`/`retv`.
module vm
import obj
import math
// RunOpts configures a VM run: tracing, program arguments, the build root,
// the interactive debugger, an instruction budget, and profiling.
pub struct RunOpts {
pub:
trace bool
args []string = []
root string
debug bool // start the interactive debugger (vr debug)
breakpoints []int // source lines to stop at; empty + debug = stop at entry
max_ops i64 // instruction budget; 0 = unlimited
profile bool // count instructions/calls per function
}
// run executes the function named `entry` from the executable `bin` and
// returns its return value (0 if it never returns one).
pub fn run(bin obj.Bin, entry string, trace bool) !i64 {
return run_with_args(bin, entry, trace, []string{})
}
// run_with_args is run() with command-line arguments exposed to the program
// via the `args()` builtin.
pub fn run_with_args(bin obj.Bin, entry string, trace bool, args []string) !i64 {
return run_opts(bin, entry, RunOpts{ trace: trace, args: args })!
}
// run_build executes a .vrmm build module: the entry target receives the
// extra CLI arguments via `args()`, and `build_root()` reports the module's
// own directory so scripts can find files regardless of the working directory.
pub fn run_build(bin obj.Bin, entry string, args []string, root string) !i64 {
return run_opts(bin, entry, RunOpts{ args: args, root: root })!
}
// run_opts runs the program with full control over the runtime options.
pub fn run_opts(bin obj.Bin, entry string, opts RunOpts) !i64 {
mut v := new_vm(bin, entry, opts)!
return v.run_result()!
}
// run_debug runs the program under the interactive debugger, stopping at the
// given source-line breakpoints (or at entry when none are given).
pub fn run_debug(bin obj.Bin, entry string, breakpoints []int, args []string) !i64 {
return run_opts(bin, entry, RunOpts{ args: args, debug: true, breakpoints: breakpoints })!
}
// ProfileRow is one function's profile totals.
pub struct ProfileRow {
pub:
name string
calls u64
instr u64
}
// ProfileReport is the result of a profiled run: per-function instruction
// and call counts, sorted by instructions executed (hot first).
pub struct ProfileReport {
pub:
rows []ProfileRow
total u64 // instructions executed across all functions
}
// run_profiled executes the program counting instructions and calls per
// function, and returns the report.
pub fn run_profiled(bin obj.Bin, entry string, args []string) !ProfileReport {
mut v := new_vm(bin, entry, RunOpts{ args: args, profile: true })!
_ = v.run_result()!
mut rows := []ProfileRow{}
for i in 0..v.fns.len {
rows << ProfileRow{ name: v.fns[i].name, calls: v.prof_calls[i], instr: v.prof_instr[i] }
}
rows.sort_with_compare(fn (a &ProfileRow, b &ProfileRow) int {
if a.instr > b.instr {
return -1
}
if a.instr < b.instr {
return 1
}
return 0
})
mut total := u64(0)
for r in rows {
total += r.instr
}
return ProfileReport{ rows: rows, total: total }
}
// new_vm builds a configured Vm for the entry function, pushing the synthetic
// entry frame and pointing ip at the entry point.
fn new_vm(bin obj.Bin, entry string, opts RunOpts) !Vm {
mut v := Vm{
code: bin.code
strings: bin.strings.clone()
stack: []i64{len: stack_cap}
trace: opts.trace
prog_args: opts.args
lines: bin.lines
fns: bin.fns
const_strs: bin.strings.len
bin: bin
build_root: opts.root
dbg_locals: bin.locals
max_ops: opts.max_ops
}
if opts.profile {
v.profiling = true
v.prof_instr = []u64{len: v.fns.len}
v.prof_calls = []u64{len: v.fns.len}
v.fn_of_ip = v.build_fn_of_ip()
}
if opts.debug {
v.dbg.enabled = true
v.dbg.breakpoints = opts.breakpoints.clone()
v.dbg.mode = if opts.breakpoints.len > 0 { DbgMode.run } else { DbgMode.step }
}
mut entry_ip := -1
for f in bin.fns {
if f.name == entry {
entry_ip = f.entry
break
}
}
if entry_ip < 0 {
names := bin.fns.map(fn (f obj.BinFn) string {
return f.name
})
return error('no function "${entry}" in program (available: ${names.join(', ')})')
}
// synthetic frame: retaddr = -1 (halt sentinel), old bp = 0, argc = 0
v.stack[v.sp] = v.enc_int(-1)
v.sp++
v.stack[v.sp] = v.enc_int(0)
v.sp++
v.stack[v.sp] = v.enc_int(0)
v.sp++
v.bp = v.sp
v.ip = entry_ip
return v
}
// run_result executes until halt/error and extracts the program's result.
fn (mut v Vm) run_result() !i64 {
v.exec() or {
return error('${err.msg()} at ${v.where()}\n${v.stack_trace()}')
}
if v.did_exit {
return v.exit_code
}
if v.sp > 0 {
return v.dec_int(v.stack[0])
}
return 0
}
// build_fn_of_ip precomputes, for every code offset, the index of the
// function that contains it, so profiling adds one array lookup per opcode.
// Function tables are not guaranteed to be in entry order (the linker builds
// them from a map), so entries are sorted by offset first.
fn (v Vm) build_fn_of_ip() []int {
mut out := []int{len: v.code.len}
mut fes := []FnEntry{}
for i, f in v.fns {
fes << FnEntry{ idx: i, entry: f.entry }
}
fes.sort_with_compare(fn (a &FnEntry, b &FnEntry) int {
return a.entry - b.entry
})
mut fi := 0
for ip in 0..v.code.len {
for fi + 1 < fes.len && fes[fi + 1].entry <= ip {
fi++
}
out[ip] = fes[fi].idx
}
return out
}
// where returns a source-level location for the current instruction pointer:
// `line 12 (ip 345)` when debug info is available, otherwise just `(ip 345)`.
fn (v Vm) where() string {
return 'line ${v.line_at(v.ip)} (ip ${v.ip})'
}
// line_at maps a code offset to its source line via the line table.
fn (v Vm) line_at(ip int) int {
// line table entries are recorded in code order, so walk backwards from
// the most recent entry to find the last one at or before ip
for i := v.lines.len - 1; i >= 0; i-- {
if ip >= int(v.lines[i].off) {
return v.lines[i].line
}
}
return 0
}
// func_at returns the name of the function whose body contains the given
// code offset. Functions are laid out sequentially, so the enclosing
// function is the one with the greatest entry point <= ip.
fn (v Vm) func_at(ip int) string {
mut name := '?'
for f in v.fns {
if f.entry <= ip {
name = f.name
}
}
return name
}
// stack_trace renders the call chain at the moment an error is raised, from
// the innermost frame out to main. Each frame's return address and saved bp
// live in the frame header pushed by `call`: [retaddr, old_bp, argc] at
// bp-3..bp-1. The synthetic entry frame has retaddr == -1 (the halt sentinel).
fn (mut v Vm) stack_trace() string {
mut out := []string{}
mut bp := v.bp
mut ip := v.ip
mut guard := 0
// note: sp may have dropped below bp (an error handler pops values), so
// the frame chain is bounded by the guard and the old_bp < bp invariant
for bp >= 3 && guard < 10000 {
out << ' at ${v.func_at(ip)} (line ${v.line_at(ip)})'
ret := v.dec_int(v.stack[bp - 3])
old_bp := int(v.dec_int(v.stack[bp - 2]))
if ret == -1 || old_bp < 0 || old_bp >= bp {
break // reached the synthetic entry frame
}
ip = int(ret)
bp = old_bp
guard++
}
return out.join('\n')
}
fn (mut v Vm) exec() ! {
for !v.halted {
// garbage collection: runs between opcodes when the heap has grown by
// gc_alloc_trigger entries since the last collection, so no live value
// is ever mid-flight in an instruction handler
heap := v.strings.len + v.arrays.len + v.structs.len + v.floats.len + v.closures.len
if heap > v.last_heap + gc_alloc_trigger {
v.collect()
v.last_heap = v.strings.len + v.arrays.len + v.structs.len + v.floats.len + v.closures.len
} else {
v.last_heap = heap
}
op := v.code[v.ip]
if v.trace {
v.trace_op(op)
}
if v.profiling {
v.prof_instr[v.fn_of_ip[v.ip]]++
}
if v.max_ops > 0 {
v.ops++
if v.ops > v.max_ops {
return error('max ops exceeded (${v.max_ops}) — possible infinite loop')
}
}
if v.dbg.enabled {
v.dbg_tick()!
}
match op {
op_halt {
v.halted = true
}
op_push_i {
v.ip++
v.push(v.enc_int(v.read_i64()))!
}
op_push_s {
v.ip++
idx := int(v.read_i64())
v.push(v.mkstr(idx))!
}
op_push_f {
v.ip++
f := v.read_f64()
v.push(v.push_float(f))!
}
op_load {
v.ip++
idx := int(v.read_i64())
v.push(v.stack[v.bp + idx])!
}
op_store {
v.ip++
idx := int(v.read_i64())
v.stack[v.bp + idx] = v.pop()!
}
op_pop {
v.ip++
v.pop()!
}
op_dup {
v.ip++
a := v.pop()!
v.push(a)!
v.push(a)!
}
op_add {
v.ip++
b := v.pop()!
a := v.pop()!
v.push(v.add(a, b)!)!
}
op_sub {
v.ip++
b := v.pop()!
a := v.pop()!
v.push(v.arith(a, b, '-')!)!
}
op_mul {
v.ip++
b := v.pop()!
a := v.pop()!
v.push(v.arith(a, b, '*')!)!
}
op_div {
v.ip++
b := v.pop()!
a := v.pop()!
v.push(v.arith(a, b, '/')!)!
}
op_mod {
v.ip++
b := v.pop()!
a := v.pop()!
v.push(v.arith(a, b, '%')!)!
}
op_neg {
v.ip++
a := v.pop()!
if v.is_float(a) {
v.push(v.push_float(-v.fval(a)))!
} else if v.is_str(a) {
return error('cannot negate a string')
} else if v.is_arr(a) {
return error('cannot negate an array')
} else if v.is_struct(a) {
return error('cannot negate a struct')
} else {
v.push(v.enc_int(-v.dec_int(a)))!
}
}
op_eq {
v.ip++
b := v.pop()!
a := v.pop()!
v.push(v.enc_int(v.cmp(a, b, '==')!))!
}
op_ne {
v.ip++
b := v.pop()!
a := v.pop()!
v.push(v.enc_int(v.cmp(a, b, '!=')!))!
}
op_lt {
v.ip++
b := v.pop()!
a := v.pop()!
v.push(v.enc_int(v.cmp(a, b, '<')!))!
}
op_le {
v.ip++
b := v.pop()!
a := v.pop()!
v.push(v.enc_int(v.cmp(a, b, '<=')!))!
}
op_gt {
v.ip++
b := v.pop()!
a := v.pop()!
v.push(v.enc_int(v.cmp(a, b, '>')!))!
}
op_ge {
v.ip++
b := v.pop()!
a := v.pop()!
v.push(v.enc_int(v.cmp(a, b, '>=')!))!
}
op_and {
v.ip++
b := v.pop()!
a := v.pop()!
v.push(v.enc_int(bool_i64(v.truthy(a) && v.truthy(b))))!
}
op_or {
v.ip++
b := v.pop()!
a := v.pop()!
v.push(v.enc_int(bool_i64(v.truthy(a) || v.truthy(b))))!
}
op_not {
v.ip++
a := v.pop()!
v.push(v.enc_int(bool_i64(!v.truthy(a))))!
}
op_jmp {
v.ip++
// jump targets are PC-relative (delta from the end of the
// operand), so merged/linked bytecode stays position-independent
v.ip += int(v.read_i64())
}
op_jz {
v.ip++
target := int(v.read_i64())
if !v.truthy(v.pop()!) {
v.ip += target
}
}
op_jnz {
v.ip++
target := int(v.read_i64())
if v.truthy(v.pop()!) {
v.ip += target
}
}
op_call {
v.ip++
target := int(v.read_i64())
argc := int(v.read_i64())
if v.profiling {
v.prof_calls[v.fn_of_ip[target]]++
}
v.call(target, argc)
}
op_ret {
v.ret(false)!
}
op_retv {
v.ret(true)!
}
op_print {
v.ip++
v.print_val(v.pop()!)
}
op_println {
v.ip++
v.print_val(v.pop()!)
println('')
}
op_assert {
v.ip++
if !v.truthy(v.pop()!) {
return error('assertion failed (ip ${v.ip})')
}
}
op_enter {
v.ip++
n := int(v.read_i64())
for _ in 0..n {
v.push(0)!
}
}
op_mkarray {
v.ip++
n := int(v.read_i64())
mut arr := []i64{len: n}
for i := n - 1; i >= 0; i-- {
arr[i] = v.pop()!
}
v.arrays << arr
v.push(v.mkarr(v.arrays.len - 1))!
}
op_aget {
v.ip++
idxv := v.pop()!
h := v.pop()!
// dynamic map read: m[key_expr] where the base is a struct and the
// index evaluates to a string
if v.is_struct(h) && v.valid_struct_handle(h) && v.is_str(idxv) && v.valid_handle(idxv) {
fname := v.strings[v.hand(idxv)]
s := v.structs[v.hand(h)]
idx, ok := v.field_idx(s, fname)
if ok {
v.push(s.fields[idx].val)!
} else {
return error('no field "${fname}" on struct')
}
} else {
idx := int(v.dec_int(idxv))
if v.is_arr(h) && v.valid_arr_handle(h) {
a := v.arrays[v.hand(h)]
if idx < 0 || idx >= a.len {
return error('array index ${idx} out of bounds (len ${a.len})')
}
v.push(a[idx])!
} else if v.is_str(h) && v.valid_handle(h) {
// rune-based string indexing: s[i] is the i-th character
runes := v.strings[v.hand(h)].runes()
if idx < 0 || idx >= runes.len {
return error('string index ${idx} out of bounds (len ${runes.len})')
}
v.push(v.alloc_str(runes[idx].str()))!
} else {
return error('indexing a non-array, non-string value')
}
}
}
op_aset {
v.ip++
val := v.pop()!
idxv := v.pop()!
h := v.pop()!
// dynamic map write: m[key_expr] = v (string key on a struct)
if v.is_struct(h) && v.valid_struct_handle(h) && v.is_str(idxv) && v.valid_handle(idxv) {
fname := v.strings[v.hand(idxv)]
mut s := v.structs[v.hand(h)]
idx, ok := v.field_idx(s, fname)
if ok {
s.fields[idx].val = val
} else {
// setting a missing field adds it, so maps can grow
s.fields << Field{ name: fname, val: val }
s.by_name[fname] = s.fields.len - 1
}
v.structs[v.hand(h)] = s
} else {
idx := int(v.dec_int(idxv))
if !v.is_arr(h) || !v.valid_arr_handle(h) {
return error('indexing a non-array value')
}
if idx < 0 || idx >= v.arrays[v.hand(h)].len {
return error('array index ${idx} out of bounds (len ${v.arrays[v.hand(h)].len})')
}
v.arrays[v.hand(h)][idx] = val
}
}
op_alen {
v.ip++
h := v.pop()!
if v.is_arr(h) && v.valid_arr_handle(h) {
v.push(v.enc_int(i64(v.arrays[v.hand(h)].len)))!
} else if v.is_struct(h) && v.valid_struct_handle(h) {
v.push(v.enc_int(i64(v.structs[v.hand(h)].fields.len)))!
} else if v.is_str(h) && v.valid_handle(h) {
v.push(v.enc_int(i64(v.strings[v.hand(h)].runes().len)))!
} else {
return error('len() on a non-array, non-struct, non-string value')
}
}
op_apush {
v.ip++
val := v.pop()!
h := v.pop()!
if !v.is_arr(h) || !v.valid_arr_handle(h) {
return error('push() on a non-array value')
}
v.arrays[v.hand(h)] << val
v.push(h)!
}
op_mkstruct {
v.ip++
n := int(v.read_i64())
mut fields := []Field{len: n}
// stack holds (name, value) pairs; pop from the last field back
for i := n - 1; i >= 0; i-- {
val := v.pop()!
name := v.pop()!
if !v.is_str(name) || !v.valid_handle(name) {
return error('internal: struct field name is not a string')
}
fields[i] = Field{ name: v.strings[v.hand(name)], val: val }
}
v.structs << StructVal{ fields: fields, by_name: v.index_fields(fields) }
v.push(v.mkstruct_handle(v.structs.len - 1))!
}
op_sget {
v.ip++
name := v.pop()!
h := v.pop()!
if !v.is_struct(h) || !v.valid_struct_handle(h) {
return error('field access on a non-struct value')
}
if !v.is_str(name) || !v.valid_handle(name) {
return error('internal: field name is not a string')
}
fname := v.strings[v.hand(name)]
s := v.structs[v.hand(h)]
idx, ok := v.field_idx(s, fname)
if ok {
v.push(s.fields[idx].val)!
} else {
return error('no field "${fname}" on struct')
}
}
op_sset {
v.ip++
// stack: [struct, value, "name"] — the name is on top
name := v.pop()!
val := v.pop()!
h := v.pop()!
if !v.is_struct(h) || !v.valid_struct_handle(h) {
return error('field assignment on a non-struct value')
}
if !v.is_str(name) || !v.valid_handle(name) {
return error('internal: field name is not a string')
}
fname := v.strings[v.hand(name)]
mut s := v.structs[v.hand(h)]
idx, ok := v.field_idx(s, fname)
if ok {
s.fields[idx].val = val
} else {
// setting a missing field adds it, so records can be built
// incrementally from an empty `{}`
s.fields << Field{ name: fname, val: val }
s.by_name[fname] = s.fields.len - 1
}
v.structs[v.hand(h)] = s
}
op_shas {
v.op_shas()!
}
op_sdel {
v.op_sdel()!
}
op_slen {
v.op_slen()!
}
op_skeys {
v.op_skeys()!
}
op_slice {
v.op_slice()!
}
op_in {
v.ip++
col := v.pop()!
needle := v.pop()!
// map/struct: string key membership
if v.is_struct(col) && v.valid_struct_handle(col) {
if v.is_str(needle) && v.valid_handle(needle) {
key := v.strings[v.hand(needle)]
v.push(v.enc_int(if key in v.structs[v.hand(col)].by_name { 1 } else { 0 }))!
} else {
v.push(v.enc_int(0))!
}
} else if v.is_arr(col) && v.valid_arr_handle(col) {
found := v.arr_contains(v.arrays[v.hand(col)], needle)
v.push(v.enc_int(if found { 1 } else { 0 }))!
} else if v.is_str(col) && v.valid_handle(col) && v.is_str(needle) && v.valid_handle(needle) {
v.push(v.enc_int(if v.strings[v.hand(col)].contains(v.strings[v.hand(needle)]) { 1 } else { 0 }))!
} else {
v.push(v.enc_int(0))!
}
}
op_native {
v.ip++
id := int(v.read_i64())
argc := int(v.read_i64())
v.native(id, argc) or {
// a failed builtin becomes a VM-level throw, so try/catch can
// intercept it exactly like an explicit `throw`; with no
// handler it keeps propagating to the caller
if v.handlers.len == 0 {
return err
}
h := v.handlers[v.handlers.len - 1]
v.handlers.delete_last()
v.bp = h.bp
v.sp = h.sp
v.push(v.alloc_str(err.msg()))!
v.ip = h.ip
}
}
op_and_b {
v.ip++
b := v.pop()!
a := v.pop()!
v.push(v.enc_int(v.dec_int(a) & v.dec_int(b)))!
}
op_or_b {
v.ip++
b := v.pop()!
a := v.pop()!
v.push(v.enc_int(v.dec_int(a) | v.dec_int(b)))!
}
op_xor {
v.ip++
b := v.pop()!
a := v.pop()!
v.push(v.enc_int(v.dec_int(a) ^ v.dec_int(b)))!
}
op_shl {
v.ip++
b := v.pop()!
a := v.pop()!
x := v.dec_int(a)
y := u32(v.dec_int(b))
v.push(v.enc_int(x << y))!
}
op_shr {
v.ip++
b := v.pop()!
a := v.pop()!
x := v.dec_int(a)
y := u32(v.dec_int(b))
v.push(v.enc_int(x >> y))!
}
op_not_b {
v.ip++
a := v.pop()!
v.push(v.enc_int(~v.dec_int(a)))!
}
op_try {
v.ip++
catch_ip := int(v.read_i64()) + v.ip
v.handlers << Handler{ ip: catch_ip, bp: v.bp, sp: v.sp }
}
op_throw {
v.ip++
err_val := v.pop()!
if v.handlers.len == 0 {
return error('unhandled throw: ${v.val_str(err_val, 0)}')
}
h := v.handlers[v.handlers.len - 1]
v.handlers.delete_last()
v.bp = h.bp
v.sp = h.sp
v.push(err_val)!
v.ip = h.ip
}
op_catch_done {
v.ip++
if v.handlers.len > 0 {
v.handlers.delete_last()
}
}
op_closure {
v.ip++
entry := int(v.read_i64())
n := int(v.read_i64())
// the captured values were pushed by the compiler in capture
// order; pop them back into the closure's own array
mut captured := []i64{len: n}
for i := n - 1; i >= 0; i-- {
captured[i] = v.pop()!
}
v.closures << Closure{ entry: entry, captured: captured }
v.push(v.mkclosure(v.closures.len - 1))!
}
op_call_closure {
v.ip++
argc := int(v.read_i64())
// stack: [...closure, arg_0, ..., arg_{argc-1}]
h := v.stack[v.sp - argc - 1]
if !v.is_closure(h) || !v.valid_closure_handle(h) {
return error('cannot call a non-function value')
}
cl := v.closures[v.hand(h)]
n := cl.captured.len
c := v.sp - argc - 1 // closure slot
// Rearrange the stack from [...closure, arg_0..arg_{argc-1}] to
// [capture_0..capture_{n-1}, arg_0..arg_{argc-1}]: the captures
// take over the closure slot, and the args shift by (1 - n) so
// the callee sees captures as its leading locals followed by the
// real arguments. v.call cleans the whole region up on ret.
if n > 1 {
// shifting right: copy backwards to avoid clobbering
for i := argc - 1; i >= 0; i-- {
v.stack[c + n + i] = v.stack[c + 1 + i]
}
} else {
// shifting left (or no shift): copy forwards
for i := 0; i < argc; i++ {
v.stack[c + n + i] = v.stack[c + 1 + i]
}
}
for i in 0..n {
v.stack[c + i] = cl.captured[i]
}
v.sp = c + n + argc
if v.profiling {
v.prof_calls[v.fn_of_ip[cl.entry]]++
}
v.call(cl.entry, argc + n)
}
op_argc {
v.ip++
argc := v.dec_int(v.stack[v.bp - 1])
v.push(v.enc_int(argc))!
}
op_load_dyn {
v.ip++
idx := int(v.dec_int(v.pop()!))
if v.bp + idx < 0 || v.bp + idx >= v.sp {
return error('dynamic load index ${idx} out of range')
}
v.push(v.stack[v.bp + idx])!
}
op_varargs {
v.ip++
named := int(v.read_i64())
dst := int(v.read_i64())
argc := int(v.dec_int(v.stack[v.bp - 1]))
mut n := argc - named
if n < 0 {
n = 0
}
mut arr := []i64{len: n}
for i in 0..n {
arr[i] = v.stack[v.bp + named + i]
}
v.arrays << arr
v.stack[v.bp + dst] = v.mkarr(v.arrays.len - 1)
}
op_str_method {
v.ip++
sidx := int(v.read_i64())
argc := int(v.read_i64())
v.str_method(v.strings[sidx], argc)!
}
op_push_none {
v.ip++
v.push(none_val)!
}
else {
return error('unknown opcode ${op} at ip ${v.ip}')
}
}
}
}
// field_idx returns the index of a named field via the hash index, or
// (0, false) when the field does not exist.
fn (v Vm) field_idx(s StructVal, fname string) (int, bool) {
if fname in s.by_name {
return s.by_name[fname], true
}
return 0, false
}
// index_fields builds the name -> index hash map for a freshly built field
// list (used by mkstruct and other struct constructors).
fn (v Vm) index_fields(fields []Field) map[string]int {
mut m := map[string]int{}
for i, f in fields {
m[f.name] = i
}
return m
}
// op_shas checks if a struct has a field with the given name.
// stack: struct, "key" → pushes 1 if found, 0 if not.
fn (mut v Vm) op_shas() ! {
v.ip++
name := v.pop()!
h := v.pop()!
if !v.is_struct(h) || !v.valid_struct_handle(h) {
return error('has() on a non-struct value')
}
if !v.is_str(name) || !v.valid_handle(name) {
return error('internal: field name is not a string')
}
fname := v.strings[v.hand(name)]
found := fname in v.structs[v.hand(h)].by_name
v.push(v.enc_int(if found { 1 } else { 0 }))!
}
// op_sdel removes a field from a struct.
// stack: struct, "key" → pushes the struct handle back.
fn (mut v Vm) op_sdel() ! {
v.ip++
name := v.pop()!
h := v.pop()!
if !v.is_struct(h) || !v.valid_struct_handle(h) {
return error('delete() on a non-struct value')
}
if !v.is_str(name) || !v.valid_handle(name) {
return error('internal: field name is not a string')
} fname := v.strings[v.hand(name)]
mut s := v.structs[v.hand(h)]
idx, ok := v.field_idx(s, fname)
if ok {
mut new_fields := []Field{}
for i, f in s.fields {
if i != idx {
new_fields << f
}
}
s.fields = new_fields
s.by_name = v.index_fields(new_fields)
v.structs[v.hand(h)] = s
}
v.push(h)!
}
// op_slen returns the number of fields in a struct.
// stack: struct → pushes field count.
fn (mut v Vm) op_slen() ! {
v.ip++
h := v.pop()!
if !v.is_struct(h) || !v.valid_struct_handle(h) {
return error('len() on a non-struct value')
}
v.push(v.enc_int(i64(v.structs[v.hand(h)].fields.len)))!
}
// op_skeys returns an array of field name strings.
// stack: struct → pushes array handle.
fn (mut v Vm) op_skeys() ! {
v.ip++
h := v.pop()!
if !v.is_struct(h) || !v.valid_struct_handle(h) {
return error('keys() on a non-struct value')
}
mut arr := []i64{}
for f in v.structs[v.hand(h)].fields {
v.strings << f.name
arr << v.mkstr(v.strings.len - 1)
}
v.arrays << arr
v.push(v.mkarr(v.arrays.len - 1))!
}
// op_slice slices an array or string: stack = [value, start, end] → sliced value.
// end == -1 means "open-ended" (slice to the end).
fn (mut v Vm) op_slice() ! {
v.ip++
end_val := v.dec_int(v.pop()!)
start_val := v.dec_int(v.pop()!)
h := v.pop()!
// --- array slicing ---
if v.is_arr(h) && v.valid_arr_handle(h) {
arr := v.arrays[v.hand(h)]
mut s := if start_val < 0 { 0 } else { int(start_val) }
mut e := if end_val < 0 { arr.len } else { int(end_val) }
if s > arr.len {
s = arr.len
}
if e > arr.len {
e = arr.len
}
if s > e {
e = s
}
mut sliced := []i64{}
for i in s..e {
sliced << arr[i]
}
v.arrays << sliced
v.push(v.mkarr(v.arrays.len - 1))!
return
}
// --- string slicing ---
if v.is_str(h) && v.valid_handle(h) {
src := v.strings[v.hand(h)]
runes := src.runes()
mut s := if start_val < 0 { 0 } else { int(start_val) }
mut e := if end_val < 0 { runes.len } else { int(end_val) }
if s > runes.len {
s = runes.len
}
if e > runes.len {
e = runes.len
}
if s > e {
e = s
}
mut sliced := ''
for i in s..e {
sliced += runes[i].str()
}
v.push(v.alloc_str(sliced))!
return
}
return error('slice() on a non-array, non-string value')
}
fn (mut v Vm) read_i64() i64 {
mut val := u64(0)
for i in 0..8 {
val |= u64(v.code[v.ip + i]) << u32(8 * i)
}
v.ip += 8
return i64(val)
}
fn (mut v Vm) read_f64() f64 {
mut val := u64(0)
for i in 0..8 {
val |= u64(v.code[v.ip + i]) << u32(8 * i)
}
v.ip += 8
return math.f64_from_bits(val)
}
fn (mut v Vm) push(x i64) ! {
if v.sp >= v.stack.len {
return error('stack overflow')
}
v.stack[v.sp] = x
v.sp++
}
fn (mut v Vm) pop() !i64 {
if v.sp <= 0 {
return error('stack underflow')
}
v.sp--
return v.stack[v.sp]
}
fn (mut v Vm) call(target int, argc int) {
v.stack[v.sp] = v.enc_int(i64(v.ip)) // return address (ip already past both operands)
v.sp++
v.stack[v.sp] = v.enc_int(i64(v.bp))
v.sp++
v.stack[v.sp] = v.enc_int(i64(argc))
v.sp++
v.bp = v.sp
// copy the arguments below the frame into local slots 0..argc-1
for i in 0..argc {
v.stack[v.bp + i] = v.stack[v.bp - 3 - argc + i]
}
v.sp = v.bp + argc
v.ip = target
}
// arr_contains reports whether an array holds a value equal to needle
// (comparing ints/floats numerically and strings by content).
fn (mut v Vm) arr_contains(a []i64, needle i64) bool {
for el in a {
if v.vals_eq(el, needle) {
return true
}
}
return false
}
// vals_eq compares two values for equality, treating int/float numerically.
fn (mut v Vm) vals_eq(x i64, y i64) bool {
if (v.is_float(x) || v.is_float(y)) && !v.is_closure(x) && !v.is_closure(y) && !v.is_str(x) && !v.is_str(y) && !v.is_arr(x) && !v.is_arr(y) && !v.is_struct(x) && !v.is_struct(y) {
return v.to_f64(x) == v.to_f64(y)
}
return x == y
}
fn (mut v Vm) ret(with_val bool) ! {
retval := if with_val { v.pop()! } else { v.enc_int(0) }
v.sp = v.bp - 1
argc := int(v.dec_int(v.stack[v.sp]))
v.sp = v.bp - 2
old_bp := int(v.dec_int(v.stack[v.sp]))
v.sp = v.bp - 3
ip := int(v.dec_int(v.stack[v.sp]))
v.sp -= argc
v.bp = old_bp
if ip == -1 {
// returned to the synthetic frame: we are done
v.halted = true
v.push(retval)!
return
}
v.ip = ip
v.push(retval)!
}