Skip to main content

aver/interpreter/
eval.rs

1use super::lowered::{
2    self, ExprId, LoweredDirectCallTarget, LoweredExpr, LoweredForwardArg, LoweredFunctionBody,
3    LoweredLeafOp, LoweredMatchArm, LoweredStmt, LoweredStrPart, LoweredTailCallTarget,
4};
5use super::*;
6
7type SharedExprs = Rc<[ExprId]>;
8type SharedStrParts = Rc<[LoweredStrPart]>;
9type SharedMapEntries = Rc<[(ExprId, ExprId)]>;
10type SharedRecordFields = Rc<[(String, ExprId)]>;
11type SharedMatchArms = Rc<[LoweredMatchArm]>;
12
13#[derive(Debug)]
14enum EvalState {
15    Expr {
16        lowered: Rc<LoweredFunctionBody>,
17        expr: ExprId,
18    },
19    Body {
20        lowered: Rc<LoweredFunctionBody>,
21        idx: usize,
22        local_slots: Option<Rc<HashMap<String, u16>>>,
23        last: NanValue,
24    },
25    Apply(Result<NanValue, RuntimeError>),
26}
27
28#[derive(Debug, Clone)]
29enum EvalCont {
30    Attr(String),
31    Call {
32        lowered: Rc<LoweredFunctionBody>,
33        args: SharedExprs,
34        idx: usize,
35        fn_val: Option<NanValue>,
36        arg_vals: Vec<NanValue>,
37    },
38    BinOpLeft {
39        lowered: Rc<LoweredFunctionBody>,
40        op: BinOp,
41        right: ExprId,
42    },
43    BinOpRight {
44        op: BinOp,
45        left: NanValue,
46    },
47    Match {
48        lowered: Rc<LoweredFunctionBody>,
49        arms: SharedMatchArms,
50        line: usize,
51    },
52    DirectCall {
53        lowered: Rc<LoweredFunctionBody>,
54        target: LoweredDirectCallTarget,
55        idx: usize,
56        args: SharedExprs,
57        arg_vals: Vec<NanValue>,
58    },
59    Leaf {
60        lowered: Rc<LoweredFunctionBody>,
61        leaf: LoweredLeafOp,
62        idx: usize,
63        values: Vec<NanValue>,
64    },
65    Constructor(String),
66    ErrorProp,
67    InterpolatedStr {
68        lowered: Rc<LoweredFunctionBody>,
69        parts: SharedStrParts,
70        idx: usize,
71        result: String,
72    },
73    List {
74        lowered: Rc<LoweredFunctionBody>,
75        items: SharedExprs,
76        idx: usize,
77        values: Vec<NanValue>,
78    },
79    Tuple {
80        lowered: Rc<LoweredFunctionBody>,
81        items: SharedExprs,
82        idx: usize,
83        values: Vec<NanValue>,
84    },
85    MapKey {
86        lowered: Rc<LoweredFunctionBody>,
87        entries: SharedMapEntries,
88        idx: usize,
89        map: crate::nan_value::PersistentMap,
90    },
91    MapValue {
92        lowered: Rc<LoweredFunctionBody>,
93        entries: SharedMapEntries,
94        idx: usize,
95        map: crate::nan_value::PersistentMap,
96        key: NanValue,
97    },
98    RecordCreate(RecordCreateProgress),
99    RecordUpdateBase {
100        lowered: Rc<LoweredFunctionBody>,
101        type_name: String,
102        updates: SharedRecordFields,
103    },
104    RecordUpdateField(RecordUpdateProgress),
105    TailCallArgs {
106        lowered: Rc<LoweredFunctionBody>,
107        target: LoweredTailCallTarget,
108        args: SharedExprs,
109        idx: usize,
110        values: Vec<NanValue>,
111    },
112    BodyBinding {
113        name: String,
114        next_idx: usize,
115        lowered: Rc<LoweredFunctionBody>,
116        local_slots: Option<Rc<HashMap<String, u16>>>,
117    },
118    BodyExpr {
119        next_idx: usize,
120        lowered: Rc<LoweredFunctionBody>,
121        local_slots: Option<Rc<HashMap<String, u16>>>,
122    },
123    MatchScope,
124    FunctionReturn(FunctionFrame),
125}
126
127#[derive(Debug, Clone)]
128struct ActiveFunction {
129    function: Rc<crate::value::FunctionValue>,
130}
131
132#[derive(Debug, Clone)]
133struct FunctionFrame {
134    active: ActiveFunction,
135    prev_local_slots: Option<Rc<HashMap<String, u16>>>,
136    /// The env_base of the caller — restored on return so the caller's
137    /// frames become visible to lookup_ref again.
138    saved_base: usize,
139    prev_global: Option<EnvFrame>,
140    memo_key: Option<(u64, Vec<NanValue>)>,
141}
142
143#[derive(Debug, Clone)]
144struct RecordCreateProgress {
145    lowered: Rc<LoweredFunctionBody>,
146    type_name: String,
147    fields: SharedRecordFields,
148    idx: usize,
149    seen: HashSet<String>,
150    values: Vec<(String, NanValue)>,
151}
152
153#[derive(Debug, Clone)]
154struct RecordUpdateProgress {
155    lowered: Rc<LoweredFunctionBody>,
156    type_name: String,
157    base_type_id: u32,
158    base_fields: Vec<NanValue>,
159    base_field_names: Vec<String>,
160    updates: SharedRecordFields,
161    idx: usize,
162    update_vals: Vec<(String, NanValue)>,
163}
164
165enum CallDispatch {
166    Immediate(Result<NanValue, RuntimeError>),
167    EnterFunction {
168        frame: Box<FunctionFrame>,
169        state: EvalState,
170    },
171}
172
173impl Interpreter {
174    fn empty_slots_nv(local_count: u16) -> Vec<NanValue> {
175        vec![NanValue::UNIT; local_count as usize]
176    }
177
178    /// Public eval_expr — returns Value for external callers.
179    pub fn eval_expr(&mut self, expr: &Expr) -> Result<Value, RuntimeError> {
180        let nv = self.eval_expr_nv(expr)?;
181        Ok(nv.to_value(&self.arena))
182    }
183
184    /// Internal eval_expr — returns NanValue natively.
185    pub(super) fn eval_expr_nv(&mut self, expr: &Expr) -> Result<NanValue, RuntimeError> {
186        let ctx = super::ir_bridge::InterpreterLowerCtx::new(self);
187        let (lowered, root) = lowered::lower_expr_root(expr, &ctx);
188        self.eval_loop(
189            EvalState::Expr {
190                lowered,
191                expr: root,
192            },
193            Vec::new(),
194        )
195    }
196
197    fn eval_loop(
198        &mut self,
199        initial: EvalState,
200        mut conts: Vec<EvalCont>,
201    ) -> Result<NanValue, RuntimeError> {
202        let mut state = initial;
203
204        loop {
205            state = match state {
206                EvalState::Expr { lowered, expr } => self.step_expr(lowered, expr, &mut conts),
207                EvalState::Body {
208                    lowered,
209                    idx,
210                    local_slots,
211                    last,
212                } => self.step_body(lowered, idx, local_slots, last, &mut conts),
213                EvalState::Apply(result) => {
214                    let Some(cont) = conts.pop() else {
215                        return result;
216                    };
217                    self.apply_cont(cont, result, &mut conts)
218                }
219            };
220        }
221    }
222
223    fn step_expr(
224        &mut self,
225        lowered: Rc<LoweredFunctionBody>,
226        expr_id: ExprId,
227        conts: &mut Vec<EvalCont>,
228    ) -> EvalState {
229        match lowered.expr(expr_id) {
230            LoweredExpr::Literal(lit) => EvalState::Apply(Ok(self.eval_literal_nv(lit))),
231            &LoweredExpr::Resolved(slot) => EvalState::Apply(self.lookup_slot(slot)),
232            LoweredExpr::Ident(name) => EvalState::Apply(self.lookup_nv(name)),
233            LoweredExpr::Attr { obj, field } => {
234                let obj = *obj;
235                if let LoweredExpr::Ident(name) = lowered.expr(obj) {
236                    let nv = match self.lookup_nv(name) {
237                        Ok(nv) => nv,
238                        Err(err) => return EvalState::Apply(Err(err)),
239                    };
240                    let result = self.attr_access_nv(nv, field);
241                    return EvalState::Apply(result);
242                }
243
244                let field = field.clone();
245                conts.push(EvalCont::Attr(field));
246                EvalState::Expr { lowered, expr: obj }
247            }
248            LoweredExpr::FnCall { fn_expr, args } => {
249                let fn_expr = *fn_expr;
250                let args = Rc::clone(args);
251                conts.push(EvalCont::Call {
252                    lowered: Rc::clone(&lowered),
253                    idx: 0,
254                    fn_val: None,
255                    arg_vals: Vec::with_capacity(args.len()),
256                    args,
257                });
258                EvalState::Expr {
259                    lowered,
260                    expr: fn_expr,
261                }
262            }
263            LoweredExpr::DirectCall { target, args } => self.resume_direct_call(
264                Rc::clone(&lowered),
265                target.clone(),
266                Rc::clone(args),
267                0,
268                Vec::with_capacity(args.len()),
269                conts,
270            ),
271            LoweredExpr::ForwardCall { target, args } => {
272                self.dispatch_forward_call(target.clone(), args, conts)
273            }
274            &LoweredExpr::BinOp { op, left, right } => {
275                conts.push(EvalCont::BinOpLeft {
276                    lowered: Rc::clone(&lowered),
277                    op,
278                    right,
279                });
280                EvalState::Expr {
281                    lowered,
282                    expr: left,
283                }
284            }
285            LoweredExpr::Match {
286                subject,
287                arms,
288                line,
289            } => {
290                let subject = *subject;
291                let line = *line;
292                conts.push(EvalCont::Match {
293                    lowered: Rc::clone(&lowered),
294                    arms: Rc::clone(arms),
295                    line,
296                });
297                EvalState::Expr {
298                    lowered,
299                    expr: subject,
300                }
301            }
302            LoweredExpr::Leaf(leaf) => {
303                let leaf = leaf.clone();
304                self.resume_leaf(
305                    Rc::clone(&lowered),
306                    leaf.clone(),
307                    0,
308                    Vec::with_capacity(leaf.arity()),
309                    conts,
310                )
311            }
312            LoweredExpr::Constructor { name, arg } => match arg {
313                Some(inner) => {
314                    let inner = *inner;
315                    conts.push(EvalCont::Constructor(name.clone()));
316                    EvalState::Expr {
317                        lowered,
318                        expr: inner,
319                    }
320                }
321                None => EvalState::Apply(self.apply_runtime_constructor_nv(name, None)),
322            },
323            &LoweredExpr::ErrorProp { inner } => {
324                conts.push(EvalCont::ErrorProp);
325                EvalState::Expr {
326                    lowered,
327                    expr: inner,
328                }
329            }
330            LoweredExpr::InterpolatedStr(parts) => {
331                let parts = Rc::clone(parts);
332                self.resume_interpolated_str(lowered, parts, 0, String::new(), conts)
333            }
334            LoweredExpr::List(items) => {
335                let items = Rc::clone(items);
336                self.resume_list(lowered, items, 0, Vec::new(), conts)
337            }
338            LoweredExpr::Tuple(items) => {
339                let cap = items.len();
340                let items = Rc::clone(items);
341                self.resume_tuple(lowered, items, 0, Vec::with_capacity(cap), conts)
342            }
343            LoweredExpr::MapLiteral(entries) => {
344                let entries = Rc::clone(entries);
345                self.resume_map(
346                    lowered,
347                    entries,
348                    0,
349                    crate::nan_value::PersistentMap::new(),
350                    conts,
351                )
352            }
353            LoweredExpr::RecordCreate { type_name, fields } => {
354                let type_name = type_name.clone();
355                let fields = Rc::clone(fields);
356                self.resume_record_create(
357                    RecordCreateProgress {
358                        lowered,
359                        type_name,
360                        fields,
361                        idx: 0,
362                        seen: HashSet::new(),
363                        values: Vec::new(),
364                    },
365                    conts,
366                )
367            }
368            LoweredExpr::RecordUpdate {
369                type_name,
370                base,
371                updates,
372            } => {
373                let base = *base;
374                conts.push(EvalCont::RecordUpdateBase {
375                    lowered: Rc::clone(&lowered),
376                    type_name: type_name.clone(),
377                    updates: Rc::clone(updates),
378                });
379                EvalState::Expr {
380                    lowered,
381                    expr: base,
382                }
383            }
384            LoweredExpr::TailCall { target, args } => {
385                let target = target.clone();
386                let args = Rc::clone(args);
387                self.resume_tail_call(lowered, target, args, 0, Vec::new(), conts)
388            }
389        }
390    }
391
392    fn step_body(
393        &mut self,
394        lowered: Rc<LoweredFunctionBody>,
395        idx: usize,
396        local_slots: Option<Rc<HashMap<String, u16>>>,
397        last: NanValue,
398        conts: &mut Vec<EvalCont>,
399    ) -> EvalState {
400        let Some(stmt) = lowered.stmt(idx) else {
401            return EvalState::Apply(Ok(last));
402        };
403
404        match stmt {
405            LoweredStmt::Binding(name, expr) => {
406                let expr = *expr;
407                conts.push(EvalCont::BodyBinding {
408                    name: name.clone(),
409                    next_idx: idx + 1,
410                    lowered: Rc::clone(&lowered),
411                    local_slots,
412                });
413                EvalState::Expr { lowered, expr }
414            }
415            &LoweredStmt::Expr(expr) => {
416                conts.push(EvalCont::BodyExpr {
417                    next_idx: idx + 1,
418                    lowered: Rc::clone(&lowered),
419                    local_slots,
420                });
421                EvalState::Expr { lowered, expr }
422            }
423        }
424    }
425
426    fn apply_cont(
427        &mut self,
428        cont: EvalCont,
429        result: Result<NanValue, RuntimeError>,
430        conts: &mut Vec<EvalCont>,
431    ) -> EvalState {
432        match cont {
433            EvalCont::Attr(field) => match result {
434                Ok(nv) => EvalState::Apply(self.attr_access_nv(nv, &field)),
435                Err(err) => EvalState::Apply(Err(err)),
436            },
437            EvalCont::Call {
438                lowered,
439                args,
440                mut idx,
441                mut fn_val,
442                mut arg_vals,
443            } => match result {
444                Ok(value) => {
445                    if fn_val.is_none() {
446                        fn_val = Some(value);
447                        if args.is_empty() {
448                            return self.dispatch_call(
449                                fn_val.expect("function value set before dispatch"),
450                                arg_vals,
451                                conts,
452                            );
453                        }
454                        conts.push(EvalCont::Call {
455                            lowered: Rc::clone(&lowered),
456                            args: Rc::clone(&args),
457                            idx,
458                            fn_val,
459                            arg_vals,
460                        });
461                        return EvalState::Expr {
462                            lowered,
463                            expr: args[idx],
464                        };
465                    }
466
467                    arg_vals.push(value);
468                    idx += 1;
469                    if idx < args.len() {
470                        conts.push(EvalCont::Call {
471                            lowered: Rc::clone(&lowered),
472                            args: Rc::clone(&args),
473                            idx,
474                            fn_val,
475                            arg_vals,
476                        });
477                        EvalState::Expr {
478                            lowered,
479                            expr: args[idx],
480                        }
481                    } else {
482                        self.dispatch_call(
483                            fn_val.expect("function value present when args are done"),
484                            arg_vals,
485                            conts,
486                        )
487                    }
488                }
489                Err(err) => EvalState::Apply(Err(err)),
490            },
491            EvalCont::BinOpLeft { lowered, op, right } => match result {
492                Ok(left) => {
493                    conts.push(EvalCont::BinOpRight { op, left });
494                    EvalState::Expr {
495                        lowered,
496                        expr: right,
497                    }
498                }
499                Err(err) => EvalState::Apply(Err(err)),
500            },
501            EvalCont::BinOpRight { op, left } => match result {
502                Ok(right) => EvalState::Apply(self.eval_binop_nv(&op, left, right)),
503                Err(err) => EvalState::Apply(Err(err)),
504            },
505            EvalCont::Match {
506                lowered,
507                arms,
508                line,
509            } => match result {
510                Ok(subject) => self.dispatch_match(lowered, subject, arms, line, conts),
511                Err(err) => EvalState::Apply(Err(err)),
512            },
513            EvalCont::DirectCall {
514                lowered,
515                target,
516                idx,
517                args,
518                mut arg_vals,
519            } => match result {
520                Ok(value) => {
521                    arg_vals.push(value);
522                    self.resume_direct_call(lowered, target, args, idx + 1, arg_vals, conts)
523                }
524                Err(err) => EvalState::Apply(Err(err)),
525            },
526            EvalCont::Leaf {
527                lowered,
528                leaf,
529                idx,
530                mut values,
531            } => match result {
532                Ok(value) => {
533                    values.push(value);
534                    self.resume_leaf(lowered, leaf, idx + 1, values, conts)
535                }
536                Err(err) => EvalState::Apply(Err(err)),
537            },
538            EvalCont::Constructor(name) => match result {
539                Ok(inner) => {
540                    EvalState::Apply(self.apply_runtime_constructor_nv(&name, Some(inner)))
541                }
542                Err(err) => EvalState::Apply(Err(err)),
543            },
544            EvalCont::ErrorProp => match result {
545                Ok(value) => EvalState::Apply(if value.is_ok() {
546                    let inner = value.wrapper_inner(&self.arena);
547                    Ok(inner)
548                } else if value.is_err() {
549                    let inner = value.wrapper_inner(&self.arena);
550                    Err(RuntimeError::ErrProp(inner))
551                } else {
552                    Err(RuntimeError::Error(
553                        "Operator '?' can only be applied to Result".to_string(),
554                    ))
555                }),
556                Err(err) => EvalState::Apply(Err(err)),
557            },
558            EvalCont::InterpolatedStr {
559                lowered,
560                parts,
561                idx,
562                result: mut text,
563            } => match result {
564                Ok(value) => {
565                    text.push_str(&value.repr(&self.arena));
566                    self.resume_interpolated_str(lowered, parts, idx, text, conts)
567                }
568                Err(err) => EvalState::Apply(Err(err)),
569            },
570            EvalCont::List {
571                lowered,
572                items,
573                idx,
574                mut values,
575            } => match result {
576                Ok(value) => {
577                    values.push(value);
578                    self.resume_list(lowered, items, idx, values, conts)
579                }
580                Err(err) => EvalState::Apply(Err(err)),
581            },
582            EvalCont::Tuple {
583                lowered,
584                items,
585                idx,
586                mut values,
587            } => match result {
588                Ok(value) => {
589                    values.push(value);
590                    self.resume_tuple(lowered, items, idx, values, conts)
591                }
592                Err(err) => EvalState::Apply(Err(err)),
593            },
594            EvalCont::MapKey {
595                lowered,
596                entries,
597                idx,
598                map,
599            } => match result {
600                Ok(key) => {
601                    if !Self::is_hashable_map_key_nv(key) {
602                        return EvalState::Apply(Err(RuntimeError::Error(
603                            "Map literal key must be Int, Float, String, or Bool".to_string(),
604                        )));
605                    }
606                    conts.push(EvalCont::MapValue {
607                        lowered: Rc::clone(&lowered),
608                        entries: Rc::clone(&entries),
609                        idx,
610                        map,
611                        key,
612                    });
613                    EvalState::Expr {
614                        lowered,
615                        expr: entries[idx].1,
616                    }
617                }
618                Err(err) => EvalState::Apply(Err(err)),
619            },
620            EvalCont::MapValue {
621                lowered,
622                entries,
623                idx,
624                map,
625                key,
626            } => match result {
627                Ok(value) => {
628                    let hash = key.map_key_hash(&self.arena);
629                    let map = map.insert(hash, (key, value));
630                    self.resume_map(lowered, entries, idx + 1, map, conts)
631                }
632                Err(err) => EvalState::Apply(Err(err)),
633            },
634            EvalCont::RecordCreate(mut progress) => match result {
635                Ok(value) => {
636                    let field_name = progress.fields[progress.idx].0.clone();
637                    if !progress.seen.insert(field_name.clone()) {
638                        return EvalState::Apply(Err(RuntimeError::Error(format!(
639                            "Record '{}' field '{}' provided more than once",
640                            progress.type_name, field_name
641                        ))));
642                    }
643                    progress.values.push((field_name, value));
644                    progress.idx += 1;
645                    self.resume_record_create(progress, conts)
646                }
647                Err(err) => EvalState::Apply(Err(err)),
648            },
649            EvalCont::RecordUpdateBase {
650                lowered,
651                type_name,
652                updates,
653            } => match result {
654                Ok(base_nv) => {
655                    if !base_nv.is_record() {
656                        return EvalState::Apply(Err(RuntimeError::Error(format!(
657                            "{}.update: base must be a {} record",
658                            type_name, type_name
659                        ))));
660                    }
661                    let (base_type_id, base_fields) = self.arena.get_record(base_nv.arena_index());
662                    let base_type_name = self.arena.get_type_name(base_type_id).to_string();
663                    if base_type_name != type_name {
664                        return EvalState::Apply(Err(RuntimeError::Error(format!(
665                            "{}.update: base is a {} record, expected {}",
666                            type_name, base_type_name, type_name
667                        ))));
668                    }
669                    let base_field_names: Vec<String> =
670                        self.arena.get_field_names(base_type_id).to_vec();
671                    let base_fields_vec: Vec<NanValue> = base_fields.to_vec();
672                    self.resume_record_update(
673                        RecordUpdateProgress {
674                            lowered,
675                            type_name,
676                            base_type_id,
677                            base_fields: base_fields_vec,
678                            base_field_names,
679                            updates,
680                            idx: 0,
681                            update_vals: Vec::new(),
682                        },
683                        conts,
684                    )
685                }
686                Err(err) => EvalState::Apply(Err(err)),
687            },
688            EvalCont::RecordUpdateField(mut progress) => match result {
689                Ok(value) => {
690                    progress
691                        .update_vals
692                        .push((progress.updates[progress.idx].0.clone(), value));
693                    progress.idx += 1;
694                    self.resume_record_update(progress, conts)
695                }
696                Err(err) => EvalState::Apply(Err(err)),
697            },
698            EvalCont::TailCallArgs {
699                lowered,
700                target,
701                args,
702                idx,
703                mut values,
704            } => match result {
705                Ok(value) => {
706                    values.push(value);
707                    self.resume_tail_call(lowered, target, args, idx + 1, values, conts)
708                }
709                Err(err) => EvalState::Apply(Err(err)),
710            },
711            EvalCont::BodyBinding {
712                name,
713                next_idx,
714                lowered,
715                local_slots,
716            } => match result {
717                Ok(value) => {
718                    if let Some(local_slots) = local_slots.as_ref()
719                        && let Some(&slot) = local_slots.get(&name)
720                    {
721                        self.define_slot(slot, value);
722                    } else {
723                        self.define_nv(name, value);
724                    }
725                    EvalState::Body {
726                        lowered,
727                        idx: next_idx,
728                        local_slots,
729                        last: NanValue::UNIT,
730                    }
731                }
732                Err(err) => EvalState::Apply(Err(err)),
733            },
734            EvalCont::BodyExpr {
735                next_idx,
736                lowered,
737                local_slots,
738            } => match result {
739                Ok(value) => EvalState::Body {
740                    lowered,
741                    idx: next_idx,
742                    local_slots,
743                    last: value,
744                },
745                Err(err) => EvalState::Apply(Err(err)),
746            },
747            EvalCont::MatchScope => {
748                self.pop_env();
749                EvalState::Apply(result)
750            }
751            EvalCont::FunctionReturn(frame) => self.finish_function_call(frame, result, conts),
752        }
753    }
754
755    /// Attribute access on a NanValue.
756    fn attr_access_nv(&self, nv: NanValue, field: &str) -> Result<NanValue, RuntimeError> {
757        if nv.is_record() {
758            let (tid, fields) = self.arena.get_record(nv.arena_index());
759            let field_names = self.arena.get_field_names(tid);
760            if let Some(pos) = field_names.iter().position(|n| n == field) {
761                return Ok(fields[pos]);
762            }
763            return Err(RuntimeError::Error(format!("Unknown field '{}'", field)));
764        }
765        if nv.is_namespace() {
766            let (name, members) = self.arena.get_namespace(nv.symbol_index());
767            if let Some((_, member_nv)) = members.iter().find(|(k, _)| k.as_ref() == field) {
768                return Ok(*member_nv);
769            }
770            return Err(RuntimeError::Error(format!(
771                "Unknown member '{}.{}'",
772                name, field
773            )));
774        }
775        Err(RuntimeError::Error(format!(
776            "Field access '{}' is not supported on this value",
777            field
778        )))
779    }
780
781    fn dispatch_match(
782        &mut self,
783        lowered: Rc<LoweredFunctionBody>,
784        subject: NanValue,
785        arms: SharedMatchArms,
786        line: usize,
787        conts: &mut Vec<EvalCont>,
788    ) -> EvalState {
789        if let Some((arm_idx, bindings)) = self.try_dispatch_match_plan_nv(subject, arms.as_ref()) {
790            let arm_count = arms.len();
791            let arm = &arms[arm_idx];
792            self.note_verify_match_arm(line, arm_count, arm_idx);
793            if bindings.is_empty() {
794                return EvalState::Expr {
795                    lowered,
796                    expr: arm.body,
797                };
798            }
799
800            if let Some(local_slots) = self.active_local_slots.clone() {
801                let all_slotted = bindings
802                    .iter()
803                    .all(|(name, _)| local_slots.contains_key(name));
804                if all_slotted {
805                    for (name, nv) in bindings {
806                        if let Some(&slot) = local_slots.get(&name) {
807                            self.define_slot(slot, nv);
808                        }
809                    }
810                    return EvalState::Expr {
811                        lowered,
812                        expr: arm.body,
813                    };
814                }
815            }
816
817            let scope: HashMap<String, NanValue> = bindings.into_iter().collect();
818            self.push_env(EnvFrame::Owned(scope));
819            conts.push(EvalCont::MatchScope);
820            return EvalState::Expr {
821                lowered,
822                expr: arm.body,
823            };
824        }
825
826        let arm_count = arms.len();
827        for (arm_idx, arm) in arms.iter().enumerate() {
828            if let Some(bindings) = self.match_pattern_nv(&arm.pattern, subject) {
829                self.note_verify_match_arm(line, arm_count, arm_idx);
830                if bindings.is_empty() {
831                    return EvalState::Expr {
832                        lowered,
833                        expr: arm.body,
834                    };
835                }
836
837                if let Some(local_slots) = self.active_local_slots.clone() {
838                    let all_slotted = bindings
839                        .iter()
840                        .all(|(name, _)| local_slots.contains_key(name));
841                    if all_slotted {
842                        for (name, nv) in bindings {
843                            if let Some(&slot) = local_slots.get(&name) {
844                                self.define_slot(slot, nv);
845                            }
846                        }
847                        return EvalState::Expr {
848                            lowered,
849                            expr: arm.body,
850                        };
851                    }
852                }
853
854                let scope: HashMap<String, NanValue> = bindings.into_iter().collect();
855                self.push_env(EnvFrame::Owned(scope));
856                conts.push(EvalCont::MatchScope);
857                return EvalState::Expr {
858                    lowered,
859                    expr: arm.body,
860                };
861            }
862        }
863
864        EvalState::Apply(Err(RuntimeError::Error(format!(
865            "No match found for value {}",
866            subject.repr(&self.arena)
867        ))))
868    }
869
870    fn dispatch_call(
871        &mut self,
872        fn_val: NanValue,
873        args: Vec<NanValue>,
874        conts: &mut Vec<EvalCont>,
875    ) -> EvalState {
876        match self.start_call_nv(fn_val, args) {
877            Ok(CallDispatch::Immediate(result)) => EvalState::Apply(result),
878            Ok(CallDispatch::EnterFunction { frame, state }) => {
879                conts.push(EvalCont::FunctionReturn(*frame));
880                state
881            }
882            Err(err) => EvalState::Apply(Err(err)),
883        }
884    }
885
886    fn start_call_nv(
887        &mut self,
888        fn_val: NanValue,
889        args: Vec<NanValue>,
890    ) -> Result<CallDispatch, RuntimeError> {
891        if fn_val.is_builtin() {
892            let name = self.arena.get_builtin(fn_val.symbol_index()).to_string();
893            self.ensure_effects_allowed(&name, Self::builtin_effects(&name).iter().copied())?;
894            let result = self.call_builtin_nv(&name, &args);
895            return Ok(CallDispatch::Immediate(result));
896        }
897        if fn_val.is_fn() {
898            let function = Rc::clone(self.arena.get_fn_rc(fn_val.symbol_index()));
899            if args.len() != function.params.len() {
900                return Err(RuntimeError::Error(format!(
901                    "Function '{}' expects {} arguments, got {}",
902                    function.name,
903                    function.params.len(),
904                    args.len()
905                )));
906            }
907            self.ensure_effects_allowed(
908                function.name.as_str(),
909                function.effects.iter().map(String::as_str),
910            )?;
911
912            let memo_key = if function.memo_eligible {
913                let key = hash_memo_args_nv(&args, &self.arena);
914                let fn_name: &str = function.name.as_ref();
915                if let Some(cache) = self.memo_cache.get_mut(fn_name)
916                    && let Some(cached_val) = cache.get_nv_as_value(key, &args, &self.arena)
917                {
918                    let cached_nv = NanValue::from_value(&cached_val, &mut self.arena);
919                    return Ok(CallDispatch::Immediate(Ok(cached_nv)));
920                }
921                Some((key, args.clone()))
922            } else {
923                None
924            };
925
926            self.call_stack.push(CallFrame {
927                name: Rc::clone(&function.name),
928                effects: Rc::clone(&function.effects),
929            });
930
931            let prev_local_slots = self.active_local_slots.take();
932            let saved_base = self.env_base;
933            self.env_base = self.env.len();
934            let prev_global = if let Some(home) = function.home_globals.as_ref() {
935                let global = self
936                    .env
937                    .first_mut()
938                    .ok_or_else(|| RuntimeError::Error("No global scope".to_string()))?;
939                Some(std::mem::replace(global, EnvFrame::Shared(Rc::clone(home))))
940            } else {
941                None
942            };
943
944            let active = ActiveFunction { function };
945            let frame = FunctionFrame {
946                active,
947                prev_local_slots,
948                saved_base,
949                prev_global,
950                memo_key,
951            };
952            let state = self.enter_function_body_nv(&frame.active, args);
953            return Ok(CallDispatch::EnterFunction {
954                frame: Box::new(frame),
955                state,
956            });
957        }
958        Err(RuntimeError::Error(format!(
959            "Cannot call value: {}",
960            fn_val.repr(&self.arena)
961        )))
962    }
963
964    fn enter_function_body_nv(
965        &mut self,
966        active: &ActiveFunction,
967        args: Vec<NanValue>,
968    ) -> EvalState {
969        if let Some(resolution) = &active.function.resolution {
970            let local_slots = Rc::clone(&resolution.local_slots);
971            let mut slots = Self::empty_slots_nv(resolution.local_count);
972            for ((param_name, _), arg_nv) in active.function.params.iter().zip(args.into_iter()) {
973                if let Some(&slot) = resolution.local_slots.get(param_name) {
974                    slots[slot as usize] = arg_nv;
975                }
976            }
977            self.active_local_slots = Some(Rc::clone(&local_slots));
978            self.push_env(EnvFrame::Slots(slots));
979            EvalState::Body {
980                lowered: Rc::clone(&active.function.lowered_body),
981                idx: 0,
982                local_slots: Some(local_slots),
983                last: NanValue::UNIT,
984            }
985        } else {
986            let mut params_scope: HashMap<String, NanValue> = HashMap::new();
987            for ((param_name, _), arg_nv) in active.function.params.iter().zip(args.into_iter()) {
988                params_scope.insert(param_name.clone(), arg_nv);
989            }
990            self.push_env(EnvFrame::Owned(params_scope));
991            EvalState::Body {
992                lowered: Rc::clone(&active.function.lowered_body),
993                idx: 0,
994                local_slots: None,
995                last: NanValue::UNIT,
996            }
997        }
998    }
999
1000    fn finish_function_call(
1001        &mut self,
1002        mut frame: FunctionFrame,
1003        result: Result<NanValue, RuntimeError>,
1004        conts: &mut Vec<EvalCont>,
1005    ) -> EvalState {
1006        self.pop_env();
1007
1008        match result {
1009            Err(RuntimeError::TailCall(boxed)) => {
1010                let (target, nv_args) = *boxed;
1011                let next_active = if target == frame.active.function.name.as_str() {
1012                    frame.active.clone()
1013                } else {
1014                    let next_function = match self.lookup_nv(&target) {
1015                        Ok(nv) => {
1016                            if !nv.is_fn() {
1017                                return EvalState::Apply(Err(RuntimeError::Error(format!(
1018                                    "TCO target '{}' is not a function: {}",
1019                                    target,
1020                                    nv.repr(&self.arena)
1021                                ))));
1022                            }
1023                            Rc::clone(self.arena.get_fn_rc(nv.symbol_index()))
1024                        }
1025                        Err(err) => return EvalState::Apply(Err(err)),
1026                    };
1027
1028                    if let Some(call_frame) = self.call_stack.last_mut() {
1029                        call_frame.name = Rc::clone(&next_function.name);
1030                        call_frame.effects = Rc::clone(&next_function.effects);
1031                    }
1032
1033                    ActiveFunction {
1034                        function: next_function,
1035                    }
1036                };
1037
1038                frame.active = next_active;
1039                let state = self.enter_function_body_nv(&frame.active, nv_args);
1040                conts.push(EvalCont::FunctionReturn(frame));
1041                state
1042            }
1043            other => {
1044                self.active_local_slots = frame.prev_local_slots;
1045                if let Some(prev) = frame.prev_global
1046                    && let Some(global) = self.env.first_mut()
1047                {
1048                    *global = prev;
1049                }
1050                self.env_base = frame.saved_base;
1051                self.call_stack.pop();
1052
1053                let final_result = match other {
1054                    Ok(value) => Ok(value),
1055                    Err(RuntimeError::ErrProp(err_nv)) => {
1056                        Ok(NanValue::new_err_value(err_nv, &mut self.arena))
1057                    }
1058                    Err(err) => Err(err),
1059                };
1060
1061                if let (Some((key, memo_args)), Ok(value)) = (&frame.memo_key, &final_result) {
1062                    let fn_name: &str = frame.active.function.name.as_ref();
1063                    let cache = if let Some(c) = self.memo_cache.get_mut(fn_name) {
1064                        c
1065                    } else {
1066                        self.memo_cache
1067                            .entry(frame.active.function.name.as_ref().clone())
1068                            .or_default()
1069                    };
1070                    cache.insert_nv(
1071                        *key,
1072                        memo_args.clone(),
1073                        *value,
1074                        &self.arena,
1075                        MEMO_CACHE_CAP_PER_FN,
1076                    );
1077                }
1078
1079                EvalState::Apply(final_result)
1080            }
1081        }
1082    }
1083
1084    fn resume_interpolated_str(
1085        &mut self,
1086        lowered: Rc<LoweredFunctionBody>,
1087        parts: SharedStrParts,
1088        mut idx: usize,
1089        mut result: String,
1090        conts: &mut Vec<EvalCont>,
1091    ) -> EvalState {
1092        while idx < parts.len() {
1093            match parts[idx].clone() {
1094                LoweredStrPart::Literal(text) => {
1095                    result.push_str(&text);
1096                    idx += 1;
1097                }
1098                LoweredStrPart::Parsed(expr) => {
1099                    conts.push(EvalCont::InterpolatedStr {
1100                        lowered: Rc::clone(&lowered),
1101                        parts: Rc::clone(&parts),
1102                        idx: idx + 1,
1103                        result,
1104                    });
1105                    return EvalState::Expr { lowered, expr };
1106                }
1107            }
1108        }
1109        EvalState::Apply(Ok(NanValue::new_string_value(&result, &mut self.arena)))
1110    }
1111
1112    fn resume_direct_call(
1113        &mut self,
1114        lowered: Rc<LoweredFunctionBody>,
1115        target: LoweredDirectCallTarget,
1116        args: SharedExprs,
1117        idx: usize,
1118        arg_vals: Vec<NanValue>,
1119        conts: &mut Vec<EvalCont>,
1120    ) -> EvalState {
1121        if idx >= args.len() {
1122            return self.dispatch_direct_call(target, arg_vals, conts);
1123        }
1124
1125        conts.push(EvalCont::DirectCall {
1126            lowered: Rc::clone(&lowered),
1127            target,
1128            idx,
1129            args: Rc::clone(&args),
1130            arg_vals,
1131        });
1132        EvalState::Expr {
1133            lowered,
1134            expr: args[idx],
1135        }
1136    }
1137
1138    fn resume_leaf(
1139        &mut self,
1140        lowered: Rc<LoweredFunctionBody>,
1141        leaf: LoweredLeafOp,
1142        idx: usize,
1143        values: Vec<NanValue>,
1144        conts: &mut Vec<EvalCont>,
1145    ) -> EvalState {
1146        if let Some(expr) = leaf.arg_at(idx) {
1147            conts.push(EvalCont::Leaf {
1148                lowered: Rc::clone(&lowered),
1149                leaf,
1150                idx,
1151                values,
1152            });
1153            EvalState::Expr { lowered, expr }
1154        } else {
1155            EvalState::Apply(self.apply_leaf_op_nv(&leaf, &values))
1156        }
1157    }
1158
1159    fn resume_list(
1160        &mut self,
1161        lowered: Rc<LoweredFunctionBody>,
1162        items: SharedExprs,
1163        idx: usize,
1164        values: Vec<NanValue>,
1165        conts: &mut Vec<EvalCont>,
1166    ) -> EvalState {
1167        if idx >= items.len() {
1168            let list_idx = self.arena.push_list(values);
1169            return EvalState::Apply(Ok(NanValue::new_list(list_idx)));
1170        }
1171
1172        conts.push(EvalCont::List {
1173            lowered: Rc::clone(&lowered),
1174            items: Rc::clone(&items),
1175            idx: idx + 1,
1176            values,
1177        });
1178        EvalState::Expr {
1179            lowered,
1180            expr: items[idx],
1181        }
1182    }
1183
1184    fn resume_tuple(
1185        &mut self,
1186        lowered: Rc<LoweredFunctionBody>,
1187        items: SharedExprs,
1188        idx: usize,
1189        values: Vec<NanValue>,
1190        conts: &mut Vec<EvalCont>,
1191    ) -> EvalState {
1192        if idx >= items.len() {
1193            let tuple_idx = self.arena.push_tuple(values);
1194            return EvalState::Apply(Ok(NanValue::new_tuple(tuple_idx)));
1195        }
1196
1197        conts.push(EvalCont::Tuple {
1198            lowered: Rc::clone(&lowered),
1199            items: Rc::clone(&items),
1200            idx: idx + 1,
1201            values,
1202        });
1203        EvalState::Expr {
1204            lowered,
1205            expr: items[idx],
1206        }
1207    }
1208
1209    fn resume_map(
1210        &mut self,
1211        lowered: Rc<LoweredFunctionBody>,
1212        entries: SharedMapEntries,
1213        idx: usize,
1214        map: crate::nan_value::PersistentMap,
1215        conts: &mut Vec<EvalCont>,
1216    ) -> EvalState {
1217        if idx >= entries.len() {
1218            let map_idx = self.arena.push_map(map);
1219            return EvalState::Apply(Ok(NanValue::new_map(map_idx)));
1220        }
1221
1222        conts.push(EvalCont::MapKey {
1223            lowered: Rc::clone(&lowered),
1224            entries: Rc::clone(&entries),
1225            idx,
1226            map,
1227        });
1228        EvalState::Expr {
1229            lowered,
1230            expr: entries[idx].0,
1231        }
1232    }
1233
1234    fn resume_record_create(
1235        &mut self,
1236        progress: RecordCreateProgress,
1237        conts: &mut Vec<EvalCont>,
1238    ) -> EvalState {
1239        if progress.idx >= progress.fields.len() {
1240            return EvalState::Apply(
1241                self.build_record_create_nv(&progress.type_name, progress.values),
1242            );
1243        }
1244
1245        let lowered = Rc::clone(&progress.lowered);
1246        let expr = progress.fields[progress.idx].1;
1247        conts.push(EvalCont::RecordCreate(progress));
1248        EvalState::Expr { lowered, expr }
1249    }
1250
1251    fn resume_record_update(
1252        &mut self,
1253        progress: RecordUpdateProgress,
1254        conts: &mut Vec<EvalCont>,
1255    ) -> EvalState {
1256        if progress.idx >= progress.updates.len() {
1257            return EvalState::Apply(self.build_record_update_nv(
1258                &progress.type_name,
1259                progress.base_type_id,
1260                progress.base_fields,
1261                progress.base_field_names,
1262                progress.update_vals,
1263            ));
1264        }
1265
1266        let next_expr = progress.updates[progress.idx].1;
1267        let lowered = Rc::clone(&progress.lowered);
1268        conts.push(EvalCont::RecordUpdateField(progress));
1269        EvalState::Expr {
1270            lowered,
1271            expr: next_expr,
1272        }
1273    }
1274
1275    fn resume_tail_call(
1276        &mut self,
1277        lowered: Rc<LoweredFunctionBody>,
1278        target: LoweredTailCallTarget,
1279        args: SharedExprs,
1280        idx: usize,
1281        values: Vec<NanValue>,
1282        conts: &mut Vec<EvalCont>,
1283    ) -> EvalState {
1284        if idx >= args.len() {
1285            let resolved_target = match target {
1286                LoweredTailCallTarget::SelfCall => self
1287                    .call_stack
1288                    .last()
1289                    .map(|frame| frame.name.as_ref().clone())
1290                    .unwrap_or_default(),
1291                LoweredTailCallTarget::KnownFunction(name)
1292                | LoweredTailCallTarget::Unknown(name) => name,
1293            };
1294            return EvalState::Apply(Err(RuntimeError::TailCall(Box::new((
1295                resolved_target,
1296                values,
1297            )))));
1298        }
1299
1300        conts.push(EvalCont::TailCallArgs {
1301            lowered: Rc::clone(&lowered),
1302            target,
1303            args: Rc::clone(&args),
1304            idx,
1305            values,
1306        });
1307        EvalState::Expr {
1308            lowered,
1309            expr: args[idx],
1310        }
1311    }
1312
1313    fn build_record_create_nv(
1314        &mut self,
1315        type_name: &str,
1316        field_vals: Vec<(String, NanValue)>,
1317    ) -> Result<NanValue, RuntimeError> {
1318        if let Some(schema) = self.record_schemas.get(type_name) {
1319            // When we have a schema, register the type using schema field order
1320            // (which is the declaration order from the record definition).
1321            let schema_clone = schema.clone();
1322            let type_id = self.arena.find_type_id(type_name).unwrap_or_else(|| {
1323                self.arena
1324                    .register_record_type(type_name, schema_clone.clone())
1325            });
1326            let mut by_name = HashMap::with_capacity(field_vals.len());
1327            for (name, value) in field_vals {
1328                if by_name.insert(name.clone(), value).is_some() {
1329                    return Err(RuntimeError::Error(format!(
1330                        "Record '{}' field '{}' provided more than once",
1331                        type_name, name
1332                    )));
1333                }
1334            }
1335
1336            for provided in by_name.keys() {
1337                if !schema_clone.iter().any(|field| field == provided) {
1338                    return Err(RuntimeError::Error(format!(
1339                        "Record '{}' has no field '{}'",
1340                        type_name, provided
1341                    )));
1342                }
1343            }
1344
1345            let mut ordered = Vec::with_capacity(schema_clone.len());
1346            for required in &schema_clone {
1347                let value = by_name.remove(required).ok_or_else(|| {
1348                    RuntimeError::Error(format!(
1349                        "Record '{}' missing required field '{}'",
1350                        type_name, required
1351                    ))
1352                })?;
1353                ordered.push(value);
1354            }
1355
1356            let rec_idx = self.arena.push_record(type_id, ordered);
1357            return Ok(NanValue::new_record(rec_idx));
1358        }
1359
1360        // No schema — just use field order as-is
1361        let type_id = self.arena.find_type_id(type_name).unwrap_or_else(|| {
1362            let field_names: Vec<String> = field_vals.iter().map(|(n, _)| n.clone()).collect();
1363            self.arena.register_record_type(type_name, field_names)
1364        });
1365        let nv_fields: Vec<NanValue> = field_vals.iter().map(|(_, v)| *v).collect();
1366        let rec_idx = self.arena.push_record(type_id, nv_fields);
1367        Ok(NanValue::new_record(rec_idx))
1368    }
1369
1370    fn apply_leaf_op_nv(
1371        &mut self,
1372        leaf: &LoweredLeafOp,
1373        args: &[NanValue],
1374    ) -> Result<NanValue, RuntimeError> {
1375        match leaf {
1376            LoweredLeafOp::MapGet { .. } => {
1377                map::call_nv("Map.get", args, &mut self.arena).expect("Map.get leaf owned by Map")
1378            }
1379            LoweredLeafOp::MapSet { .. } => {
1380                map::call_nv("Map.set", args, &mut self.arena).expect("Map.set leaf owned by Map")
1381            }
1382            LoweredLeafOp::VectorNew { .. } => vector::call_nv("Vector.new", args, &mut self.arena)
1383                .expect("Vector.new leaf owned by Vector"),
1384            LoweredLeafOp::VectorGetOrDefaultLiteral {
1385                default_literal, ..
1386            } => {
1387                let opt = vector::call_nv("Vector.get", args, &mut self.arena)
1388                    .expect("Vector.get leaf owned by Vector")?;
1389                if opt.is_some() {
1390                    Ok(opt.wrapper_inner(&self.arena))
1391                } else if opt.is_none() {
1392                    Ok(self.eval_literal_nv(default_literal))
1393                } else {
1394                    Err(RuntimeError::Error(
1395                        "Vector.get leaf expected Option result".to_string(),
1396                    ))
1397                }
1398            }
1399            LoweredLeafOp::IntModOrDefaultLiteral {
1400                default_literal, ..
1401            } => {
1402                let a = args[0].as_int(&self.arena);
1403                let b = args[1].as_int(&self.arena);
1404                if b == 0 {
1405                    Ok(self.eval_literal_nv(default_literal))
1406                } else {
1407                    Ok(NanValue::new_int(a.rem_euclid(b), &mut self.arena))
1408                }
1409            }
1410        }
1411    }
1412
1413    fn dispatch_direct_call(
1414        &mut self,
1415        target: LoweredDirectCallTarget,
1416        args: Vec<NanValue>,
1417        conts: &mut Vec<EvalCont>,
1418    ) -> EvalState {
1419        match target {
1420            LoweredDirectCallTarget::Builtin(name) | LoweredDirectCallTarget::Function(name) => {
1421                match self.lookup_path_nv(&name) {
1422                    Ok(fn_val) => self.dispatch_call(fn_val, args, conts),
1423                    Err(err) => EvalState::Apply(Err(err)),
1424                }
1425            }
1426            LoweredDirectCallTarget::Wrapper(kind) => match args.as_slice() {
1427                [inner] => {
1428                    let value = match kind {
1429                        crate::ir::WrapperKind::ResultOk => {
1430                            NanValue::new_ok_value(*inner, &mut self.arena)
1431                        }
1432                        crate::ir::WrapperKind::ResultErr => {
1433                            NanValue::new_err_value(*inner, &mut self.arena)
1434                        }
1435                        crate::ir::WrapperKind::OptionSome => {
1436                            NanValue::new_some_value(*inner, &mut self.arena)
1437                        }
1438                    };
1439                    EvalState::Apply(Ok(value))
1440                }
1441                _ => EvalState::Apply(Err(RuntimeError::Error(
1442                    "Wrapper constructor expects exactly 1 argument".to_string(),
1443                ))),
1444            },
1445            LoweredDirectCallTarget::NoneValue => {
1446                if args.is_empty() {
1447                    EvalState::Apply(Ok(NanValue::NONE))
1448                } else {
1449                    EvalState::Apply(Err(RuntimeError::Error(
1450                        "Option.None expects 0 arguments".to_string(),
1451                    )))
1452                }
1453            }
1454            LoweredDirectCallTarget::TypeConstructor {
1455                qualified_type_name,
1456                variant_name,
1457            } => {
1458                let name = format!("{qualified_type_name}.{variant_name}");
1459                EvalState::Apply(self.apply_runtime_constructor_args_nv(&name, &args))
1460            }
1461        }
1462    }
1463
1464    fn dispatch_forward_call(
1465        &mut self,
1466        target: LoweredDirectCallTarget,
1467        args: &[LoweredForwardArg],
1468        conts: &mut Vec<EvalCont>,
1469    ) -> EvalState {
1470        let mut values = Vec::with_capacity(args.len());
1471        for arg in args {
1472            let value = match arg {
1473                LoweredForwardArg::Local(name) => match self.lookup_nv(name) {
1474                    Ok(value) => value,
1475                    Err(err) => return EvalState::Apply(Err(err)),
1476                },
1477                LoweredForwardArg::Slot(slot) => match self.lookup_slot(*slot) {
1478                    Ok(value) => value,
1479                    Err(err) => return EvalState::Apply(Err(err)),
1480                },
1481            };
1482            values.push(value);
1483        }
1484        self.dispatch_direct_call(target, values, conts)
1485    }
1486
1487    fn build_record_update_nv(
1488        &mut self,
1489        type_name: &str,
1490        base_type_id: u32,
1491        mut base_fields: Vec<NanValue>,
1492        base_field_names: Vec<String>,
1493        update_vals: Vec<(String, NanValue)>,
1494    ) -> Result<NanValue, RuntimeError> {
1495        if let Some(schema) = self.record_schemas.get(type_name) {
1496            for (field_name, _) in &update_vals {
1497                if !schema.iter().any(|field| field == field_name) {
1498                    return Err(RuntimeError::Error(format!(
1499                        "Record '{}' has no field '{}'",
1500                        type_name, field_name
1501                    )));
1502                }
1503            }
1504        }
1505
1506        for (update_name, update_val) in update_vals {
1507            if let Some(pos) = base_field_names.iter().position(|n| n == &update_name) {
1508                base_fields[pos] = update_val;
1509            } else {
1510                return Err(RuntimeError::Error(format!(
1511                    "Record '{}' has no field '{}'",
1512                    type_name, update_name
1513                )));
1514            }
1515        }
1516
1517        let rec_idx = self.arena.push_record(base_type_id, base_fields);
1518        Ok(NanValue::new_record(rec_idx))
1519    }
1520
1521    fn is_hashable_map_key_nv(value: NanValue) -> bool {
1522        value.is_int() || value.is_float() || value.is_string() || value.is_bool()
1523    }
1524
1525    pub(super) fn eval_literal_nv(&mut self, lit: &Literal) -> NanValue {
1526        match lit {
1527            Literal::Int(i) => NanValue::new_int(*i, &mut self.arena),
1528            Literal::Float(f) => NanValue::new_float(*f),
1529            Literal::Str(s) => NanValue::new_string_value(s, &mut self.arena),
1530            Literal::Bool(b) => NanValue::new_bool(*b),
1531            Literal::Unit => NanValue::UNIT,
1532        }
1533    }
1534
1535    #[allow(dead_code)]
1536    pub(super) fn eval_literal(&self, lit: &Literal) -> Value {
1537        match lit {
1538            Literal::Int(i) => Value::Int(*i),
1539            Literal::Float(f) => Value::Float(*f),
1540            Literal::Str(s) => Value::Str(s.clone()),
1541            Literal::Bool(b) => Value::Bool(*b),
1542            Literal::Unit => Value::Unit,
1543        }
1544    }
1545
1546    pub(super) fn call_value(
1547        &mut self,
1548        fn_val: Value,
1549        args: Vec<Value>,
1550    ) -> Result<Value, RuntimeError> {
1551        let fn_nv = NanValue::from_value(&fn_val, &mut self.arena);
1552        let nv_args: Vec<NanValue> = args
1553            .iter()
1554            .map(|v| NanValue::from_value(v, &mut self.arena))
1555            .collect();
1556        match self.start_call_nv(fn_nv, nv_args)? {
1557            CallDispatch::Immediate(result) => result.map(|nv| nv.to_value(&self.arena)),
1558            CallDispatch::EnterFunction { frame, state } => {
1559                let nv = self.eval_loop(state, vec![EvalCont::FunctionReturn(*frame)])?;
1560                Ok(nv.to_value(&self.arena))
1561            }
1562        }
1563    }
1564}
1565
1566/// Hash NanValue args for memo cache — uses arena for content-based hashing.
1567fn hash_memo_args_nv(args: &[NanValue], arena: &Arena) -> u64 {
1568    use std::hash::{Hash, Hasher};
1569    let mut hasher = std::collections::hash_map::DefaultHasher::new();
1570    args.len().hash(&mut hasher);
1571    for arg in args {
1572        arg.hash_in(&mut hasher, arena);
1573    }
1574    hasher.finish()
1575}