Skip to main content

VM

Struct VM 

Source
pub struct VM {
    pub stack: Vec<Value>,
    pub frames: Vec<Frame>,
    pub globals: Vec<Value>,
    pub ip: usize,
    pub chunk: Chunk,
    pub last_status: i32,
    pub host: Option<Box<dyn ShellHost>>,
    pub awk_host: Option<Box<dyn AwkHost>>,
    /* private fields */
}
Expand description

The virtual machine.

Fields§

§stack: Vec<Value>

Value stack

§frames: Vec<Frame>

Call frame stack

§globals: Vec<Value>

Global variables (name pool index → value)

§ip: usize

Instruction pointer

§chunk: Chunk

Current chunk being executed

§last_status: i32

Last exit status ($?)

§host: Option<Box<dyn ShellHost>>

Frontend-supplied shell host (glob/expand/redirect/pipeline/etc). When None, shell ops fall back to minimal stub behavior.

§awk_host: Option<Box<dyn AwkHost>>

Frontend-supplied AWK host (fields/record/print/getline/string builtins). The VM routes the reserved AWK op range (Op::ExtendedWide with id >= awk_builtins::AWK_OP_BASE) here. When None, AWK ops are inert stubs and the universal ops still execute normally.

Implementations§

Source§

impl VM

Source

pub fn new(chunk: Chunk) -> Self

Construct a fresh VM bound to the given chunk. Allocates the per-name slot vector, seeds the call-frame stack with a root frame, and zeros every per-thread counter (cycle / deopt / trace stats). The chunk’s op_hash is preserved verbatim so subsequent JIT-cache lookups can short-circuit recompilation.

Source

pub fn set_sited_numeric_hook(&mut self, hook: SitedNumericHook)

Install a NumericHook, switching this VM to strict numeric mode: arithmetic that cannot be computed exactly in i64/f64 — a non-numeric operand, or integer overflow — is handed to hook instead of being coerced or wrapped.

Strict mode also constrains the JIT, which otherwise coerces and wraps in native code exactly like the default interpreter: the block tier is skipped whenever a live slot holds a non-numeric value, and JIT-compiled integer Add/Sub/Mul are emitted with overflow checks that bail back to the interpreter (where the hook runs). Native code compiled in strict mode is cached separately from the coercing kind, so the two never mix. Install a SitedNumericHook — a numeric hook that is also told which chunk and op index the arithmetic came from.

Puts the VM in strict numeric mode exactly as VM::set_numeric_hook does, and takes precedence over one installed there.

Source

pub fn set_numeric_hook(&mut self, hook: NumericHook)

Source

pub fn slot_names_at(&self, up: usize) -> &[String]

Install an UndefHook, switching the VM to strict-undef mode: a read of a variable that was never assigned asks the host rather than pushing Value::Undef.

Off by default, so a VM without one is byte-for-byte unchanged. The mode also arms the JIT’s existing non-numeric gates — see UndefHook for why a native tier cannot observe an Undef in the first place. The slot names of the frame up levels out from the running one — 0 is the current frame, 1 its caller — or an empty slice when that frame entered no subroutine, when nothing was recorded for it, or when there is no such frame.

This is the question an eval whose script must run in the calling frame’s variable context asks: what are this frame’s variables called. The names come from Chunk::sub_slot_names and the values from the frame, so two activations of a recursive subroutine answer with the same names over their own slots.

Source

pub fn slot_of_at(&self, up: usize, name: &str) -> Option<u16>

The slot index name has in the frame up levels out, or None when that frame has no slot by that name.

This is the question upvar-style aliasing asks: which slot of that frame is this name. The answer indexes Frame::slots of the same frame, so a caller pairs it with VM::slot_names_at’s up.

Source

pub fn frame_slot_names(&self) -> &[String]

The running frame’s slot names — VM::slot_names_at at level 0.

Source

pub fn frame_slot_of(&self, name: &str) -> Option<u16>

The slot name has in the running frame — VM::slot_of_at at level 0.

Source

pub fn set_undef_hook(&mut self, hook: UndefHook)

Source

pub fn is_strict_undef(&self) -> bool

Whether an UndefHook is installed (strict-undef mode).

Source

pub fn set_output_sink(&mut self, sink: OutputSink)

Install an OutputSink so Op::Print/Op::PrintLn route through sink instead of std::io::stdout(). Frontends running fusevm in a browser web worker use this to capture output and bridge it to the JS host (wasm has no real stdout). With no sink installed, output is byte-for-byte identical to the previous direct-stdout behaviour.

Source

pub fn set_input_source(&mut self, source: InputSource)

Install an InputSource so Op::ReadLine pulls from source instead of std::io::stdin(). The closure returns one line per call (newline trimmed) or None at end of input (pushed as Value::Undef).

Source

pub fn is_strict_numeric(&self) -> bool

Whether a NumericHook is installed (strict numeric mode).

Source

pub fn set_fixnum_range(&mut self, lo: i64, hi: i64)

Narrow the range the VM keeps as a native Value::Int (strict mode only).

A Lisp whose integers are tagged has fewer than 64 bits for a fixnum — Emacs gets 62, so most-positive-fixnum is 2^61-1 — and an arithmetic result outside that range is a bignum, even though it still fits an i64. Setting the range makes strict mode delegate those results to the NumericHook alongside true i64 overflow, so the host can widen them. JIT-compiled code carries the same bounds check (two ALU ops folded into the overflow accumulator — still no branch on the hot path).

Without this, the host would see only i64 overflow and integers in the 2^61..2^63 band would masquerade as fixnums.

Source

pub fn reset(&mut self, chunk: Chunk)

Reset the VM for re-use with a new chunk, preserving internal Vec allocations to avoid the construction cost of VM::new.

State that’s cleared:

  • Value stack (truncated, capacity preserved)
  • Frame stack (rebuilt with one entry pointing at the new chunk)
  • Globals (resized to match the new chunk’s name pool)
  • Instruction pointer, halted flag, exit status
  • Tracing JIT recorder / slot buffers / deopt info
  • Cached block-JIT eligibility (the new chunk has a different hash)

State that’s preserved:

  • Tracing JIT enabled flag
  • Extension handlers (ext_handler, ext_wide_handler)
  • Builtin table
  • Shell host

This pairs with VMPool for hot-path callers that run many chunks back-to-back and want to skip the per-call allocation cost of VM::new.

Source

pub fn set_shell_host(&mut self, host: Box<dyn ShellHost>)

Register the frontend shell host. Replaces any prior host.

Source

pub fn set_awk_host(&mut self, host: Box<dyn AwkHost>)

Register the frontend AWK host. Replaces any prior host. The VM then routes the reserved AWK op range to it (see crate::awk_host::AwkHost).

Source

pub fn awk_signal(&self) -> Option<u8>

AWK control-flow signal raised by the most recent run(), if any. An awk frontend reads this after run() returns to map Op::AwkSignal codes (awk_builtins::signal::{NEXT,NEXTFILE,EXIT}) onto its own record/file/exit control flow. None when no signal was raised (always the case for zshrs/stryke, which never emit Op::AwkSignal).

Source

pub fn take_sched(&mut self) -> Option<SchedReq>

Take the pending cooperative-concurrency scheduling request, if any. The crate::sched::Scheduler driver calls this after run() returns: Some means a goroutine/channel op halted the VM to request scheduling; None means the VM finished (or halted for another reason). Clears the slot.

Source

pub fn clear_halt(&mut self)

Clear the halt flag so a parked goroutine VM resumes on the next run() (which continues from the current ip, past the op that parked it). Used by crate::sched::Scheduler; a plain schedulerless run() never needs it.

Source

pub fn set_extension_handler(&mut self, handler: ExtensionHandler)

Register a handler for Op::Extended(id, arg) opcodes.

Source

pub fn set_extension_wide_handler(&mut self, handler: ExtensionWideHandler)

Register a handler for Op::ExtendedWide(id, payload) opcodes.

Source

pub fn register_builtin(&mut self, id: u16, handler: BuiltinHandler)

Register a builtin function by ID. CallBuiltin(id, argc) dispatches directly through the function pointer — no name lookup at runtime.

Source

pub fn run_builtin_by_name( &mut self, name: &str, args: &[String], ) -> Option<Value>

Run a shell builtin by name at run time — the runtime analog of the compile-time Op::CallBuiltin opcode. The compiler resolves a literal command name to a builtin id and emits CallBuiltin, so builtins only dispatch for names known at compile time. A host that resolves a name only at run time — builtin NAME, $var command indirection, eval of a computed name — needs this to reach the same builtins.

Resolves name via crate::shell_builtins::builtin_id, pushes args as string values in argument order (identical to what the compiler emits ahead of CallBuiltin, which the handler’s arg-pop reverses back), then invokes the registered handler and returns its status value. Returns None when name is not a known builtin or has no registered handler, so the caller falls through to function / external lookup.

Source

pub fn request_halt(&mut self)

Externally request the VM to halt after the current op finishes. Used by host-side shell semantics like set -e post-command checks and exit from inside builtins to stop dispatch at a safe point.

Source

pub fn push(&mut self, val: Value)

Push val onto the value stack. Inlined for hot-path callers (extension handlers, builtin shims) that bypass the dispatch loop’s own push.

Source

pub fn pop(&mut self) -> Value

Pop the top of the value stack, returning Value::Undef if the stack is empty. Returning Undef rather than panicking matches Perl’s “underflow is undef” semantic and lets extension/builtin handlers stay panic-free under malformed bytecode.

Source

pub fn peek(&self) -> &Value

Borrow the top of the value stack without popping. Returns a reference to Value::Undef when the stack is empty.

Source

pub fn run(&mut self) -> VMResult

Execute the loaded chunk until completion or error.

Phase 10: tiered auto-dispatch. When tracing_jit is enabled the VM consults all three Cranelift tiers in priority order:

  1. Block JIT — if the entire chunk is block-eligible, the block-JIT cache returns Some(result) after its own warmup threshold and the whole chunk runs in native code with zero interpreter dispatch.
  2. Tracing JIT — when block JIT doesn’t apply, the dispatch loop runs with the recorder armed at backward branches; hot loops compile to traces that take over subsequent iterations.
  3. Interpreter — fallback for cold code and chunks neither tier handles.

Block JIT is tried first because, when it applies, it has zero VM-side overhead (direct fn-ptr through the slot pointer). For chunks block JIT can’t take, control falls through to the interpreter with tracing JIT integrated. The two tiers don’t compete on the same chunk: block-eligible chunks short-circuit before tracing JIT records anything.

Source

pub fn get_slot(&self, slot: u16) -> Value

Read a slot from the current (top) call frame.

Returns Value::Undef when there is no active frame or the slot index is out of range. Public so frontend extension handlers (set_extension_handler) can read slot operands without reaching into frames directly.

Source

pub fn set_slot(&mut self, slot: u16, val: Value)

Write a slot in the current (top) call frame, growing the frame’s slot vector as needed. No-op when there is no active frame.

Public so frontend extension handlers can write slot results back without reaching into frames directly.

Auto Trait Implementations§

§

impl !Freeze for VM

§

impl !RefUnwindSafe for VM

§

impl !Sync for VM

§

impl !UnwindSafe for VM

§

impl Send for VM

§

impl Unpin for VM

§

impl UnsafeUnpin for VM

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more