Skip to main content

harn_kernel/
execution.rs

1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::rc::Rc;
4use std::sync::Arc;
5
6use crate::portable_builtin::PortableBuiltin;
7use crate::type_contract::{
8    manifest_signature_is_portable, matches_compiler_schema, matches_manifest_type,
9};
10use crate::{Chunk, CompiledFunction, Constant, Diagnostic, Op, ProgramArtifact};
11
12mod arithmetic;
13mod builtins;
14mod methods;
15mod ops;
16mod resource;
17mod runtime_value;
18mod snapshot;
19mod type_guard;
20mod types;
21
22#[cfg(test)]
23mod tests;
24
25use crate::value::{semantic_try_compare, semantic_values_equal};
26use arithmetic::{add, div, modulo, mul, negate, pow, sub};
27use ops::*;
28use resource::{validate_runtime_value, MAX_VALUE_BYTES};
29use runtime_value::{Closure, EnumValue, RuntimeException, RuntimeValue};
30use snapshot::{decode_snapshot, encode_snapshot, ReplaySnapshot};
31use type_guard::validate_call;
32use types::value_kind;
33pub use types::{CapabilityRequest, CapabilityResult, DataValue, Execution, GrantSet, ValueShape};
34
35const DEFAULT_FUEL: u64 = 2_000_000;
36const MAX_FRAMES: usize = 1_024;
37const MAX_SCOPE_DEPTH: usize = 256;
38const MAX_OPERAND_STACK: usize = 16_384;
39const MAX_ITERATORS: usize = 1_024;
40pub const PORTABLE_MAX_SNAPSHOT_BYTES: usize = 1024 * 1024;
41
42pub fn start(program: &ProgramArtifact, input: DataValue, grants: &GrantSet) -> Execution {
43    run(program, input, grants, Vec::new())
44}
45
46/// Deterministically execute from the beginning with a recorded capability
47/// transcript. This is the native replay path and the oracle for snapshot
48/// resume: responses are consumed only when their request IDs match.
49pub fn replay(
50    program: &ProgramArtifact,
51    input: DataValue,
52    grants: &GrantSet,
53    responses: Vec<CapabilityResult>,
54) -> Execution {
55    run(program, input, grants, responses)
56}
57
58pub fn resume(
59    program: &ProgramArtifact,
60    snapshot: &[u8],
61    result: CapabilityResult,
62    grants: &GrantSet,
63) -> Execution {
64    let decoded = match decode_snapshot(snapshot, grants.snapshot_key()) {
65        Ok(value) => value,
66        Err(error) => return Execution::Failed { diagnostic: error },
67    };
68    if decoded.artifact_digest != program.digest() {
69        return failed(
70            "snapshot_program_mismatch",
71            "snapshot belongs to a different program artifact",
72        );
73    }
74    if decoded.grant_fingerprint != grants.fingerprint() {
75        return failed(
76            "snapshot_grant_mismatch",
77            "resume grants differ from the grants that created the snapshot",
78        );
79    }
80    if result.request_id() != decoded.pending_request {
81        return failed(
82            "capability_result_mismatch",
83            "capability result request ID does not match the suspended request",
84        );
85    }
86    let mut responses = decoded.responses;
87    responses.push(result);
88    run_with_fuel(
89        program,
90        decoded.input,
91        grants,
92        responses,
93        decoded.fuel_consumed,
94    )
95}
96
97fn run(
98    program: &ProgramArtifact,
99    input: DataValue,
100    grants: &GrantSet,
101    responses: Vec<CapabilityResult>,
102) -> Execution {
103    run_with_fuel(program, input, grants, responses, 0)
104}
105
106fn run_with_fuel(
107    program: &ProgramArtifact,
108    input: DataValue,
109    grants: &GrantSet,
110    responses: Vec<CapabilityResult>,
111    fuel_consumed: u64,
112) -> Execution {
113    if let Err(diagnostic) = input.validate() {
114        return Execution::Failed { diagnostic };
115    }
116    for response in &responses {
117        if let CapabilityResult::Ok { value, .. } = response {
118            if let Err(diagnostic) = value.validate() {
119                return Execution::Failed { diagnostic };
120            }
121        }
122    }
123    let root = Env::root();
124    let mut machine = Machine::new(program, root.clone(), grants, responses, fuel_consumed);
125    let bootstrap = match machine.execute(program.image().clone(), root, Vec::new()) {
126        Step::Value(value) => value,
127        Step::Suspend(request) => return machine.suspend(input, request),
128        Step::Error(error) => return Execution::Failed { diagnostic: error },
129    };
130    let RuntimeValue::Closure(closure) = bootstrap else {
131        return failed(
132            "entry_not_callable",
133            "compiled entry bootstrap did not return a callable",
134        );
135    };
136    let mut arguments = vec![RuntimeValue::from(input.clone())];
137    if program.expects_harness() {
138        arguments.insert(0, RuntimeValue::Harness("root".to_string()));
139    }
140    let Some(closure_env) = closure.env.upgrade() else {
141        return failed(
142            "closure_environment",
143            "entry closure environment is no longer available",
144        );
145    };
146    let entry_env = match machine.child_env(closure_env) {
147        Ok(env) => env,
148        Err(diagnostic) => return Execution::Failed { diagnostic },
149    };
150    if let Err(diagnostic) = machine.charge_call_validation(&arguments) {
151        return Execution::Failed { diagnostic };
152    }
153    if let Err(diagnostic) = validate_call(&closure.function, &arguments) {
154        return Execution::Failed { diagnostic };
155    }
156    match machine.execute_function(&closure.function, entry_env, arguments) {
157        Step::Value(value) => match machine
158            .charge_value_work(&value)
159            .and_then(|()| DataValue::try_from(value))
160        {
161            Ok(value) => Execution::Completed { value },
162            Err(error) => Execution::Failed { diagnostic: error },
163        },
164        Step::Suspend(request) => machine.suspend(input, request),
165        Step::Error(error) => Execution::Failed { diagnostic: error },
166    }
167}
168
169struct Machine<'a> {
170    program: &'a ProgramArtifact,
171    grants: &'a GrantSet,
172    responses: Vec<CapabilityResult>,
173    response_cursor: usize,
174    request_ordinal: u64,
175    fuel: u64,
176    replay_credit: u64,
177    environments: Vec<Rc<Env>>,
178    modules: BTreeMap<String, Rc<ModuleInstance>>,
179    loading_modules: Vec<String>,
180    iterators: Vec<IteratorState>,
181}
182
183impl<'a> Machine<'a> {
184    fn new(
185        program: &'a ProgramArtifact,
186        root: Rc<Env>,
187        grants: &'a GrantSet,
188        responses: Vec<CapabilityResult>,
189        fuel_consumed: u64,
190    ) -> Self {
191        Self {
192            program,
193            grants,
194            responses,
195            response_cursor: 0,
196            request_ordinal: 0,
197            fuel: DEFAULT_FUEL.saturating_sub(fuel_consumed),
198            replay_credit: fuel_consumed.min(DEFAULT_FUEL),
199            environments: vec![root],
200            modules: BTreeMap::new(),
201            loading_modules: Vec::new(),
202            iterators: Vec::new(),
203        }
204    }
205
206    fn import_root_module(
207        &mut self,
208        path: &str,
209        selected_names: Option<&[String]>,
210        namespace_alias: Option<NamespaceProjection<'_>>,
211        env: &Rc<Env>,
212    ) -> OpStep {
213        let Some(spec) = self
214            .program
215            .root_imports()
216            .iter()
217            .find(|spec| spec.path == path)
218        else {
219            return OpStep::Error(diagnostic(
220                "portable_import",
221                format!("root import `{path}` is not present in the package closure"),
222            ));
223        };
224        let target = spec.target.clone();
225        let module = match self.load_module(&target) {
226            ModuleStep::Ready(module) => module,
227            ModuleStep::Suspend(request) => return OpStep::Suspend(request),
228            ModuleStep::Error(error) => return OpStep::Error(error),
229        };
230        match self.bind_module_projection(&module, selected_names, namespace_alias, env) {
231            Ok(()) => OpStep::Continue,
232            Err(error) => OpStep::Error(error),
233        }
234    }
235
236    fn load_module(&mut self, id: &str) -> ModuleStep {
237        if let Some(module) = self.modules.get(id).cloned() {
238            return ModuleStep::Ready(module);
239        }
240        if self.loading_modules.iter().any(|loading| loading == id) {
241            return ModuleStep::Error(diagnostic(
242                "portable_import_cycle",
243                format!("portable package import cycle reaches `{id}`"),
244            ));
245        }
246        let Some(module) = self
247            .program
248            .modules()
249            .iter()
250            .find(|module| module.id() == id)
251            .cloned()
252        else {
253            return ModuleStep::Error(diagnostic(
254                "portable_import",
255                format!("portable package does not contain module `{id}`"),
256            ));
257        };
258        let module_env = Env::root();
259        self.retain_environment(&module_env);
260        self.loading_modules.push(id.to_string());
261
262        for import in module.imports() {
263            let imported = match self.load_module(&import.target) {
264                ModuleStep::Ready(imported) => imported,
265                ModuleStep::Suspend(request) => {
266                    self.loading_modules.pop();
267                    return ModuleStep::Suspend(request);
268                }
269                ModuleStep::Error(error) => {
270                    self.loading_modules.pop();
271                    return ModuleStep::Error(error);
272                }
273            };
274            if let Err(error) = self.bind_module_projection(
275                &imported,
276                import.selected_names.as_deref(),
277                // The manifest records an alias without member demand, so a
278                // module-to-module namespace import is projected whole.
279                import.namespace_alias.as_deref().map(|alias| (alias, None)),
280                &module_env,
281            ) {
282                self.loading_modules.pop();
283                return ModuleStep::Error(error);
284            }
285        }
286
287        if let Some(init) = module.init() {
288            match self.execute(init.clone(), module_env.clone(), Vec::new()) {
289                Step::Value(_) => {}
290                Step::Suspend(request) => {
291                    self.loading_modules.pop();
292                    return ModuleStep::Suspend(request);
293                }
294                Step::Error(error) => {
295                    self.loading_modules.pop();
296                    return ModuleStep::Error(error);
297                }
298            }
299        }
300
301        for (name, function) in module.functions() {
302            self.retain_environment(&module_env);
303            let value = RuntimeValue::Closure(Closure {
304                function: function.clone(),
305                env: Rc::downgrade(&module_env),
306            });
307            module_env.define(name.clone(), value.clone());
308        }
309        let instance = Rc::new(ModuleInstance {
310            env: module_env,
311            exports: module.exports().clone(),
312        });
313        self.modules.insert(id.to_string(), instance.clone());
314        self.loading_modules.pop();
315        ModuleStep::Ready(instance)
316    }
317
318    fn bind_module_projection(
319        &self,
320        module: &ModuleInstance,
321        selected_names: Option<&[String]>,
322        namespace_alias: Option<NamespaceProjection<'_>>,
323        env: &Rc<Env>,
324    ) -> Result<(), Diagnostic> {
325        if let Some((alias, demanded)) = namespace_alias {
326            if env.contains_local(alias) {
327                return Err(diagnostic(
328                    "portable_import_collision",
329                    format!("import namespace `{alias}` collides with an existing binding"),
330                ));
331            }
332            let mut entries = BTreeMap::new();
333            for (name, kind) in &module.exports {
334                if !kind.has_runtime_value() {
335                    continue;
336                }
337                if demanded.is_some_and(|members| !members.iter().any(|member| member == name)) {
338                    continue;
339                }
340                if let Some(value) = module.env.get(name) {
341                    entries.insert(name.clone(), value);
342                }
343            }
344            if let Some(members) = demanded {
345                for member in members {
346                    if !module.exports.contains_key(member) {
347                        return Err(diagnostic(
348                            "portable_import",
349                            format!("module does not export `{member}`"),
350                        ));
351                    }
352                }
353            }
354            env.define(alias.to_string(), RuntimeValue::Record(Rc::new(entries)));
355            return Ok(());
356        }
357        let names = selected_names
358            .map(|names| names.to_vec())
359            .unwrap_or_else(|| module.exports.keys().cloned().collect());
360        for name in names {
361            let Some(kind) = module.exports.get(&name) else {
362                return Err(diagnostic(
363                    "portable_import",
364                    format!("module does not export `{name}`"),
365                ));
366            };
367            if !kind.has_runtime_value() {
368                continue;
369            }
370            let Some(value) = module.env.get(&name) else {
371                return Err(diagnostic(
372                    "portable_import",
373                    format!("module export `{name}` has no runtime value"),
374                ));
375            };
376            if env.contains_local(&name) {
377                return Err(diagnostic(
378                    "portable_import_collision",
379                    format!("imported binding `{name}` collides with an existing binding"),
380                ));
381            }
382            env.define(name, value);
383        }
384        Ok(())
385    }
386
387    fn child_env(&mut self, parent: Rc<Env>) -> Result<Rc<Env>, Diagnostic> {
388        Env::child(parent)
389    }
390
391    fn retain_environment(&mut self, environment: &Rc<Env>) {
392        self.environments.push(environment.clone());
393    }
394
395    fn charge(&mut self, amount: u64) -> Result<(), Diagnostic> {
396        let replayed = amount.min(self.replay_credit);
397        self.replay_credit -= replayed;
398        let fresh = amount - replayed;
399        if fresh > self.fuel {
400            self.fuel = 0;
401            return Err(diagnostic(
402                "execution_fuel",
403                "portable execution exhausted its deterministic fuel limit",
404            ));
405        }
406        self.fuel -= fresh;
407        Ok(())
408    }
409
410    fn charge_value_work(&mut self, value: &RuntimeValue) -> Result<(), Diagnostic> {
411        let usage = validate_runtime_value(value)?;
412        self.charge(usage.nodes as u64)
413    }
414
415    fn charge_call_validation(&mut self, arguments: &[RuntimeValue]) -> Result<(), Diagnostic> {
416        let mut nodes = 0_u64;
417        for argument in arguments {
418            let usage = validate_runtime_value(argument)?;
419            nodes = nodes.saturating_add(usage.nodes as u64);
420        }
421        self.charge(nodes)
422    }
423
424    fn charge_values_work(&mut self, values: &[&RuntimeValue]) -> Result<(), Diagnostic> {
425        let mut nodes = 0_u64;
426        for value in values {
427            let usage = validate_runtime_value(value)?;
428            nodes = nodes.saturating_add(usage.nodes as u64);
429        }
430        self.charge(nodes)
431    }
432
433    fn render_value(&mut self, value: &RuntimeValue) -> Result<String, Diagnostic> {
434        self.charge_value_work(value)?;
435        Ok(value.display())
436    }
437
438    fn push_charged(&mut self, value: RuntimeValue) -> OpStep {
439        match self.charge_value_work(&value) {
440            Ok(()) => OpStep::Push(value),
441            Err(diagnostic) => OpStep::Error(diagnostic),
442        }
443    }
444
445    fn values_equal(
446        &mut self,
447        left: &RuntimeValue,
448        right: &RuntimeValue,
449    ) -> Result<bool, Diagnostic> {
450        self.charge_values_work(&[left, right])?;
451        Ok(equal(left, right))
452    }
453
454    fn suspend(&self, input: DataValue, request: CapabilityRequest) -> Execution {
455        let Some(snapshot_key) = self.grants.snapshot_key() else {
456            return failed(
457                "snapshot_key_required",
458                "suspendable capability grants require a host-owned snapshot key",
459            );
460        };
461        let snapshot = ReplaySnapshot {
462            artifact_digest: self.program.digest(),
463            grant_fingerprint: self.grants.fingerprint(),
464            fuel_consumed: DEFAULT_FUEL - self.fuel,
465            input,
466            responses: self.responses[..self.response_cursor].to_vec(),
467            pending_request: request.id.clone(),
468        };
469        match encode_snapshot(&snapshot, snapshot_key) {
470            Ok(snapshot) => Execution::Suspended { request, snapshot },
471            Err(diagnostic) => Execution::Failed { diagnostic },
472        }
473    }
474
475    fn execute(&mut self, chunk: Arc<Chunk>, env: Rc<Env>, arguments: Vec<RuntimeValue>) -> Step {
476        let mut frames = vec![Frame::new(chunk, env, arguments)];
477        self.execute_frames(&mut frames)
478    }
479
480    fn execute_function(
481        &mut self,
482        function: &CompiledFunction,
483        env: Rc<Env>,
484        arguments: Vec<RuntimeValue>,
485    ) -> Step {
486        let frame = match self.function_frame(function, env, arguments) {
487            Ok(frame) => frame,
488            Err(diagnostic) => return Step::Error(diagnostic),
489        };
490        let mut frames = vec![frame];
491        self.execute_frames(&mut frames)
492    }
493
494    fn function_frame(
495        &mut self,
496        function: &CompiledFunction,
497        env: Rc<Env>,
498        arguments: Vec<RuntimeValue>,
499    ) -> Result<Frame, Diagnostic> {
500        let frame = Frame::for_function(function, env, arguments);
501        if function.has_rest_param && !function.params.is_empty() {
502            let rest_index = function.params.len() - 1;
503            if let Some(Some(rest)) = frame.locals.get(rest_index) {
504                self.charge_value_work(rest)?;
505            }
506        }
507        Ok(frame)
508    }
509
510    fn execute_frames(&mut self, frames: &mut Vec<Frame>) -> Step {
511        loop {
512            if let Err(diagnostic) = self.charge(1) {
513                return Step::Error(diagnostic);
514            }
515            let Some(frame) = frames.last_mut() else {
516                return Step::Error(diagnostic(
517                    "execution_state",
518                    "execution has no active frame",
519                ));
520            };
521            if frame.ip >= frame.chunk.code.len() {
522                return Step::Error(diagnostic(
523                    "instruction_pointer",
524                    "instruction pointer escaped its chunk",
525                ));
526            }
527            let offset = frame.ip;
528            let byte = frame.chunk.code[frame.ip];
529            frame.ip += 1;
530            let Some(op) = Op::from_byte(byte) else {
531                return Step::Error(diagnostic(
532                    "invalid_opcode",
533                    format!("invalid opcode 0x{byte:02x}"),
534                ));
535            };
536            let result = match self.execute_op(op, offset, frames) {
537                Ok(result) | Err(result) => result,
538            };
539            match result {
540                OpStep::Continue => {}
541                OpStep::Push(value) => frames
542                    .last_mut()
543                    .expect("active frame accepts operation result")
544                    .stack
545                    .push(value),
546                OpStep::Call(closure, args, tail) => {
547                    if frames.len() >= MAX_FRAMES {
548                        return Step::Error(diagnostic(
549                            "frame_limit",
550                            "portable execution exceeded its frame limit",
551                        ));
552                    }
553                    let Some(closure_env) = closure.env.upgrade() else {
554                        return Step::Error(diagnostic(
555                            "closure_environment",
556                            "closure environment is no longer available",
557                        ));
558                    };
559                    let env = match self.child_env(closure_env) {
560                        Ok(env) => env,
561                        Err(diagnostic) => return Step::Error(diagnostic),
562                    };
563                    if let Err(diagnostic) = self.charge_call_validation(&args) {
564                        return Step::Error(diagnostic);
565                    }
566                    if let Err(diagnostic) = validate_call(&closure.function, &args) {
567                        return Step::Error(diagnostic);
568                    }
569                    let next = match self.function_frame(&closure.function, env, args) {
570                        Ok(frame) => frame,
571                        Err(diagnostic) => return Step::Error(diagnostic),
572                    };
573                    if tail {
574                        *frames.last_mut().expect("caller exists") = next;
575                    } else {
576                        frames.push(next);
577                    }
578                }
579                OpStep::Return(value) => {
580                    frames.pop();
581                    if let Some(caller) = frames.last_mut() {
582                        caller.stack.push(value);
583                    } else {
584                        return Step::Value(value);
585                    }
586                }
587                OpStep::Suspend(request) => return Step::Suspend(request),
588                OpStep::Throw(error) => {
589                    if !handle_throw(frames, error.clone()) {
590                        let message = match self.render_value(&error.value) {
591                            Ok(message) => message,
592                            Err(diagnostic) => return Step::Error(diagnostic),
593                        };
594                        return Step::Error(diagnostic(
595                            if error.declared {
596                                "harn_declared_throw"
597                            } else {
598                                "harn_throw"
599                            },
600                            message,
601                        ));
602                    }
603                }
604                OpStep::Error(error) => return Step::Error(error),
605            }
606            if frames
607                .last()
608                .is_some_and(|frame| frame.stack.len() > MAX_OPERAND_STACK)
609            {
610                return Step::Error(diagnostic(
611                    "operand_stack_limit",
612                    "portable execution exceeded its operand stack limit",
613                ));
614            }
615        }
616    }
617
618    #[allow(clippy::too_many_lines)]
619    fn execute_op(
620        &mut self,
621        op: Op,
622        offset: usize,
623        frames: &mut [Frame],
624    ) -> Result<OpStep, OpStep> {
625        let frame = frames.last_mut().expect("active frame");
626        macro_rules! pop {
627            () => {
628                match frame.stack.pop() {
629                    Some(value) => value,
630                    None => {
631                        return Err(OpStep::Error(diagnostic(
632                            "stack_underflow",
633                            format!("{} at {offset}", op.name()),
634                        )))
635                    }
636                }
637            };
638        }
639        match op {
640            Op::Constant => {
641                let index = read_u16(frame)?;
642                let Some(value) = frame.chunk.constants.get(index).cloned() else {
643                    return Err(invalid_index("constant", index));
644                };
645                frame.stack.push(RuntimeValue::from(value));
646            }
647            Op::Nil => frame.stack.push(RuntimeValue::Nil),
648            Op::True => frame.stack.push(RuntimeValue::Bool(true)),
649            Op::False => frame.stack.push(RuntimeValue::Bool(false)),
650            Op::RootHarness => frame.stack.push(RuntimeValue::Harness("root".to_string())),
651            Op::GetVar => {
652                let name = read_constant_string(frame)?;
653                frame.stack.push(
654                    frame
655                        .env
656                        .get(&name)
657                        .unwrap_or_else(|| RuntimeValue::Builtin(name)),
658                );
659            }
660            Op::DefLet | Op::DefVar | Op::DefCell => {
661                let name = read_constant_string(frame)?;
662                let value = pop!();
663                frame.env.define(name, value);
664            }
665            Op::SetVar => {
666                let name = read_constant_string(frame)?;
667                let value = pop!();
668                frame.env.set(&name, value);
669            }
670            Op::PushScope => {
671                frame.env = self.child_env(frame.env.clone()).map_err(OpStep::Error)?;
672            }
673            Op::PopScope => {
674                if let Some(parent) = &frame.env.parent {
675                    frame.env = parent.clone();
676                }
677            }
678            Op::GetLocalSlot => {
679                let slot = read_u16(frame)?;
680                let Some(value) = frame.locals.get(slot).and_then(Clone::clone) else {
681                    return Err(invalid_index("local", slot));
682                };
683                frame.stack.push(value);
684            }
685            Op::DefLocalSlot | Op::SetLocalSlot => {
686                let slot = read_u16(frame)?;
687                let value = pop!();
688                if slot >= frame.locals.len() {
689                    return Err(invalid_index("local", slot));
690                }
691                frame.locals[slot] = Some(value.clone());
692                if let Some(local) = frame.chunk.local_slots.get(slot) {
693                    if op == Op::DefLocalSlot {
694                        frame.env.define(local.name.clone(), value);
695                    } else {
696                        frame.env.set(&local.name, value);
697                    }
698                }
699            }
700            Op::ConcatAssignLocal => {
701                let slot = read_u16(frame)?;
702                let rhs = pop!();
703                let Some(local) = frame.chunk.local_slots.get(slot) else {
704                    return Err(invalid_index("local", slot));
705                };
706                if !local.mutable {
707                    return Err(OpStep::Error(diagnostic(
708                        "immutable_assignment",
709                        format!("cannot assign to immutable binding `{}`", local.name),
710                    )));
711                }
712                let Some(lhs) = frame.locals.get(slot).and_then(Clone::clone) else {
713                    return Err(invalid_index("local", slot));
714                };
715                let value = add(lhs, rhs).map_err(OpStep::Error)?;
716                self.charge_value_work(&value).map_err(OpStep::Error)?;
717                frame.locals[slot] = Some(value.clone());
718                frame.env.set(&local.name, value);
719            }
720            Op::GetArgc => frame.stack.push(RuntimeValue::Int(frame.argc as i64)),
721            Op::Pop => {
722                pop!();
723            }
724            Op::Dup => {
725                let value = pop!();
726                frame.stack.push(value.clone());
727                frame.stack.push(value);
728            }
729            Op::Swap => {
730                let right = pop!();
731                let left = pop!();
732                frame.stack.push(right);
733                frame.stack.push(left);
734            }
735            Op::Add | Op::AddInt | Op::AddFloat => {
736                let value = binary(frame, add)?;
737                self.charge_value_work(&value).map_err(OpStep::Error)?;
738                frame.stack.push(value);
739            }
740            Op::Sub | Op::SubInt | Op::SubFloat => {
741                let value = binary(frame, sub)?;
742                self.charge_value_work(&value).map_err(OpStep::Error)?;
743                frame.stack.push(value);
744            }
745            Op::Mul | Op::MulInt | Op::MulFloat => {
746                let value = binary(frame, mul)?;
747                self.charge_value_work(&value).map_err(OpStep::Error)?;
748                frame.stack.push(value);
749            }
750            Op::Div | Op::DivInt | Op::DivFloat => {
751                let value = binary(frame, div)?;
752                self.charge_value_work(&value).map_err(OpStep::Error)?;
753                frame.stack.push(value);
754            }
755            Op::Mod | Op::ModInt | Op::ModFloat => {
756                let value = binary(frame, modulo)?;
757                self.charge_value_work(&value).map_err(OpStep::Error)?;
758                frame.stack.push(value);
759            }
760            Op::Pow => {
761                let value = binary(frame, pow)?;
762                self.charge_value_work(&value).map_err(OpStep::Error)?;
763                frame.stack.push(value);
764            }
765            Op::Negate => {
766                let value = pop!();
767                frame.stack.push(negate(value).map_err(OpStep::Error)?);
768            }
769            Op::Not => {
770                let value = pop!();
771                frame.stack.push(RuntimeValue::Bool(!value.truthy()));
772            }
773            Op::Equal | Op::EqualInt | Op::EqualFloat | Op::EqualBool | Op::EqualString => {
774                compare_equality(self, frame, true)?;
775            }
776            Op::NotEqual
777            | Op::NotEqualInt
778            | Op::NotEqualFloat
779            | Op::NotEqualBool
780            | Op::NotEqualString => compare_equality(self, frame, false)?,
781            Op::Less | Op::LessInt | Op::LessFloat => compare(self, frame, |value| value < 0)?,
782            Op::Greater | Op::GreaterInt | Op::GreaterFloat => {
783                compare(self, frame, |value| value > 0)?;
784            }
785            Op::LessEqual | Op::LessEqualInt | Op::LessEqualFloat => {
786                compare(self, frame, |value| value <= 0)?;
787            }
788            Op::GreaterEqual | Op::GreaterEqualInt | Op::GreaterEqualFloat => {
789                compare(self, frame, |value| value >= 0)?;
790            }
791            Op::Jump => frame.ip = read_u16(frame)?,
792            Op::JumpIfFalse => {
793                let target = read_u16(frame)?;
794                if !frame.stack.last().is_some_and(RuntimeValue::truthy) {
795                    frame.ip = target;
796                }
797            }
798            Op::JumpIfTrue => {
799                let target = read_u16(frame)?;
800                if frame.stack.last().is_some_and(RuntimeValue::truthy) {
801                    frame.ip = target;
802                }
803            }
804            Op::Closure => {
805                let index = read_u16(frame)?;
806                let Some(function) = frame.chunk.functions.get(index).cloned() else {
807                    return Err(invalid_index("function", index));
808                };
809                self.retain_environment(&frame.env);
810                frame.stack.push(RuntimeValue::Closure(Closure {
811                    function,
812                    env: Rc::downgrade(&frame.env),
813                }));
814            }
815            Op::Call | Op::TailCall => {
816                let argc = read_u8(frame)?;
817                let args = pop_args(frame, argc)?;
818                let callee = pop!();
819                return Ok(call_value(
820                    self,
821                    &frame.env,
822                    callee,
823                    args,
824                    op == Op::TailCall,
825                ));
826            }
827            Op::Return => {
828                return Ok(OpStep::Return(
829                    frame.stack.pop().unwrap_or(RuntimeValue::Nil),
830                ))
831            }
832            Op::BuildList => {
833                let count = read_u16(frame)?;
834                let values = pop_args(frame, count)?;
835                let value = RuntimeValue::List(Rc::new(values));
836                self.charge_value_work(&value).map_err(OpStep::Error)?;
837                frame.stack.push(value);
838            }
839            Op::BuildDict => {
840                let count = read_u16(frame)?;
841                let values = pop_args(frame, count * 2)?;
842                let mut map = BTreeMap::new();
843                for pair in values.chunks_exact(2) {
844                    let key = self.render_value(&pair[0]).map_err(OpStep::Error)?;
845                    map.insert(key, pair[1].clone());
846                }
847                let value = RuntimeValue::Record(Rc::new(map));
848                self.charge_value_work(&value).map_err(OpStep::Error)?;
849                frame.stack.push(value);
850            }
851            Op::GetProperty | Op::GetPropertyOpt => {
852                let name = read_constant_string(frame)?;
853                let value = pop!();
854                match get_property(&value, &name) {
855                    Some(value) => frame.stack.push(value),
856                    None if op == Op::GetPropertyOpt => frame.stack.push(RuntimeValue::Nil),
857                    None => {
858                        return Err(OpStep::Error(diagnostic(
859                            "missing_property",
860                            format!("value has no property `{name}`"),
861                        )))
862                    }
863                }
864            }
865            Op::Subscript | Op::SubscriptOpt => {
866                let index = pop!();
867                let value = pop!();
868                match self.subscript(&value, &index).map_err(OpStep::Error)? {
869                    Some(value) => frame.stack.push(value),
870                    None if op == Op::SubscriptOpt => frame.stack.push(RuntimeValue::Nil),
871                    None => {
872                        return Err(OpStep::Error(diagnostic(
873                            "subscript",
874                            "subscript does not exist",
875                        )))
876                    }
877                }
878            }
879            Op::SetProperty => {
880                let property = read_constant_string(frame)?;
881                let binding = read_constant_string(frame)?;
882                let value = pop!();
883                let Some(target) = frame.env.get(&binding) else {
884                    return Err(OpStep::Error(diagnostic(
885                        "undefined_variable",
886                        format!("cannot assign property on undefined binding `{binding}`"),
887                    )));
888                };
889                let updated =
890                    set_property_value(target, &property, value).map_err(OpStep::Error)?;
891                self.charge_value_work(&updated).map_err(OpStep::Error)?;
892                frame.env.set(&binding, updated);
893            }
894            Op::SetSubscript => {
895                let binding = read_constant_string(frame)?;
896                let index = pop!();
897                let value = pop!();
898                let Some(target) = frame.env.get(&binding) else {
899                    return Err(OpStep::Error(diagnostic(
900                        "undefined_variable",
901                        format!("cannot assign subscript on undefined binding `{binding}`"),
902                    )));
903                };
904                let updated = set_subscript_value(target, index, value).map_err(OpStep::Error)?;
905                self.charge_value_work(&updated).map_err(OpStep::Error)?;
906                frame.env.set(&binding, updated);
907            }
908            Op::SetLocalSlotProperty => {
909                let property = read_constant_string(frame)?;
910                let slot = read_u16(frame)?;
911                let value = pop!();
912                let Some(local) = frame.chunk.local_slots.get(slot) else {
913                    return Err(invalid_index("local", slot));
914                };
915                if !local.mutable {
916                    return Err(OpStep::Error(diagnostic(
917                        "immutable_assignment",
918                        format!("cannot assign to immutable binding `{}`", local.name),
919                    )));
920                }
921                let Some(target) = frame.locals.get(slot).and_then(Clone::clone) else {
922                    return Err(invalid_index("local", slot));
923                };
924                let updated =
925                    set_property_value(target, &property, value).map_err(OpStep::Error)?;
926                self.charge_value_work(&updated).map_err(OpStep::Error)?;
927                frame.locals[slot] = Some(updated.clone());
928                frame.env.set(&local.name, updated);
929            }
930            Op::SetLocalSlotSubscript => {
931                let slot = read_u16(frame)?;
932                let index = pop!();
933                let value = pop!();
934                let Some(local) = frame.chunk.local_slots.get(slot) else {
935                    return Err(invalid_index("local", slot));
936                };
937                if !local.mutable {
938                    return Err(OpStep::Error(diagnostic(
939                        "immutable_assignment",
940                        format!("cannot assign to immutable binding `{}`", local.name),
941                    )));
942                }
943                let Some(target) = frame.locals.get(slot).and_then(Clone::clone) else {
944                    return Err(invalid_index("local", slot));
945                };
946                let updated = set_subscript_value(target, index, value).map_err(OpStep::Error)?;
947                self.charge_value_work(&updated).map_err(OpStep::Error)?;
948                frame.locals[slot] = Some(updated.clone());
949                frame.env.set(&local.name, updated);
950            }
951            Op::Slice => {
952                let end = pop!();
953                let start = pop!();
954                let value = pop!();
955                let value = slice(value, start, end).map_err(OpStep::Error)?;
956                self.charge_value_work(&value).map_err(OpStep::Error)?;
957                frame.stack.push(value);
958            }
959            Op::MethodCall | Op::MethodCallOpt => {
960                let name = read_constant_string(frame)?;
961                let argc = read_u8(frame)?;
962                let args = pop_args(frame, argc)?;
963                let receiver = pop!();
964                if op == Op::MethodCallOpt && matches!(receiver, RuntimeValue::Nil) {
965                    frame.stack.push(RuntimeValue::Nil);
966                } else {
967                    return Ok(self.call_method(receiver, &name, args));
968                }
969            }
970            Op::Concat => {
971                let count = read_u16(frame)?;
972                let values = pop_args(frame, count)?;
973                let mut rendered = String::new();
974                for value in &values {
975                    let part = self.render_value(value).map_err(OpStep::Error)?;
976                    if rendered.len().saturating_add(part.len()) > MAX_VALUE_BYTES {
977                        return Err(OpStep::Error(diagnostic(
978                            "value_byte_limit",
979                            "string interpolation exceeds the portable value byte limit",
980                        )));
981                    }
982                    rendered.push_str(&part);
983                }
984                frame.stack.push(RuntimeValue::String(Arc::from(rendered)));
985            }
986            Op::Contains => {
987                let container = pop!();
988                let item = pop!();
989                let found = self.contains(&container, &item).map_err(OpStep::Error)?;
990                frame.stack.push(RuntimeValue::Bool(found));
991            }
992            Op::IterInit => {
993                if self.iterators.len() >= MAX_ITERATORS {
994                    return Err(OpStep::Error(diagnostic(
995                        "iterator_limit",
996                        "portable execution exceeded its iterator limit",
997                    )));
998                }
999                let iterable = pop!();
1000                let iterator = match iterable {
1001                    RuntimeValue::List(values) => IteratorState::List { values, index: 0 },
1002                    RuntimeValue::Record(values) => IteratorState::Record {
1003                        keys: values.keys().cloned().collect(),
1004                        values,
1005                        index: 0,
1006                    },
1007                    other => {
1008                        return Err(OpStep::Error(diagnostic(
1009                            "iterator_type",
1010                            format!(
1011                                "cannot iterate over {} in the portable kernel",
1012                                runtime_value_kind(&other)
1013                            ),
1014                        )))
1015                    }
1016                };
1017                self.iterators.push(iterator);
1018            }
1019            Op::IterNext => {
1020                let target = read_u16(frame)?;
1021                let Some(iterator) = self.iterators.last_mut() else {
1022                    return Err(OpStep::Error(diagnostic(
1023                        "iterator_state",
1024                        "iterator step has no active iterator",
1025                    )));
1026                };
1027                match iterator {
1028                    IteratorState::List { values, index } => {
1029                        if let Some(value) = values.get(*index).cloned() {
1030                            *index += 1;
1031                            frame.stack.push(value);
1032                        } else {
1033                            self.iterators.pop();
1034                            frame.ip = target;
1035                        }
1036                    }
1037                    IteratorState::Record {
1038                        keys,
1039                        values,
1040                        index,
1041                    } => {
1042                        if let Some(key) = keys.get(*index) {
1043                            let value = values.get(key).cloned().unwrap_or(RuntimeValue::Nil);
1044                            *index += 1;
1045                            frame
1046                                .stack
1047                                .push(RuntimeValue::Record(Rc::new(BTreeMap::from([
1048                                    (
1049                                        "key".to_string(),
1050                                        RuntimeValue::String(Arc::from(key.as_str())),
1051                                    ),
1052                                    ("value".to_string(), value),
1053                                ]))));
1054                        } else {
1055                            self.iterators.pop();
1056                            frame.ip = target;
1057                        }
1058                    }
1059                }
1060            }
1061            Op::PopIterator => {
1062                self.iterators.pop();
1063            }
1064            Op::TryCatchSetup | Op::TryCatchPreserve => {
1065                let target = read_u16(frame)?;
1066                let _type_name = read_u16(frame)?;
1067                frame.handlers.push(Handler {
1068                    target,
1069                    stack_depth: frame.stack.len(),
1070                    env: frame.env.clone(),
1071                    preserve: op == Op::TryCatchPreserve,
1072                });
1073            }
1074            Op::PopHandler => {
1075                frame.handlers.pop();
1076            }
1077            Op::Throw | Op::ThrowDeclared => {
1078                return Ok(OpStep::Throw(RuntimeException {
1079                    value: pop!(),
1080                    declared: op == Op::ThrowDeclared,
1081                }))
1082            }
1083            Op::Rethrow => {
1084                let RuntimeValue::Exception(error) = pop!() else {
1085                    return Err(OpStep::Error(diagnostic(
1086                        "invalid_rethrow",
1087                        "rethrow requires a caught exception carrier",
1088                    )));
1089                };
1090                return Ok(OpStep::Throw((*error).clone()));
1091            }
1092            Op::Import => {
1093                let path = read_constant_string(frame)?;
1094                let env = frame.env.clone();
1095                return Ok(self.import_root_module(&path, None, None, &env));
1096            }
1097            Op::SelectiveImport => {
1098                let path = read_constant_string(frame)?;
1099                let names = read_constant_string(frame)?;
1100                let selected = names
1101                    .split(',')
1102                    .filter(|name| !name.is_empty())
1103                    .map(str::to_string)
1104                    .collect::<Vec<_>>();
1105                let env = frame.env.clone();
1106                return Ok(self.import_root_module(&path, Some(&selected), None, &env));
1107            }
1108            Op::NamespaceImport => {
1109                let path = read_constant_string(frame)?;
1110                let alias = read_constant_string(frame)?;
1111                let env = frame.env.clone();
1112                return Ok(self.import_root_module(&path, None, Some((&alias, None)), &env));
1113            }
1114            // The narrowed form the compiler emits when every use of the
1115            // namespace is a statically known member. The projection is the
1116            // demand set rather than the module's whole export surface;
1117            // escaping or dynamic uses compile to `NamespaceImport` instead,
1118            // so a missing member here is a compiler contract violation, not a
1119            // program error.
1120            Op::NamespaceImportMembers => {
1121                let path = read_constant_string(frame)?;
1122                let alias = read_constant_string(frame)?;
1123                let members = read_constant_string(frame)?;
1124                let demanded = members
1125                    .split(',')
1126                    .filter(|name| !name.is_empty())
1127                    .map(str::to_string)
1128                    .collect::<Vec<_>>();
1129                let env = frame.env.clone();
1130                return Ok(self.import_root_module(
1131                    &path,
1132                    None,
1133                    Some((&alias, Some(&demanded))),
1134                    &env,
1135                ));
1136            }
1137            Op::BuildEnum => {
1138                let enum_name = read_constant_string(frame)?;
1139                let variant = read_constant_string(frame)?;
1140                let field_count = read_u16(frame)?;
1141                let fields = pop_args(frame, field_count)?;
1142                let value = RuntimeValue::Enum(Rc::new(EnumValue {
1143                    enum_name: Arc::from(enum_name),
1144                    variant: Arc::from(variant),
1145                    fields: Rc::new(fields),
1146                }));
1147                self.charge_value_work(&value).map_err(OpStep::Error)?;
1148                frame.stack.push(value);
1149            }
1150            Op::MatchEnum => {
1151                let enum_name = read_constant_string(frame)?;
1152                let variant = read_constant_string(frame)?;
1153                let value = pop!();
1154                let matches = matches!(
1155                    &value,
1156                    RuntimeValue::Enum(candidate)
1157                        if candidate.is_variant(&enum_name, &variant)
1158                );
1159                frame.stack.push(value);
1160                frame.stack.push(RuntimeValue::Bool(matches));
1161            }
1162            Op::TryWrapOk => {
1163                let value = pop!();
1164                if matches!(&value, RuntimeValue::Enum(candidate) if candidate.enum_name.as_ref() == "Result")
1165                {
1166                    frame.stack.push(value);
1167                } else {
1168                    frame.stack.push(RuntimeValue::Enum(Rc::new(EnumValue {
1169                        enum_name: Arc::from("Result"),
1170                        variant: Arc::from("Ok"),
1171                        fields: Rc::new(vec![value]),
1172                    })));
1173                }
1174            }
1175            Op::TryUnwrap => {
1176                let value = pop!();
1177                let RuntimeValue::Enum(result) = &value else {
1178                    return Err(OpStep::Error(diagnostic(
1179                        "try_unwrap_type",
1180                        format!(
1181                            "? operator requires a Result value, got {}",
1182                            runtime_value_kind(&value)
1183                        ),
1184                    )));
1185                };
1186                if result.enum_name.as_ref() != "Result" {
1187                    return Err(OpStep::Error(diagnostic(
1188                        "try_unwrap_type",
1189                        format!(
1190                            "? operator requires a Result value, got {}",
1191                            runtime_value_kind(&value)
1192                        ),
1193                    )));
1194                }
1195                if result.variant.as_ref() == "Ok" {
1196                    frame
1197                        .stack
1198                        .push(result.fields.first().cloned().unwrap_or(RuntimeValue::Nil));
1199                } else {
1200                    return Ok(OpStep::Return(value));
1201                }
1202            }
1203            Op::AssertBindingType => {
1204                let index = read_u16(frame)?;
1205                let Some(slot) = frame.chunk.binding_types.get(index) else {
1206                    return Err(invalid_index("binding type", index));
1207                };
1208                let Some(value) = frame.stack.last() else {
1209                    return Err(OpStep::Error(diagnostic(
1210                        "stack_underflow",
1211                        "AssertBindingType requires the bound value on the stack",
1212                    )));
1213                };
1214                type_guard::validate_binding(value, slot).map_err(OpStep::Error)?;
1215            }
1216            Op::CheckType => {
1217                return Err(OpStep::Error(diagnostic(
1218                    "unsupported_portable_opcode",
1219                    format!("{} is not implemented by the portable kernel", op.name()),
1220                )))
1221            }
1222            Op::CallBuiltin => {
1223                frame.ip += 8;
1224                let name = read_constant_string(frame)?;
1225                let argc = read_u8(frame)?;
1226                let args = pop_args(frame, argc)?;
1227                return Ok(call_named(self, &frame.env, &name, args, false));
1228            }
1229            Op::CallBuiltinSpread => {
1230                frame.ip += 8;
1231                let name = read_constant_string(frame)?;
1232                let spread = pop!();
1233                let RuntimeValue::List(args) = spread else {
1234                    return Err(OpStep::Error(diagnostic(
1235                        "spread_type",
1236                        "spread call requires a list",
1237                    )));
1238                };
1239                return Ok(call_named(
1240                    self,
1241                    &frame.env,
1242                    &name,
1243                    Rc::unwrap_or_clone(args),
1244                    false,
1245                ));
1246            }
1247            unsupported @ (Op::Pipe
1248            | Op::Parallel
1249            | Op::ParallelMap
1250            | Op::ParallelMapStream
1251            | Op::ParallelSettle
1252            | Op::Spawn
1253            | Op::SyncMutexEnter
1254            | Op::SyncMutexEnterKeyed
1255            | Op::TaskScopeEnter
1256            | Op::TaskScopeExit
1257            | Op::DeadlineSetup
1258            | Op::DeadlineEnd
1259            | Op::CallSpread
1260            | Op::MethodCallSpread
1261            | Op::Yield) => {
1262                return Err(OpStep::Error(diagnostic(
1263                    "unsupported_portable_opcode",
1264                    format!(
1265                        "{} is not implemented by the portable kernel",
1266                        unsupported.name()
1267                    ),
1268                )))
1269            }
1270        }
1271        Ok(OpStep::Continue)
1272    }
1273}
1274
1275struct Env {
1276    values: RefCell<BTreeMap<String, RuntimeValue>>,
1277    parent: Option<Rc<Env>>,
1278    depth: usize,
1279}
1280
1281struct ModuleInstance {
1282    env: Rc<Env>,
1283    exports: BTreeMap<String, crate::PortableExportKind>,
1284}
1285
1286enum IteratorState {
1287    List {
1288        values: Rc<Vec<RuntimeValue>>,
1289        index: usize,
1290    },
1291    Record {
1292        values: Rc<BTreeMap<String, RuntimeValue>>,
1293        keys: Vec<String>,
1294        index: usize,
1295    },
1296}
1297
1298enum ModuleStep {
1299    Ready(Rc<ModuleInstance>),
1300    Suspend(CapabilityRequest),
1301    Error(Diagnostic),
1302}
1303
1304/// A namespace import's alias and, when the compiler proved the whole set of
1305/// members a module actually demands, that demand set. `None` members means
1306/// the namespace escaped static analysis and must be projected whole.
1307type NamespaceProjection<'a> = (&'a str, Option<&'a [String]>);
1308
1309impl Env {
1310    fn root() -> Rc<Self> {
1311        Rc::new(Self {
1312            values: RefCell::new(BTreeMap::new()),
1313            parent: None,
1314            depth: 0,
1315        })
1316    }
1317    fn child(parent: Rc<Self>) -> Result<Rc<Self>, Diagnostic> {
1318        if parent.depth >= MAX_SCOPE_DEPTH {
1319            return Err(diagnostic(
1320                "scope_depth_limit",
1321                "portable execution exceeded its lexical scope depth limit",
1322            ));
1323        }
1324        let depth = parent.depth + 1;
1325        Ok(Rc::new(Self {
1326            values: RefCell::new(BTreeMap::new()),
1327            parent: Some(parent),
1328            depth,
1329        }))
1330    }
1331    fn define(&self, name: String, value: RuntimeValue) {
1332        self.values.borrow_mut().insert(name, value);
1333    }
1334    fn contains_local(&self, name: &str) -> bool {
1335        self.values.borrow().contains_key(name)
1336    }
1337    fn get(&self, name: &str) -> Option<RuntimeValue> {
1338        self.values
1339            .borrow()
1340            .get(name)
1341            .cloned()
1342            .or_else(|| self.parent.as_ref().and_then(|parent| parent.get(name)))
1343    }
1344    fn set(&self, name: &str, value: RuntimeValue) {
1345        if self.values.borrow().contains_key(name) {
1346            self.values.borrow_mut().insert(name.to_string(), value);
1347        } else if let Some(parent) = &self.parent {
1348            parent.set(name, value);
1349        } else {
1350            self.values.borrow_mut().insert(name.to_string(), value);
1351        }
1352    }
1353}
1354
1355struct Frame {
1356    chunk: Arc<Chunk>,
1357    ip: usize,
1358    stack: Vec<RuntimeValue>,
1359    locals: Vec<Option<RuntimeValue>>,
1360    env: Rc<Env>,
1361    handlers: Vec<Handler>,
1362    argc: usize,
1363}
1364impl Frame {
1365    fn new(chunk: Arc<Chunk>, env: Rc<Env>, arguments: Vec<RuntimeValue>) -> Self {
1366        let argc = arguments.len();
1367        let mut locals = vec![None; chunk.local_slots.len()];
1368        for (index, value) in arguments.into_iter().enumerate().take(locals.len()) {
1369            locals[index] = Some(value);
1370        }
1371        Self {
1372            chunk,
1373            ip: 0,
1374            stack: Vec::new(),
1375            locals,
1376            env,
1377            handlers: Vec::new(),
1378            argc,
1379        }
1380    }
1381
1382    fn for_function(
1383        function: &CompiledFunction,
1384        env: Rc<Env>,
1385        mut arguments: Vec<RuntimeValue>,
1386    ) -> Self {
1387        let supplied = arguments.len();
1388        if function.has_rest_param && !function.params.is_empty() {
1389            let rest_index = function.params.len() - 1;
1390            let rest = if arguments.len() > rest_index {
1391                arguments.split_off(rest_index)
1392            } else {
1393                Vec::new()
1394            };
1395            arguments.push(RuntimeValue::List(Rc::new(rest)));
1396        } else {
1397            arguments.truncate(function.params.len());
1398        }
1399        let mut frame = Self::new(function.chunk.clone(), env, arguments);
1400        for (parameter, value) in function.params.iter().zip(frame.locals.iter()) {
1401            if let Some(value) = value {
1402                frame.env.define(parameter.name.clone(), value.clone());
1403            }
1404        }
1405        frame.argc = supplied;
1406        frame
1407    }
1408}
1409struct Handler {
1410    target: usize,
1411    stack_depth: usize,
1412    env: Rc<Env>,
1413    preserve: bool,
1414}
1415enum Step {
1416    Value(RuntimeValue),
1417    Suspend(CapabilityRequest),
1418    Error(Diagnostic),
1419}
1420enum OpStep {
1421    Continue,
1422    Push(RuntimeValue),
1423    Call(Closure, Vec<RuntimeValue>, bool),
1424    Return(RuntimeValue),
1425    Suspend(CapabilityRequest),
1426    Throw(RuntimeException),
1427    Error(Diagnostic),
1428}
1429
1430fn read_constant_string(frame: &mut Frame) -> Result<String, OpStep> {
1431    let index = read_u16(frame)?;
1432    match frame.chunk.constants.get(index) {
1433        Some(Constant::String(value)) => Ok(value.clone()),
1434        _ => Err(invalid_index("string constant", index)),
1435    }
1436}
1437fn pop_args(frame: &mut Frame, count: usize) -> Result<Vec<RuntimeValue>, OpStep> {
1438    if frame.stack.len() < count {
1439        return Err(OpStep::Error(diagnostic(
1440            "stack_underflow",
1441            "call argument stack is truncated",
1442        )));
1443    }
1444    Ok(frame.stack.split_off(frame.stack.len() - count))
1445}
1446fn invalid_index(kind: &str, index: usize) -> OpStep {
1447    OpStep::Error(diagnostic(
1448        "invalid_index",
1449        format!("{kind} index {index} is out of bounds"),
1450    ))
1451}
1452
1453fn call_value(
1454    machine: &mut Machine<'_>,
1455    env: &Rc<Env>,
1456    callee: RuntimeValue,
1457    args: Vec<RuntimeValue>,
1458    tail: bool,
1459) -> OpStep {
1460    match callee {
1461        RuntimeValue::Closure(closure) => OpStep::Call(closure, args, tail),
1462        RuntimeValue::Builtin(name) => machine.call_builtin(&name, args),
1463        // Optimized named tail calls carry the source name as a string. Match
1464        // the native VM's lexical-first late binding so recursion, mutual
1465        // recursion, and sibling calls all retain one compiler representation.
1466        RuntimeValue::String(name) => call_named(machine, env, &name, args, tail),
1467        RuntimeValue::Harness(capability) => {
1468            machine.call_method(RuntimeValue::Harness(capability), "call", args)
1469        }
1470        other => OpStep::Error(diagnostic(
1471            "not_callable",
1472            format!("{} is not callable", runtime_value_kind(&other)),
1473        )),
1474    }
1475}
1476
1477fn call_named(
1478    machine: &mut Machine<'_>,
1479    env: &Rc<Env>,
1480    name: &str,
1481    args: Vec<RuntimeValue>,
1482    tail: bool,
1483) -> OpStep {
1484    match env.get(name) {
1485        Some(callee) => call_value(machine, env, callee, args, tail),
1486        None => machine.call_builtin(name, args),
1487    }
1488}