// 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

// 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_internal(bin, entry, trace, 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_internal(bin, entry, false, args, root)!
}

fn run_internal(bin obj.Bin, entry string, trace bool, args []string, root string) !i64 {
	mut v := Vm{
		code:       bin.code
		strings:    bin.strings.clone()
		stack:      []i64{len: stack_cap}
		trace:      trace
		prog_args:  args
		lines:      bin.lines
		fns:        bin.fns
		const_strs: bin.strings.len
		build_root: root
	}
	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
	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
}

// 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)
		}
		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())
				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_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
				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
}

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)!
}
