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, 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(value) => {
589                    if !handle_throw(frames, value.clone()) {
590                        let message = match self.render_value(&value) {
591                            Ok(message) => message,
592                            Err(diagnostic) => return Step::Error(diagnostic),
593                        };
594                        return Step::Error(diagnostic("harn_throw", message));
595                    }
596                }
597                OpStep::Error(error) => return Step::Error(error),
598            }
599            if frames
600                .last()
601                .is_some_and(|frame| frame.stack.len() > MAX_OPERAND_STACK)
602            {
603                return Step::Error(diagnostic(
604                    "operand_stack_limit",
605                    "portable execution exceeded its operand stack limit",
606                ));
607            }
608        }
609    }
610
611    #[allow(clippy::too_many_lines)]
612    fn execute_op(
613        &mut self,
614        op: Op,
615        offset: usize,
616        frames: &mut [Frame],
617    ) -> Result<OpStep, OpStep> {
618        let frame = frames.last_mut().expect("active frame");
619        macro_rules! pop {
620            () => {
621                match frame.stack.pop() {
622                    Some(value) => value,
623                    None => {
624                        return Err(OpStep::Error(diagnostic(
625                            "stack_underflow",
626                            format!("{} at {offset}", op.name()),
627                        )))
628                    }
629                }
630            };
631        }
632        match op {
633            Op::Constant => {
634                let index = read_u16(frame)?;
635                let Some(value) = frame.chunk.constants.get(index).cloned() else {
636                    return Err(invalid_index("constant", index));
637                };
638                frame.stack.push(RuntimeValue::from(value));
639            }
640            Op::Nil => frame.stack.push(RuntimeValue::Nil),
641            Op::True => frame.stack.push(RuntimeValue::Bool(true)),
642            Op::False => frame.stack.push(RuntimeValue::Bool(false)),
643            Op::RootHarness => frame.stack.push(RuntimeValue::Harness("root".to_string())),
644            Op::GetVar => {
645                let name = read_constant_string(frame)?;
646                frame.stack.push(
647                    frame
648                        .env
649                        .get(&name)
650                        .unwrap_or_else(|| RuntimeValue::Builtin(name)),
651                );
652            }
653            Op::DefLet | Op::DefVar | Op::DefCell => {
654                let name = read_constant_string(frame)?;
655                let value = pop!();
656                frame.env.define(name, value);
657            }
658            Op::SetVar => {
659                let name = read_constant_string(frame)?;
660                let value = pop!();
661                frame.env.set(&name, value);
662            }
663            Op::PushScope => {
664                frame.env = self.child_env(frame.env.clone()).map_err(OpStep::Error)?;
665            }
666            Op::PopScope => {
667                if let Some(parent) = &frame.env.parent {
668                    frame.env = parent.clone();
669                }
670            }
671            Op::GetLocalSlot => {
672                let slot = read_u16(frame)?;
673                let Some(value) = frame.locals.get(slot).and_then(Clone::clone) else {
674                    return Err(invalid_index("local", slot));
675                };
676                frame.stack.push(value);
677            }
678            Op::DefLocalSlot | Op::SetLocalSlot => {
679                let slot = read_u16(frame)?;
680                let value = pop!();
681                if slot >= frame.locals.len() {
682                    return Err(invalid_index("local", slot));
683                }
684                frame.locals[slot] = Some(value.clone());
685                if let Some(local) = frame.chunk.local_slots.get(slot) {
686                    if op == Op::DefLocalSlot {
687                        frame.env.define(local.name.clone(), value);
688                    } else {
689                        frame.env.set(&local.name, value);
690                    }
691                }
692            }
693            Op::ConcatAssignLocal => {
694                let slot = read_u16(frame)?;
695                let rhs = pop!();
696                let Some(local) = frame.chunk.local_slots.get(slot) else {
697                    return Err(invalid_index("local", slot));
698                };
699                if !local.mutable {
700                    return Err(OpStep::Error(diagnostic(
701                        "immutable_assignment",
702                        format!("cannot assign to immutable binding `{}`", local.name),
703                    )));
704                }
705                let Some(lhs) = frame.locals.get(slot).and_then(Clone::clone) else {
706                    return Err(invalid_index("local", slot));
707                };
708                let value = add(lhs, rhs).map_err(OpStep::Error)?;
709                self.charge_value_work(&value).map_err(OpStep::Error)?;
710                frame.locals[slot] = Some(value.clone());
711                frame.env.set(&local.name, value);
712            }
713            Op::GetArgc => frame.stack.push(RuntimeValue::Int(frame.argc as i64)),
714            Op::Pop => {
715                pop!();
716            }
717            Op::Dup => {
718                let value = pop!();
719                frame.stack.push(value.clone());
720                frame.stack.push(value);
721            }
722            Op::Swap => {
723                let right = pop!();
724                let left = pop!();
725                frame.stack.push(right);
726                frame.stack.push(left);
727            }
728            Op::Add | Op::AddInt | Op::AddFloat => {
729                let value = binary(frame, add)?;
730                self.charge_value_work(&value).map_err(OpStep::Error)?;
731                frame.stack.push(value);
732            }
733            Op::Sub | Op::SubInt | Op::SubFloat => {
734                let value = binary(frame, sub)?;
735                self.charge_value_work(&value).map_err(OpStep::Error)?;
736                frame.stack.push(value);
737            }
738            Op::Mul | Op::MulInt | Op::MulFloat => {
739                let value = binary(frame, mul)?;
740                self.charge_value_work(&value).map_err(OpStep::Error)?;
741                frame.stack.push(value);
742            }
743            Op::Div | Op::DivInt | Op::DivFloat => {
744                let value = binary(frame, div)?;
745                self.charge_value_work(&value).map_err(OpStep::Error)?;
746                frame.stack.push(value);
747            }
748            Op::Mod | Op::ModInt | Op::ModFloat => {
749                let value = binary(frame, modulo)?;
750                self.charge_value_work(&value).map_err(OpStep::Error)?;
751                frame.stack.push(value);
752            }
753            Op::Pow => {
754                let value = binary(frame, pow)?;
755                self.charge_value_work(&value).map_err(OpStep::Error)?;
756                frame.stack.push(value);
757            }
758            Op::Negate => {
759                let value = pop!();
760                frame.stack.push(negate(value).map_err(OpStep::Error)?);
761            }
762            Op::Not => {
763                let value = pop!();
764                frame.stack.push(RuntimeValue::Bool(!value.truthy()));
765            }
766            Op::Equal | Op::EqualInt | Op::EqualFloat | Op::EqualBool | Op::EqualString => {
767                compare_equality(self, frame, true)?;
768            }
769            Op::NotEqual
770            | Op::NotEqualInt
771            | Op::NotEqualFloat
772            | Op::NotEqualBool
773            | Op::NotEqualString => compare_equality(self, frame, false)?,
774            Op::Less | Op::LessInt | Op::LessFloat => compare(self, frame, |value| value < 0)?,
775            Op::Greater | Op::GreaterInt | Op::GreaterFloat => {
776                compare(self, frame, |value| value > 0)?;
777            }
778            Op::LessEqual | Op::LessEqualInt | Op::LessEqualFloat => {
779                compare(self, frame, |value| value <= 0)?;
780            }
781            Op::GreaterEqual | Op::GreaterEqualInt | Op::GreaterEqualFloat => {
782                compare(self, frame, |value| value >= 0)?;
783            }
784            Op::Jump => frame.ip = read_u16(frame)?,
785            Op::JumpIfFalse => {
786                let target = read_u16(frame)?;
787                if !frame.stack.last().is_some_and(RuntimeValue::truthy) {
788                    frame.ip = target;
789                }
790            }
791            Op::JumpIfTrue => {
792                let target = read_u16(frame)?;
793                if frame.stack.last().is_some_and(RuntimeValue::truthy) {
794                    frame.ip = target;
795                }
796            }
797            Op::Closure => {
798                let index = read_u16(frame)?;
799                let Some(function) = frame.chunk.functions.get(index).cloned() else {
800                    return Err(invalid_index("function", index));
801                };
802                self.retain_environment(&frame.env);
803                frame.stack.push(RuntimeValue::Closure(Closure {
804                    function,
805                    env: Rc::downgrade(&frame.env),
806                }));
807            }
808            Op::Call | Op::TailCall => {
809                let argc = read_u8(frame)?;
810                let args = pop_args(frame, argc)?;
811                let callee = pop!();
812                return Ok(call_value(
813                    self,
814                    &frame.env,
815                    callee,
816                    args,
817                    op == Op::TailCall,
818                ));
819            }
820            Op::Return => {
821                return Ok(OpStep::Return(
822                    frame.stack.pop().unwrap_or(RuntimeValue::Nil),
823                ))
824            }
825            Op::BuildList => {
826                let count = read_u16(frame)?;
827                let values = pop_args(frame, count)?;
828                let value = RuntimeValue::List(Rc::new(values));
829                self.charge_value_work(&value).map_err(OpStep::Error)?;
830                frame.stack.push(value);
831            }
832            Op::BuildDict => {
833                let count = read_u16(frame)?;
834                let values = pop_args(frame, count * 2)?;
835                let mut map = BTreeMap::new();
836                for pair in values.chunks_exact(2) {
837                    let key = self.render_value(&pair[0]).map_err(OpStep::Error)?;
838                    map.insert(key, pair[1].clone());
839                }
840                let value = RuntimeValue::Record(Rc::new(map));
841                self.charge_value_work(&value).map_err(OpStep::Error)?;
842                frame.stack.push(value);
843            }
844            Op::GetProperty | Op::GetPropertyOpt => {
845                let name = read_constant_string(frame)?;
846                let value = pop!();
847                match get_property(&value, &name) {
848                    Some(value) => frame.stack.push(value),
849                    None if op == Op::GetPropertyOpt => frame.stack.push(RuntimeValue::Nil),
850                    None => {
851                        return Err(OpStep::Error(diagnostic(
852                            "missing_property",
853                            format!("value has no property `{name}`"),
854                        )))
855                    }
856                }
857            }
858            Op::Subscript | Op::SubscriptOpt => {
859                let index = pop!();
860                let value = pop!();
861                match self.subscript(&value, &index).map_err(OpStep::Error)? {
862                    Some(value) => frame.stack.push(value),
863                    None if op == Op::SubscriptOpt => frame.stack.push(RuntimeValue::Nil),
864                    None => {
865                        return Err(OpStep::Error(diagnostic(
866                            "subscript",
867                            "subscript does not exist",
868                        )))
869                    }
870                }
871            }
872            Op::SetProperty => {
873                let property = read_constant_string(frame)?;
874                let binding = read_constant_string(frame)?;
875                let value = pop!();
876                let Some(target) = frame.env.get(&binding) else {
877                    return Err(OpStep::Error(diagnostic(
878                        "undefined_variable",
879                        format!("cannot assign property on undefined binding `{binding}`"),
880                    )));
881                };
882                let updated =
883                    set_property_value(target, &property, value).map_err(OpStep::Error)?;
884                self.charge_value_work(&updated).map_err(OpStep::Error)?;
885                frame.env.set(&binding, updated);
886            }
887            Op::SetSubscript => {
888                let binding = read_constant_string(frame)?;
889                let index = pop!();
890                let value = pop!();
891                let Some(target) = frame.env.get(&binding) else {
892                    return Err(OpStep::Error(diagnostic(
893                        "undefined_variable",
894                        format!("cannot assign subscript on undefined binding `{binding}`"),
895                    )));
896                };
897                let updated = set_subscript_value(target, index, value).map_err(OpStep::Error)?;
898                self.charge_value_work(&updated).map_err(OpStep::Error)?;
899                frame.env.set(&binding, updated);
900            }
901            Op::SetLocalSlotProperty => {
902                let property = read_constant_string(frame)?;
903                let slot = read_u16(frame)?;
904                let value = pop!();
905                let Some(local) = frame.chunk.local_slots.get(slot) else {
906                    return Err(invalid_index("local", slot));
907                };
908                if !local.mutable {
909                    return Err(OpStep::Error(diagnostic(
910                        "immutable_assignment",
911                        format!("cannot assign to immutable binding `{}`", local.name),
912                    )));
913                }
914                let Some(target) = frame.locals.get(slot).and_then(Clone::clone) else {
915                    return Err(invalid_index("local", slot));
916                };
917                let updated =
918                    set_property_value(target, &property, value).map_err(OpStep::Error)?;
919                self.charge_value_work(&updated).map_err(OpStep::Error)?;
920                frame.locals[slot] = Some(updated.clone());
921                frame.env.set(&local.name, updated);
922            }
923            Op::SetLocalSlotSubscript => {
924                let slot = read_u16(frame)?;
925                let index = pop!();
926                let value = pop!();
927                let Some(local) = frame.chunk.local_slots.get(slot) else {
928                    return Err(invalid_index("local", slot));
929                };
930                if !local.mutable {
931                    return Err(OpStep::Error(diagnostic(
932                        "immutable_assignment",
933                        format!("cannot assign to immutable binding `{}`", local.name),
934                    )));
935                }
936                let Some(target) = frame.locals.get(slot).and_then(Clone::clone) else {
937                    return Err(invalid_index("local", slot));
938                };
939                let updated = set_subscript_value(target, index, value).map_err(OpStep::Error)?;
940                self.charge_value_work(&updated).map_err(OpStep::Error)?;
941                frame.locals[slot] = Some(updated.clone());
942                frame.env.set(&local.name, updated);
943            }
944            Op::Slice => {
945                let end = pop!();
946                let start = pop!();
947                let value = pop!();
948                let value = slice(value, start, end).map_err(OpStep::Error)?;
949                self.charge_value_work(&value).map_err(OpStep::Error)?;
950                frame.stack.push(value);
951            }
952            Op::MethodCall | Op::MethodCallOpt => {
953                let name = read_constant_string(frame)?;
954                let argc = read_u8(frame)?;
955                let args = pop_args(frame, argc)?;
956                let receiver = pop!();
957                if op == Op::MethodCallOpt && matches!(receiver, RuntimeValue::Nil) {
958                    frame.stack.push(RuntimeValue::Nil);
959                } else {
960                    return Ok(self.call_method(receiver, &name, args));
961                }
962            }
963            Op::Concat => {
964                let count = read_u16(frame)?;
965                let values = pop_args(frame, count)?;
966                let mut rendered = String::new();
967                for value in &values {
968                    let part = self.render_value(value).map_err(OpStep::Error)?;
969                    if rendered.len().saturating_add(part.len()) > MAX_VALUE_BYTES {
970                        return Err(OpStep::Error(diagnostic(
971                            "value_byte_limit",
972                            "string interpolation exceeds the portable value byte limit",
973                        )));
974                    }
975                    rendered.push_str(&part);
976                }
977                frame.stack.push(RuntimeValue::String(Arc::from(rendered)));
978            }
979            Op::Contains => {
980                let container = pop!();
981                let item = pop!();
982                let found = self.contains(&container, &item).map_err(OpStep::Error)?;
983                frame.stack.push(RuntimeValue::Bool(found));
984            }
985            Op::IterInit => {
986                if self.iterators.len() >= MAX_ITERATORS {
987                    return Err(OpStep::Error(diagnostic(
988                        "iterator_limit",
989                        "portable execution exceeded its iterator limit",
990                    )));
991                }
992                let iterable = pop!();
993                let iterator = match iterable {
994                    RuntimeValue::List(values) => IteratorState::List { values, index: 0 },
995                    RuntimeValue::Record(values) => IteratorState::Record {
996                        keys: values.keys().cloned().collect(),
997                        values,
998                        index: 0,
999                    },
1000                    other => {
1001                        return Err(OpStep::Error(diagnostic(
1002                            "iterator_type",
1003                            format!(
1004                                "cannot iterate over {} in the portable kernel",
1005                                runtime_value_kind(&other)
1006                            ),
1007                        )))
1008                    }
1009                };
1010                self.iterators.push(iterator);
1011            }
1012            Op::IterNext => {
1013                let target = read_u16(frame)?;
1014                let Some(iterator) = self.iterators.last_mut() else {
1015                    return Err(OpStep::Error(diagnostic(
1016                        "iterator_state",
1017                        "iterator step has no active iterator",
1018                    )));
1019                };
1020                match iterator {
1021                    IteratorState::List { values, index } => {
1022                        if let Some(value) = values.get(*index).cloned() {
1023                            *index += 1;
1024                            frame.stack.push(value);
1025                        } else {
1026                            self.iterators.pop();
1027                            frame.ip = target;
1028                        }
1029                    }
1030                    IteratorState::Record {
1031                        keys,
1032                        values,
1033                        index,
1034                    } => {
1035                        if let Some(key) = keys.get(*index) {
1036                            let value = values.get(key).cloned().unwrap_or(RuntimeValue::Nil);
1037                            *index += 1;
1038                            frame
1039                                .stack
1040                                .push(RuntimeValue::Record(Rc::new(BTreeMap::from([
1041                                    (
1042                                        "key".to_string(),
1043                                        RuntimeValue::String(Arc::from(key.as_str())),
1044                                    ),
1045                                    ("value".to_string(), value),
1046                                ]))));
1047                        } else {
1048                            self.iterators.pop();
1049                            frame.ip = target;
1050                        }
1051                    }
1052                }
1053            }
1054            Op::PopIterator => {
1055                self.iterators.pop();
1056            }
1057            Op::TryCatchSetup => {
1058                let target = read_u16(frame)?;
1059                let _type_name = read_u16(frame)?;
1060                frame.handlers.push(Handler {
1061                    target,
1062                    stack_depth: frame.stack.len(),
1063                    env: frame.env.clone(),
1064                });
1065            }
1066            Op::PopHandler => {
1067                frame.handlers.pop();
1068            }
1069            Op::Throw => return Ok(OpStep::Throw(pop!())),
1070            Op::Import => {
1071                let path = read_constant_string(frame)?;
1072                let env = frame.env.clone();
1073                return Ok(self.import_root_module(&path, None, None, &env));
1074            }
1075            Op::SelectiveImport => {
1076                let path = read_constant_string(frame)?;
1077                let names = read_constant_string(frame)?;
1078                let selected = names
1079                    .split(',')
1080                    .filter(|name| !name.is_empty())
1081                    .map(str::to_string)
1082                    .collect::<Vec<_>>();
1083                let env = frame.env.clone();
1084                return Ok(self.import_root_module(&path, Some(&selected), None, &env));
1085            }
1086            Op::NamespaceImport => {
1087                let path = read_constant_string(frame)?;
1088                let alias = read_constant_string(frame)?;
1089                let env = frame.env.clone();
1090                return Ok(self.import_root_module(&path, None, Some((&alias, None)), &env));
1091            }
1092            // The narrowed form the compiler emits when every use of the
1093            // namespace is a statically known member. The projection is the
1094            // demand set rather than the module's whole export surface;
1095            // escaping or dynamic uses compile to `NamespaceImport` instead,
1096            // so a missing member here is a compiler contract violation, not a
1097            // program error.
1098            Op::NamespaceImportMembers => {
1099                let path = read_constant_string(frame)?;
1100                let alias = read_constant_string(frame)?;
1101                let members = read_constant_string(frame)?;
1102                let demanded = members
1103                    .split(',')
1104                    .filter(|name| !name.is_empty())
1105                    .map(str::to_string)
1106                    .collect::<Vec<_>>();
1107                let env = frame.env.clone();
1108                return Ok(self.import_root_module(
1109                    &path,
1110                    None,
1111                    Some((&alias, Some(&demanded))),
1112                    &env,
1113                ));
1114            }
1115            Op::BuildEnum => {
1116                let enum_name = read_constant_string(frame)?;
1117                let variant = read_constant_string(frame)?;
1118                let field_count = read_u16(frame)?;
1119                let fields = pop_args(frame, field_count)?;
1120                let value = RuntimeValue::Enum(Rc::new(EnumValue {
1121                    enum_name: Arc::from(enum_name),
1122                    variant: Arc::from(variant),
1123                    fields: Rc::new(fields),
1124                }));
1125                self.charge_value_work(&value).map_err(OpStep::Error)?;
1126                frame.stack.push(value);
1127            }
1128            Op::MatchEnum => {
1129                let enum_name = read_constant_string(frame)?;
1130                let variant = read_constant_string(frame)?;
1131                let value = pop!();
1132                let matches = matches!(
1133                    &value,
1134                    RuntimeValue::Enum(candidate)
1135                        if candidate.is_variant(&enum_name, &variant)
1136                );
1137                frame.stack.push(value);
1138                frame.stack.push(RuntimeValue::Bool(matches));
1139            }
1140            Op::TryWrapOk => {
1141                let value = pop!();
1142                if matches!(&value, RuntimeValue::Enum(candidate) if candidate.enum_name.as_ref() == "Result")
1143                {
1144                    frame.stack.push(value);
1145                } else {
1146                    frame.stack.push(RuntimeValue::Enum(Rc::new(EnumValue {
1147                        enum_name: Arc::from("Result"),
1148                        variant: Arc::from("Ok"),
1149                        fields: Rc::new(vec![value]),
1150                    })));
1151                }
1152            }
1153            Op::TryUnwrap => {
1154                let value = pop!();
1155                let RuntimeValue::Enum(result) = &value else {
1156                    return Err(OpStep::Error(diagnostic(
1157                        "try_unwrap_type",
1158                        format!(
1159                            "? operator requires a Result value, got {}",
1160                            runtime_value_kind(&value)
1161                        ),
1162                    )));
1163                };
1164                if result.enum_name.as_ref() != "Result" {
1165                    return Err(OpStep::Error(diagnostic(
1166                        "try_unwrap_type",
1167                        format!(
1168                            "? operator requires a Result value, got {}",
1169                            runtime_value_kind(&value)
1170                        ),
1171                    )));
1172                }
1173                if result.variant.as_ref() == "Ok" {
1174                    frame
1175                        .stack
1176                        .push(result.fields.first().cloned().unwrap_or(RuntimeValue::Nil));
1177                } else {
1178                    return Ok(OpStep::Return(value));
1179                }
1180            }
1181            Op::CheckType => {
1182                return Err(OpStep::Error(diagnostic(
1183                    "unsupported_portable_opcode",
1184                    format!("{} is not implemented by the portable kernel", op.name()),
1185                )))
1186            }
1187            Op::CallBuiltin => {
1188                frame.ip += 8;
1189                let name = read_constant_string(frame)?;
1190                let argc = read_u8(frame)?;
1191                let args = pop_args(frame, argc)?;
1192                return Ok(call_named(self, &frame.env, &name, args, false));
1193            }
1194            Op::CallBuiltinSpread => {
1195                frame.ip += 8;
1196                let name = read_constant_string(frame)?;
1197                let spread = pop!();
1198                let RuntimeValue::List(args) = spread else {
1199                    return Err(OpStep::Error(diagnostic(
1200                        "spread_type",
1201                        "spread call requires a list",
1202                    )));
1203                };
1204                return Ok(call_named(
1205                    self,
1206                    &frame.env,
1207                    &name,
1208                    Rc::unwrap_or_clone(args),
1209                    false,
1210                ));
1211            }
1212            unsupported @ (Op::Pipe
1213            | Op::Parallel
1214            | Op::ParallelMap
1215            | Op::ParallelMapStream
1216            | Op::ParallelSettle
1217            | Op::Spawn
1218            | Op::SyncMutexEnter
1219            | Op::SyncMutexEnterKeyed
1220            | Op::TaskScopeEnter
1221            | Op::TaskScopeExit
1222            | Op::DeadlineSetup
1223            | Op::DeadlineEnd
1224            | Op::CallSpread
1225            | Op::MethodCallSpread
1226            | Op::Yield) => {
1227                return Err(OpStep::Error(diagnostic(
1228                    "unsupported_portable_opcode",
1229                    format!(
1230                        "{} is not implemented by the portable kernel",
1231                        unsupported.name()
1232                    ),
1233                )))
1234            }
1235        }
1236        Ok(OpStep::Continue)
1237    }
1238}
1239
1240struct Env {
1241    values: RefCell<BTreeMap<String, RuntimeValue>>,
1242    parent: Option<Rc<Env>>,
1243    depth: usize,
1244}
1245
1246struct ModuleInstance {
1247    env: Rc<Env>,
1248    exports: BTreeMap<String, crate::PortableExportKind>,
1249}
1250
1251enum IteratorState {
1252    List {
1253        values: Rc<Vec<RuntimeValue>>,
1254        index: usize,
1255    },
1256    Record {
1257        values: Rc<BTreeMap<String, RuntimeValue>>,
1258        keys: Vec<String>,
1259        index: usize,
1260    },
1261}
1262
1263enum ModuleStep {
1264    Ready(Rc<ModuleInstance>),
1265    Suspend(CapabilityRequest),
1266    Error(Diagnostic),
1267}
1268
1269/// A namespace import's alias and, when the compiler proved the whole set of
1270/// members a module actually demands, that demand set. `None` members means
1271/// the namespace escaped static analysis and must be projected whole.
1272type NamespaceProjection<'a> = (&'a str, Option<&'a [String]>);
1273
1274impl Env {
1275    fn root() -> Rc<Self> {
1276        Rc::new(Self {
1277            values: RefCell::new(BTreeMap::new()),
1278            parent: None,
1279            depth: 0,
1280        })
1281    }
1282    fn child(parent: Rc<Self>) -> Result<Rc<Self>, Diagnostic> {
1283        if parent.depth >= MAX_SCOPE_DEPTH {
1284            return Err(diagnostic(
1285                "scope_depth_limit",
1286                "portable execution exceeded its lexical scope depth limit",
1287            ));
1288        }
1289        let depth = parent.depth + 1;
1290        Ok(Rc::new(Self {
1291            values: RefCell::new(BTreeMap::new()),
1292            parent: Some(parent),
1293            depth,
1294        }))
1295    }
1296    fn define(&self, name: String, value: RuntimeValue) {
1297        self.values.borrow_mut().insert(name, value);
1298    }
1299    fn contains_local(&self, name: &str) -> bool {
1300        self.values.borrow().contains_key(name)
1301    }
1302    fn get(&self, name: &str) -> Option<RuntimeValue> {
1303        self.values
1304            .borrow()
1305            .get(name)
1306            .cloned()
1307            .or_else(|| self.parent.as_ref().and_then(|parent| parent.get(name)))
1308    }
1309    fn set(&self, name: &str, value: RuntimeValue) {
1310        if self.values.borrow().contains_key(name) {
1311            self.values.borrow_mut().insert(name.to_string(), value);
1312        } else if let Some(parent) = &self.parent {
1313            parent.set(name, value);
1314        } else {
1315            self.values.borrow_mut().insert(name.to_string(), value);
1316        }
1317    }
1318}
1319
1320struct Frame {
1321    chunk: Arc<Chunk>,
1322    ip: usize,
1323    stack: Vec<RuntimeValue>,
1324    locals: Vec<Option<RuntimeValue>>,
1325    env: Rc<Env>,
1326    handlers: Vec<Handler>,
1327    argc: usize,
1328}
1329impl Frame {
1330    fn new(chunk: Arc<Chunk>, env: Rc<Env>, arguments: Vec<RuntimeValue>) -> Self {
1331        let argc = arguments.len();
1332        let mut locals = vec![None; chunk.local_slots.len()];
1333        for (index, value) in arguments.into_iter().enumerate().take(locals.len()) {
1334            locals[index] = Some(value);
1335        }
1336        Self {
1337            chunk,
1338            ip: 0,
1339            stack: Vec::new(),
1340            locals,
1341            env,
1342            handlers: Vec::new(),
1343            argc,
1344        }
1345    }
1346
1347    fn for_function(
1348        function: &CompiledFunction,
1349        env: Rc<Env>,
1350        mut arguments: Vec<RuntimeValue>,
1351    ) -> Self {
1352        let supplied = arguments.len();
1353        if function.has_rest_param && !function.params.is_empty() {
1354            let rest_index = function.params.len() - 1;
1355            let rest = if arguments.len() > rest_index {
1356                arguments.split_off(rest_index)
1357            } else {
1358                Vec::new()
1359            };
1360            arguments.push(RuntimeValue::List(Rc::new(rest)));
1361        } else {
1362            arguments.truncate(function.params.len());
1363        }
1364        let mut frame = Self::new(function.chunk.clone(), env, arguments);
1365        for (parameter, value) in function.params.iter().zip(frame.locals.iter()) {
1366            if let Some(value) = value {
1367                frame.env.define(parameter.name.clone(), value.clone());
1368            }
1369        }
1370        frame.argc = supplied;
1371        frame
1372    }
1373}
1374struct Handler {
1375    target: usize,
1376    stack_depth: usize,
1377    env: Rc<Env>,
1378}
1379enum Step {
1380    Value(RuntimeValue),
1381    Suspend(CapabilityRequest),
1382    Error(Diagnostic),
1383}
1384enum OpStep {
1385    Continue,
1386    Push(RuntimeValue),
1387    Call(Closure, Vec<RuntimeValue>, bool),
1388    Return(RuntimeValue),
1389    Suspend(CapabilityRequest),
1390    Throw(RuntimeValue),
1391    Error(Diagnostic),
1392}
1393
1394fn read_u8(frame: &mut Frame) -> Result<usize, OpStep> {
1395    let value = *frame.chunk.code.get(frame.ip).ok_or_else(|| {
1396        OpStep::Error(diagnostic(
1397            "truncated_instruction",
1398            "u8 operand is truncated",
1399        ))
1400    })?;
1401    frame.ip += 1;
1402    Ok(value as usize)
1403}
1404fn read_u16(frame: &mut Frame) -> Result<usize, OpStep> {
1405    let bytes = frame
1406        .chunk
1407        .code
1408        .get(frame.ip..frame.ip + 2)
1409        .ok_or_else(|| {
1410            OpStep::Error(diagnostic(
1411                "truncated_instruction",
1412                "u16 operand is truncated",
1413            ))
1414        })?;
1415    frame.ip += 2;
1416    Ok(u16::from_be_bytes([bytes[0], bytes[1]]) as usize)
1417}
1418fn read_constant_string(frame: &mut Frame) -> Result<String, OpStep> {
1419    let index = read_u16(frame)?;
1420    match frame.chunk.constants.get(index) {
1421        Some(Constant::String(value)) => Ok(value.clone()),
1422        _ => Err(invalid_index("string constant", index)),
1423    }
1424}
1425fn pop_args(frame: &mut Frame, count: usize) -> Result<Vec<RuntimeValue>, OpStep> {
1426    if frame.stack.len() < count {
1427        return Err(OpStep::Error(diagnostic(
1428            "stack_underflow",
1429            "call argument stack is truncated",
1430        )));
1431    }
1432    Ok(frame.stack.split_off(frame.stack.len() - count))
1433}
1434fn invalid_index(kind: &str, index: usize) -> OpStep {
1435    OpStep::Error(diagnostic(
1436        "invalid_index",
1437        format!("{kind} index {index} is out of bounds"),
1438    ))
1439}
1440
1441fn call_value(
1442    machine: &mut Machine<'_>,
1443    env: &Rc<Env>,
1444    callee: RuntimeValue,
1445    args: Vec<RuntimeValue>,
1446    tail: bool,
1447) -> OpStep {
1448    match callee {
1449        RuntimeValue::Closure(closure) => OpStep::Call(closure, args, tail),
1450        RuntimeValue::Builtin(name) => machine.call_builtin(&name, args),
1451        // Optimized named tail calls carry the source name as a string. Match
1452        // the native VM's lexical-first late binding so recursion, mutual
1453        // recursion, and sibling calls all retain one compiler representation.
1454        RuntimeValue::String(name) => call_named(machine, env, &name, args, tail),
1455        RuntimeValue::Harness(capability) => {
1456            machine.call_method(RuntimeValue::Harness(capability), "call", args)
1457        }
1458        other => OpStep::Error(diagnostic(
1459            "not_callable",
1460            format!("{} is not callable", runtime_value_kind(&other)),
1461        )),
1462    }
1463}
1464
1465fn call_named(
1466    machine: &mut Machine<'_>,
1467    env: &Rc<Env>,
1468    name: &str,
1469    args: Vec<RuntimeValue>,
1470    tail: bool,
1471) -> OpStep {
1472    match env.get(name) {
1473        Some(callee) => call_value(machine, env, callee, args, tail),
1474        None => machine.call_builtin(name, args),
1475    }
1476}