Skip to main content

tidepool_eval/
eval.rs

1use crate::env::Env;
2use crate::error::EvalError;
3use crate::heap::{Heap, ThunkState};
4use crate::value::Value;
5use tidepool_repr::{
6    AltCon, CoreExpr, CoreFrame, DataConId, DataConTable, Literal, PrimOpKind, VarId,
7};
8
9/// Create an environment pre-populated with data constructor functions.
10/// Each constructor with arity N becomes a `ConFun(tag, N, [])` value
11/// bound to its worker VarId, so that `Var` references to constructors
12/// in the expression tree resolve correctly.
13pub fn env_from_datacon_table(table: &DataConTable) -> Env {
14    let mut env = Env::new();
15    for dc in table.iter() {
16        let var = VarId(dc.id.0);
17        if dc.rep_arity == 0 {
18            // Nullary constructor: just a Con value
19            env.insert(var, Value::Con(dc.id, vec![]));
20        } else {
21            env.insert(var, Value::ConFun(dc.id, dc.rep_arity as usize, vec![]));
22        }
23    }
24    env
25}
26
27/// Evaluate a CoreExpr to a Value.
28pub fn eval(expr: &CoreExpr, env: &Env, heap: &mut dyn Heap) -> Result<Value, EvalError> {
29    if expr.nodes.is_empty() {
30        return Err(EvalError::TypeMismatch {
31            expected: "non-empty expression",
32            got: crate::error::ValueKind::Other("empty tree".into()),
33        });
34    }
35    let res = eval_at(expr, expr.nodes.len() - 1, env, heap)?;
36    force(res, heap)
37}
38
39/// Force a thunk to a value.
40pub fn force(val: Value, heap: &mut dyn Heap) -> Result<Value, EvalError> {
41    match val {
42        Value::ThunkRef(id) => {
43            match heap.read(id).clone() {
44                ThunkState::Evaluated(v) => force(v, heap),
45                ThunkState::BlackHole => Err(EvalError::InfiniteLoop(id)),
46                ThunkState::Unevaluated(env, expr) => {
47                    heap.write(id, ThunkState::BlackHole);
48                    match eval(&expr, &env, heap) {
49                        Ok(result) => {
50                            heap.write(id, ThunkState::Evaluated(result.clone()));
51                            Ok(result)
52                        }
53                        Err(err) => {
54                            // Restore state on error to avoid masking original failure
55                            // with InfiniteLoop on subsequent forces.
56                            heap.write(id, ThunkState::Unevaluated(env, expr));
57                            Err(err)
58                        }
59                    }
60                }
61            }
62        }
63        other => Ok(other),
64    }
65}
66
67/// Iteratively force a value — forces all thunks inside constructors,
68/// producing a fully-evaluated tree with no `ThunkRef` values.
69/// Uses an explicit work stack instead of recursion to handle deep
70/// structures (e.g. 1000-element lists) without overflowing the Rust stack.
71pub fn deep_force(val: Value, heap: &mut dyn Heap) -> Result<Value, EvalError> {
72    use tidepool_repr::DataConId;
73
74    const MAX_DEPTH: usize = 100_000;
75
76    enum Work {
77        Force(Value),
78        BuildCon(DataConId, usize),           // (tag, num_fields)
79        BuildConFun(DataConId, usize, usize), // (tag, arity, num_args)
80    }
81
82    let mut stack: Vec<Work> = vec![Work::Force(val)];
83    let mut results: Vec<Value> = Vec::new();
84
85    while let Some(work) = stack.pop() {
86        if stack.len() > MAX_DEPTH {
87            return Err(EvalError::DepthLimit);
88        }
89        match work {
90            Work::Force(v) => match v {
91                Value::ThunkRef(id) => {
92                    let forced = force(Value::ThunkRef(id), heap)?;
93                    stack.push(Work::Force(forced));
94                }
95                Value::Con(tag, fields) => {
96                    let n = fields.len();
97                    stack.push(Work::BuildCon(tag, n));
98                    // Push fields in reverse so they're processed in order
99                    for f in fields.into_iter().rev() {
100                        stack.push(Work::Force(f));
101                    }
102                }
103                Value::ConFun(tag, arity, args) => {
104                    let n = args.len();
105                    stack.push(Work::BuildConFun(tag, arity, n));
106                    for a in args.into_iter().rev() {
107                        stack.push(Work::Force(a));
108                    }
109                }
110                other => results.push(other),
111            },
112            Work::BuildCon(tag, n) => {
113                let start = results.len() - n;
114                let fields = results.split_off(start);
115                results.push(Value::Con(tag, fields));
116            }
117            Work::BuildConFun(tag, arity, n) => {
118                let start = results.len() - n;
119                let args = results.split_off(start);
120                results.push(Value::ConFun(tag, arity, args));
121            }
122        }
123    }
124
125    results.pop().ok_or(EvalError::TypeMismatch {
126        expected: "non-empty result stack",
127        got: crate::error::ValueKind::Closure,
128    })
129}
130
131/// Evaluate the node at `idx` in the expression tree.
132fn eval_at(
133    expr: &CoreExpr,
134    idx: usize,
135    env: &Env,
136    heap: &mut dyn Heap,
137) -> Result<Value, EvalError> {
138    match &expr.nodes[idx] {
139        CoreFrame::Var(v) => {
140            let tag = (v.0 >> 56) as u8;
141            if tag == tidepool_repr::ERROR_SENTINEL_TAG {
142                // 'E' = error tag: synthetic error VarIds from Translate.hs
143                let kind = v.0 & 0xFF;
144                return Err(match kind {
145                    0 => EvalError::TypeMismatch {
146                        expected: "non-zero divisor",
147                        got: crate::error::ValueKind::Other("division by zero".into()),
148                    },
149                    1 => EvalError::TypeMismatch {
150                        expected: "no overflow",
151                        got: crate::error::ValueKind::Other("arithmetic overflow".into()),
152                    },
153                    2 => EvalError::UserError,
154                    3 => EvalError::Undefined,
155                    _ => EvalError::UserError,
156                });
157            }
158            env.get(v).cloned().ok_or(EvalError::UnboundVar(*v))
159        }
160        CoreFrame::Lit(lit) => Ok(Value::Lit(lit.clone())),
161        CoreFrame::App { fun, arg } => {
162            let fun_val = force(eval_at(expr, *fun, env, heap)?, heap)?;
163            let arg_val = eval_at(expr, *arg, env, heap)?;
164            match fun_val {
165                Value::Closure(clos_env, binder, body) => {
166                    let mut new_env = clos_env;
167                    new_env.insert(binder, arg_val);
168                    eval(&body, &new_env, heap)
169                }
170                Value::ConFun(tag, arity, mut args) => {
171                    args.push(arg_val);
172                    if args.len() == arity {
173                        // Don't force fields — leave thunks intact for lazy evaluation.
174                        // Fields will be forced on demand when case-matched or used by primops.
175                        Ok(Value::Con(tag, args))
176                    } else {
177                        Ok(Value::ConFun(tag, arity, args))
178                    }
179                }
180                _ => Err(EvalError::NotAFunction),
181            }
182        }
183        CoreFrame::Lam { binder, body } => {
184            let body_expr = expr.extract_subtree(*body);
185            Ok(Value::Closure(env.clone(), *binder, body_expr))
186        }
187        CoreFrame::LetNonRec { binder, rhs, body } => {
188            let rhs_val = if matches!(&expr.nodes[*rhs], CoreFrame::Lam { .. }) {
189                eval_at(expr, *rhs, env, heap)? // Lambdas are values
190            } else {
191                let thunk_id = heap.alloc(env.clone(), expr.extract_subtree(*rhs));
192                Value::ThunkRef(thunk_id)
193            };
194            let new_env = env.update(*binder, rhs_val);
195            eval_at(expr, *body, &new_env, heap)
196        }
197        CoreFrame::LetRec { bindings, body } => {
198            let mut new_env = env.clone();
199            let mut thunks = Vec::new();
200
201            // 1. Allocate thunks for all binders to allow full knot-tying.
202            // (Spec: non-lambdas -> ThunkRef, but for knot-tying lambdas also need to be accessible)
203            for (binder, rhs_idx) in bindings {
204                let tid = heap.alloc(Env::new(), CoreExpr { nodes: vec![] });
205                new_env = new_env.update(*binder, Value::ThunkRef(tid));
206                thunks.push((*binder, tid, *rhs_idx));
207            }
208
209            // 2. Evaluate lambda RHSes and back-patch thunks. Update env with Closures.
210            for (binder, tid, rhs_idx) in &thunks {
211                if matches!(&expr.nodes[*rhs_idx], CoreFrame::Lam { .. }) {
212                    let lam_val = eval_at(expr, *rhs_idx, &new_env, heap)?;
213                    heap.write(*tid, ThunkState::Evaluated(lam_val.clone()));
214                    new_env = new_env.update(*binder, lam_val);
215                } else {
216                    let rhs_subtree = expr.extract_subtree(*rhs_idx);
217                    heap.write(*tid, ThunkState::Unevaluated(new_env.clone(), rhs_subtree));
218                }
219            }
220
221            eval_at(expr, *body, &new_env, heap)
222        }
223        CoreFrame::Con { tag, fields } => {
224            let mut field_vals = Vec::with_capacity(fields.len());
225            for &f in fields {
226                // Thunkify non-trivial fields to enable lazy evaluation.
227                // Var and Lit are cheap lookups; everything else (App, Case,
228                // Let, PrimOp, nested Con) gets wrapped in a thunk so that
229                // infinite structures like `cycle` and `zipWith ... [0..]`
230                // don't diverge at construction time.
231                match &expr.nodes[f] {
232                    CoreFrame::Var(_)
233                    | CoreFrame::Lit(_)
234                    | CoreFrame::Con { .. }
235                    | CoreFrame::Lam { .. }
236                    | CoreFrame::PrimOp { .. } => {
237                        field_vals.push(eval_at(expr, f, env, heap)?);
238                    }
239                    _ => {
240                        let subtree = expr.extract_subtree(f);
241                        let thunk_id = heap.alloc(env.clone(), subtree);
242                        field_vals.push(Value::ThunkRef(thunk_id));
243                    }
244                }
245            }
246            Ok(Value::Con(*tag, field_vals))
247        }
248        CoreFrame::Case {
249            scrutinee,
250            binder,
251            alts,
252        } => {
253            let scrut_val = force(eval_at(expr, *scrutinee, env, heap)?, heap)?;
254            let case_env = env.update(*binder, scrut_val.clone());
255
256            // Try specific alternatives first; Default is a fallback, not positional.
257            // GHC Core can place DEFAULT first in the alt list.
258            let mut default_alt = None;
259            for alt in alts {
260                match &alt.con {
261                    AltCon::DataAlt(tag) => {
262                        if let Value::Con(con_tag, fields) = &scrut_val {
263                            if con_tag == tag {
264                                if fields.len() != alt.binders.len() {
265                                    return Err(EvalError::ArityMismatch {
266                                        context: "case binders",
267                                        expected: alt.binders.len(),
268                                        got: fields.len(),
269                                    });
270                                }
271                                let mut alt_env = case_env;
272                                for (b, v) in alt.binders.iter().zip(fields.iter()) {
273                                    alt_env = alt_env.update(*b, v.clone());
274                                }
275                                return eval_at(expr, alt.body, &alt_env, heap);
276                            }
277                        }
278                    }
279                    AltCon::LitAlt(lit) => {
280                        if let Value::Lit(l) = &scrut_val {
281                            if l == lit {
282                                return eval_at(expr, alt.body, &case_env, heap);
283                            }
284                        }
285                    }
286                    AltCon::Default => {
287                        default_alt = Some(alt);
288                    }
289                }
290            }
291            if let Some(alt) = default_alt {
292                return eval_at(expr, alt.body, &case_env, heap);
293            }
294            Err(EvalError::NoMatchingAlt)
295        }
296        CoreFrame::PrimOp { op, args } => {
297            let mut arg_vals = Vec::with_capacity(args.len());
298            for &arg in args {
299                let val = force(eval_at(expr, arg, env, heap)?, heap)?;
300                arg_vals.push(val);
301            }
302            // Handle primops that need heap access for deep forcing
303            match op {
304                PrimOpKind::ShowDoubleAddr => {
305                    if arg_vals.len() != 1 {
306                        return Err(EvalError::ArityMismatch {
307                            context: "arguments",
308                            expected: 1,
309                            got: arg_vals.len(),
310                        });
311                    }
312                    // Defense in depth: force through Con fields to handle thunked D# fields
313                    let d = match &arg_vals[0] {
314                        Value::Lit(Literal::LitDouble(bits)) => f64::from_bits(*bits),
315                        Value::Con(_, fields) if fields.len() == 1 => {
316                            let forced = force(fields[0].clone(), heap)?;
317                            expect_double(&forced)?
318                        }
319                        other => {
320                            return Err(EvalError::TypeMismatch {
321                                expected: "Double# or D# Double#",
322                                got: crate::error::ValueKind::Other(format!("{:?}", other)),
323                            })
324                        }
325                    };
326                    let s = eval_haskell_show_double(d);
327                    let mut bytes = s.into_bytes();
328                    bytes.push(0); // null terminator for IndexCharOffAddr
329                    Ok(Value::Lit(Literal::LitString(bytes)))
330                }
331                _ => dispatch_primop(*op, arg_vals),
332            }
333        }
334        CoreFrame::Join {
335            label,
336            params,
337            rhs,
338            body,
339        } => {
340            let join_val = Value::JoinCont(params.clone(), expr.extract_subtree(*rhs), env.clone());
341            let join_var = VarId(label.0 | (1u64 << 63)); // high bit distinguishes join labels
342            let new_env = env.update(join_var, join_val);
343            eval_at(expr, *body, &new_env, heap)
344        }
345        CoreFrame::Jump { label, args } => {
346            let join_var = VarId(label.0 | (1u64 << 63));
347            match env.get(&join_var) {
348                Some(Value::JoinCont(params, rhs_expr, join_env)) => {
349                    if params.len() != args.len() {
350                        return Err(EvalError::ArityMismatch {
351                            context: "arguments",
352                            expected: params.len(),
353                            got: args.len(),
354                        });
355                    }
356                    let params = params.clone();
357                    let rhs_expr = rhs_expr.clone();
358                    let mut new_env = join_env.clone();
359                    for (param, arg_idx) in params.iter().zip(args.iter()) {
360                        let arg_val = eval_at(expr, *arg_idx, env, heap)?;
361                        new_env = new_env.update(*param, arg_val);
362                    }
363                    eval(&rhs_expr, &new_env, heap)
364                }
365                _ => Err(EvalError::UnboundJoin(*label)),
366            }
367        }
368    }
369}
370
371fn dispatch_primop(op: PrimOpKind, args: Vec<Value>) -> Result<Value, EvalError> {
372    match op {
373        PrimOpKind::IntAdd => {
374            let (a, b) = bin_op_int(op, &args)?;
375            Ok(Value::Lit(Literal::LitInt(a.wrapping_add(b))))
376        }
377        PrimOpKind::IntSub => {
378            let (a, b) = bin_op_int(op, &args)?;
379            Ok(Value::Lit(Literal::LitInt(a.wrapping_sub(b))))
380        }
381        PrimOpKind::IntMul => {
382            let (a, b) = bin_op_int(op, &args)?;
383            Ok(Value::Lit(Literal::LitInt(a.wrapping_mul(b))))
384        }
385        PrimOpKind::IntNegate => {
386            if args.len() != 1 {
387                return Err(EvalError::ArityMismatch {
388                    context: "arguments",
389                    expected: 1,
390                    got: args.len(),
391                });
392            }
393            let a = expect_int(&args[0])?;
394            Ok(Value::Lit(Literal::LitInt(a.wrapping_neg())))
395        }
396        PrimOpKind::IntEq => cmp_int(op, &args, |a, b| a == b),
397        PrimOpKind::IntNe => cmp_int(op, &args, |a, b| a != b),
398        PrimOpKind::IntLt => cmp_int(op, &args, |a, b| a < b),
399        PrimOpKind::IntLe => cmp_int(op, &args, |a, b| a <= b),
400        PrimOpKind::IntGt => cmp_int(op, &args, |a, b| a > b),
401        PrimOpKind::IntGe => cmp_int(op, &args, |a, b| a >= b),
402        PrimOpKind::IntAnd => {
403            let (a, b) = bin_op_int(op, &args)?;
404            Ok(Value::Lit(Literal::LitInt(a & b)))
405        }
406        PrimOpKind::IntOr => {
407            let (a, b) = bin_op_int(op, &args)?;
408            Ok(Value::Lit(Literal::LitInt(a | b)))
409        }
410        PrimOpKind::IntXor => {
411            let (a, b) = bin_op_int(op, &args)?;
412            Ok(Value::Lit(Literal::LitInt(a ^ b)))
413        }
414        PrimOpKind::IntNot => {
415            if args.len() != 1 {
416                return Err(EvalError::ArityMismatch {
417                    context: "arguments",
418                    expected: 1,
419                    got: args.len(),
420                });
421            }
422            let a = expect_int(&args[0])?;
423            Ok(Value::Lit(Literal::LitInt(!a)))
424        }
425        PrimOpKind::IntShl => {
426            let (a, b) = bin_op_int(op, &args)?;
427            Ok(Value::Lit(Literal::LitInt(a.wrapping_shl(b as u32))))
428        }
429        PrimOpKind::IntShra => {
430            let (a, b) = bin_op_int(op, &args)?;
431            Ok(Value::Lit(Literal::LitInt(a.wrapping_shr(b as u32))))
432        }
433        PrimOpKind::IntShrl => {
434            let (a, b) = bin_op_int(op, &args)?;
435            Ok(Value::Lit(Literal::LitInt(
436                (a as u64).wrapping_shr(b as u32) as i64,
437            )))
438        }
439
440        PrimOpKind::WordAdd => {
441            let (a, b) = bin_op_word(op, &args)?;
442            Ok(Value::Lit(Literal::LitWord(a.wrapping_add(b))))
443        }
444        PrimOpKind::WordSub => {
445            let (a, b) = bin_op_word(op, &args)?;
446            Ok(Value::Lit(Literal::LitWord(a.wrapping_sub(b))))
447        }
448        PrimOpKind::WordMul => {
449            let (a, b) = bin_op_word(op, &args)?;
450            Ok(Value::Lit(Literal::LitWord(a.wrapping_mul(b))))
451        }
452        PrimOpKind::WordEq => cmp_word(op, &args, |a, b| a == b),
453        PrimOpKind::WordNe => cmp_word(op, &args, |a, b| a != b),
454        PrimOpKind::WordLt => cmp_word(op, &args, |a, b| a < b),
455        PrimOpKind::WordLe => cmp_word(op, &args, |a, b| a <= b),
456        PrimOpKind::WordGt => cmp_word(op, &args, |a, b| a > b),
457        PrimOpKind::WordGe => cmp_word(op, &args, |a, b| a >= b),
458        PrimOpKind::WordQuot => {
459            let (a, b) = bin_op_word(op, &args)?;
460            if b == 0 {
461                return Err(EvalError::InternalError(
462                    "division by zero (quotWord#)".into(),
463                ));
464            }
465            Ok(Value::Lit(Literal::LitWord(a.wrapping_div(b))))
466        }
467        PrimOpKind::WordRem => {
468            let (a, b) = bin_op_word(op, &args)?;
469            if b == 0 {
470                return Err(EvalError::InternalError(
471                    "division by zero (remWord#)".into(),
472                ));
473            }
474            Ok(Value::Lit(Literal::LitWord(a.wrapping_rem(b))))
475        }
476        PrimOpKind::WordAnd => {
477            let (a, b) = bin_op_word(op, &args)?;
478            Ok(Value::Lit(Literal::LitWord(a & b)))
479        }
480        PrimOpKind::WordOr => {
481            let (a, b) = bin_op_word(op, &args)?;
482            Ok(Value::Lit(Literal::LitWord(a | b)))
483        }
484        PrimOpKind::WordXor => {
485            let (a, b) = bin_op_word(op, &args)?;
486            Ok(Value::Lit(Literal::LitWord(a ^ b)))
487        }
488        PrimOpKind::WordNot => {
489            if args.len() != 1 {
490                return Err(EvalError::ArityMismatch {
491                    context: "arguments",
492                    expected: 1,
493                    got: args.len(),
494                });
495            }
496            let a = expect_word(&args[0])?;
497            Ok(Value::Lit(Literal::LitWord(!a)))
498        }
499        PrimOpKind::WordShl => {
500            if args.len() != 2 {
501                return Err(EvalError::ArityMismatch {
502                    context: "arguments",
503                    expected: 2,
504                    got: args.len(),
505                });
506            }
507            let a = expect_word(&args[0])?;
508            let b = expect_int(&args[1])?;
509            Ok(Value::Lit(Literal::LitWord(a.wrapping_shl(b as u32))))
510        }
511        PrimOpKind::WordShrl => {
512            if args.len() != 2 {
513                return Err(EvalError::ArityMismatch {
514                    context: "arguments",
515                    expected: 2,
516                    got: args.len(),
517                });
518            }
519            let a = expect_word(&args[0])?;
520            let b = expect_int(&args[1])?;
521            Ok(Value::Lit(Literal::LitWord(a.wrapping_shr(b as u32))))
522        }
523
524        PrimOpKind::DoubleAdd => {
525            let (a, b) = bin_op_double(op, &args)?;
526            Ok(Value::Lit(Literal::LitDouble((a + b).to_bits())))
527        }
528        PrimOpKind::DoubleSub => {
529            let (a, b) = bin_op_double(op, &args)?;
530            Ok(Value::Lit(Literal::LitDouble((a - b).to_bits())))
531        }
532        PrimOpKind::DoubleMul => {
533            let (a, b) = bin_op_double(op, &args)?;
534            Ok(Value::Lit(Literal::LitDouble((a * b).to_bits())))
535        }
536        PrimOpKind::DoubleDiv => {
537            let (a, b) = bin_op_double(op, &args)?;
538            Ok(Value::Lit(Literal::LitDouble((a / b).to_bits())))
539        }
540        PrimOpKind::DoubleEq => cmp_double(op, &args, |a, b| a == b),
541        PrimOpKind::DoubleNe => cmp_double(op, &args, |a, b| a != b),
542        PrimOpKind::DoubleLt => cmp_double(op, &args, |a, b| a < b),
543        PrimOpKind::DoubleLe => cmp_double(op, &args, |a, b| a <= b),
544        PrimOpKind::DoubleGt => cmp_double(op, &args, |a, b| a > b),
545        PrimOpKind::DoubleGe => cmp_double(op, &args, |a, b| a >= b),
546        PrimOpKind::DoubleNegate => {
547            if args.len() != 1 {
548                return Err(EvalError::ArityMismatch {
549                    context: "arguments",
550                    expected: 1,
551                    got: args.len(),
552                });
553            }
554            let a = expect_double(&args[0])?;
555            Ok(Value::Lit(Literal::LitDouble((-a).to_bits())))
556        }
557        PrimOpKind::DoubleFabs => {
558            if args.len() != 1 {
559                return Err(EvalError::ArityMismatch {
560                    context: "arguments",
561                    expected: 1,
562                    got: args.len(),
563                });
564            }
565            let a = expect_double(&args[0])?;
566            Ok(Value::Lit(Literal::LitDouble(a.abs().to_bits())))
567        }
568        PrimOpKind::DoubleSqrt => {
569            if args.len() != 1 {
570                return Err(EvalError::ArityMismatch {
571                    context: "arguments",
572                    expected: 1,
573                    got: args.len(),
574                });
575            }
576            let a = expect_double(&args[0])?;
577            Ok(Value::Lit(Literal::LitDouble(a.sqrt().to_bits())))
578        }
579        PrimOpKind::DoubleExp => {
580            if args.len() != 1 {
581                return Err(EvalError::ArityMismatch {
582                    context: "arguments",
583                    expected: 1,
584                    got: args.len(),
585                });
586            }
587            let a = expect_double(&args[0])?;
588            Ok(Value::Lit(Literal::LitDouble(a.exp().to_bits())))
589        }
590        PrimOpKind::DoubleExpM1 => {
591            if args.len() != 1 {
592                return Err(EvalError::ArityMismatch {
593                    context: "arguments",
594                    expected: 1,
595                    got: args.len(),
596                });
597            }
598            let a = expect_double(&args[0])?;
599            Ok(Value::Lit(Literal::LitDouble(a.exp_m1().to_bits())))
600        }
601        PrimOpKind::DoubleLog => {
602            if args.len() != 1 {
603                return Err(EvalError::ArityMismatch {
604                    context: "arguments",
605                    expected: 1,
606                    got: args.len(),
607                });
608            }
609            let a = expect_double(&args[0])?;
610            Ok(Value::Lit(Literal::LitDouble(a.ln().to_bits())))
611        }
612        PrimOpKind::DoubleLog1P => {
613            if args.len() != 1 {
614                return Err(EvalError::ArityMismatch {
615                    context: "arguments",
616                    expected: 1,
617                    got: args.len(),
618                });
619            }
620            let a = expect_double(&args[0])?;
621            Ok(Value::Lit(Literal::LitDouble(a.ln_1p().to_bits())))
622        }
623        PrimOpKind::DoubleSin => {
624            if args.len() != 1 {
625                return Err(EvalError::ArityMismatch {
626                    context: "arguments",
627                    expected: 1,
628                    got: args.len(),
629                });
630            }
631            let a = expect_double(&args[0])?;
632            Ok(Value::Lit(Literal::LitDouble(a.sin().to_bits())))
633        }
634        PrimOpKind::DoubleCos => {
635            if args.len() != 1 {
636                return Err(EvalError::ArityMismatch {
637                    context: "arguments",
638                    expected: 1,
639                    got: args.len(),
640                });
641            }
642            let a = expect_double(&args[0])?;
643            Ok(Value::Lit(Literal::LitDouble(a.cos().to_bits())))
644        }
645        PrimOpKind::DoubleTan => {
646            if args.len() != 1 {
647                return Err(EvalError::ArityMismatch {
648                    context: "arguments",
649                    expected: 1,
650                    got: args.len(),
651                });
652            }
653            let a = expect_double(&args[0])?;
654            Ok(Value::Lit(Literal::LitDouble(a.tan().to_bits())))
655        }
656        PrimOpKind::DoubleAsin => {
657            if args.len() != 1 {
658                return Err(EvalError::ArityMismatch {
659                    context: "arguments",
660                    expected: 1,
661                    got: args.len(),
662                });
663            }
664            let a = expect_double(&args[0])?;
665            Ok(Value::Lit(Literal::LitDouble(a.asin().to_bits())))
666        }
667        PrimOpKind::DoubleAcos => {
668            if args.len() != 1 {
669                return Err(EvalError::ArityMismatch {
670                    context: "arguments",
671                    expected: 1,
672                    got: args.len(),
673                });
674            }
675            let a = expect_double(&args[0])?;
676            Ok(Value::Lit(Literal::LitDouble(a.acos().to_bits())))
677        }
678        PrimOpKind::DoubleAtan => {
679            if args.len() != 1 {
680                return Err(EvalError::ArityMismatch {
681                    context: "arguments",
682                    expected: 1,
683                    got: args.len(),
684                });
685            }
686            let a = expect_double(&args[0])?;
687            Ok(Value::Lit(Literal::LitDouble(a.atan().to_bits())))
688        }
689        PrimOpKind::DoubleSinh => {
690            if args.len() != 1 {
691                return Err(EvalError::ArityMismatch {
692                    context: "arguments",
693                    expected: 1,
694                    got: args.len(),
695                });
696            }
697            let a = expect_double(&args[0])?;
698            Ok(Value::Lit(Literal::LitDouble(a.sinh().to_bits())))
699        }
700        PrimOpKind::DoubleCosh => {
701            if args.len() != 1 {
702                return Err(EvalError::ArityMismatch {
703                    context: "arguments",
704                    expected: 1,
705                    got: args.len(),
706                });
707            }
708            let a = expect_double(&args[0])?;
709            Ok(Value::Lit(Literal::LitDouble(a.cosh().to_bits())))
710        }
711        PrimOpKind::DoubleTanh => {
712            if args.len() != 1 {
713                return Err(EvalError::ArityMismatch {
714                    context: "arguments",
715                    expected: 1,
716                    got: args.len(),
717                });
718            }
719            let a = expect_double(&args[0])?;
720            Ok(Value::Lit(Literal::LitDouble(a.tanh().to_bits())))
721        }
722        PrimOpKind::DoubleAsinh => {
723            if args.len() != 1 {
724                return Err(EvalError::ArityMismatch {
725                    context: "arguments",
726                    expected: 1,
727                    got: args.len(),
728                });
729            }
730            let a = expect_double(&args[0])?;
731            Ok(Value::Lit(Literal::LitDouble(a.asinh().to_bits())))
732        }
733        PrimOpKind::DoubleAcosh => {
734            if args.len() != 1 {
735                return Err(EvalError::ArityMismatch {
736                    context: "arguments",
737                    expected: 1,
738                    got: args.len(),
739                });
740            }
741            let a = expect_double(&args[0])?;
742            Ok(Value::Lit(Literal::LitDouble(a.acosh().to_bits())))
743        }
744        PrimOpKind::DoubleAtanh => {
745            if args.len() != 1 {
746                return Err(EvalError::ArityMismatch {
747                    context: "arguments",
748                    expected: 1,
749                    got: args.len(),
750                });
751            }
752            let a = expect_double(&args[0])?;
753            Ok(Value::Lit(Literal::LitDouble(a.atanh().to_bits())))
754        }
755        PrimOpKind::DoublePower => {
756            if args.len() != 2 {
757                return Err(EvalError::ArityMismatch {
758                    context: "arguments",
759                    expected: 2,
760                    got: args.len(),
761                });
762            }
763            let a = expect_double(&args[0])?;
764            let b = expect_double(&args[1])?;
765            Ok(Value::Lit(Literal::LitDouble(a.powf(b).to_bits())))
766        }
767        PrimOpKind::FloatAdd => {
768            let (a, b) = bin_op_float(op, &args)?;
769            Ok(Value::Lit(Literal::LitFloat((a + b).to_bits() as u64)))
770        }
771        PrimOpKind::FloatSub => {
772            let (a, b) = bin_op_float(op, &args)?;
773            Ok(Value::Lit(Literal::LitFloat((a - b).to_bits() as u64)))
774        }
775        PrimOpKind::FloatMul => {
776            let (a, b) = bin_op_float(op, &args)?;
777            Ok(Value::Lit(Literal::LitFloat((a * b).to_bits() as u64)))
778        }
779        PrimOpKind::FloatDiv => {
780            let (a, b) = bin_op_float(op, &args)?;
781            Ok(Value::Lit(Literal::LitFloat((a / b).to_bits() as u64)))
782        }
783        PrimOpKind::FloatNegate => {
784            if args.len() != 1 {
785                return Err(EvalError::ArityMismatch {
786                    context: "arguments",
787                    expected: 1,
788                    got: args.len(),
789                });
790            }
791            let a = expect_float(&args[0])?;
792            Ok(Value::Lit(Literal::LitFloat((-a).to_bits() as u64)))
793        }
794        PrimOpKind::FloatEq => cmp_float(op, &args, |a, b| a == b),
795        PrimOpKind::FloatNe => cmp_float(op, &args, |a, b| a != b),
796        PrimOpKind::FloatLt => cmp_float(op, &args, |a, b| a < b),
797        PrimOpKind::FloatLe => cmp_float(op, &args, |a, b| a <= b),
798        PrimOpKind::FloatGt => cmp_float(op, &args, |a, b| a > b),
799        PrimOpKind::FloatGe => cmp_float(op, &args, |a, b| a >= b),
800
801        PrimOpKind::CharEq => cmp_char(op, &args, |a, b| a == b),
802        PrimOpKind::CharNe => cmp_char(op, &args, |a, b| a != b),
803        PrimOpKind::CharLt => cmp_char(op, &args, |a, b| a < b),
804        PrimOpKind::CharLe => cmp_char(op, &args, |a, b| a <= b),
805        PrimOpKind::CharGt => cmp_char(op, &args, |a, b| a > b),
806        PrimOpKind::CharGe => cmp_char(op, &args, |a, b| a >= b),
807        PrimOpKind::Int2Word => {
808            if args.len() != 1 {
809                return Err(EvalError::ArityMismatch {
810                    context: "arguments",
811                    expected: 1,
812                    got: args.len(),
813                });
814            }
815            let a = expect_int(&args[0])?;
816            Ok(Value::Lit(Literal::LitWord(a as u64)))
817        }
818        PrimOpKind::Word2Int => {
819            if args.len() != 1 {
820                return Err(EvalError::ArityMismatch {
821                    context: "arguments",
822                    expected: 1,
823                    got: args.len(),
824                });
825            }
826            let a = expect_word(&args[0])?;
827            Ok(Value::Lit(Literal::LitInt(a as i64)))
828        }
829        PrimOpKind::Narrow8Int => {
830            if args.len() != 1 {
831                return Err(EvalError::ArityMismatch {
832                    context: "arguments",
833                    expected: 1,
834                    got: args.len(),
835                });
836            }
837            let a = expect_int(&args[0])?;
838            Ok(Value::Lit(Literal::LitInt(a as i8 as i64)))
839        }
840        PrimOpKind::Narrow16Int => {
841            if args.len() != 1 {
842                return Err(EvalError::ArityMismatch {
843                    context: "arguments",
844                    expected: 1,
845                    got: args.len(),
846                });
847            }
848            let a = expect_int(&args[0])?;
849            Ok(Value::Lit(Literal::LitInt(a as i16 as i64)))
850        }
851        PrimOpKind::Narrow32Int => {
852            if args.len() != 1 {
853                return Err(EvalError::ArityMismatch {
854                    context: "arguments",
855                    expected: 1,
856                    got: args.len(),
857                });
858            }
859            let a = expect_int(&args[0])?;
860            Ok(Value::Lit(Literal::LitInt(a as i32 as i64)))
861        }
862        PrimOpKind::Narrow8Word => {
863            if args.len() != 1 {
864                return Err(EvalError::ArityMismatch {
865                    context: "arguments",
866                    expected: 1,
867                    got: args.len(),
868                });
869            }
870            let a = expect_word(&args[0])?;
871            Ok(Value::Lit(Literal::LitWord(a as u8 as u64)))
872        }
873        PrimOpKind::Narrow16Word => {
874            if args.len() != 1 {
875                return Err(EvalError::ArityMismatch {
876                    context: "arguments",
877                    expected: 1,
878                    got: args.len(),
879                });
880            }
881            let a = expect_word(&args[0])?;
882            Ok(Value::Lit(Literal::LitWord(a as u16 as u64)))
883        }
884        PrimOpKind::Narrow32Word => {
885            if args.len() != 1 {
886                return Err(EvalError::ArityMismatch {
887                    context: "arguments",
888                    expected: 1,
889                    got: args.len(),
890                });
891            }
892            let a = expect_word(&args[0])?;
893            Ok(Value::Lit(Literal::LitWord(a as u32 as u64)))
894        }
895        PrimOpKind::Int2Double => {
896            if args.len() != 1 {
897                return Err(EvalError::ArityMismatch {
898                    context: "arguments",
899                    expected: 1,
900                    got: args.len(),
901                });
902            }
903            let a = expect_int(&args[0])?;
904            Ok(Value::Lit(Literal::LitDouble((a as f64).to_bits())))
905        }
906        PrimOpKind::Double2Int => {
907            if args.len() != 1 {
908                return Err(EvalError::ArityMismatch {
909                    context: "arguments",
910                    expected: 1,
911                    got: args.len(),
912                });
913            }
914            let a = expect_double(&args[0])?;
915            Ok(Value::Lit(Literal::LitInt(a as i64)))
916        }
917        PrimOpKind::DecodeDoubleMantissa => {
918            if args.len() != 1 {
919                return Err(EvalError::ArityMismatch {
920                    context: "arguments",
921                    expected: 1,
922                    got: args.len(),
923                });
924            }
925            let d = expect_double(&args[0])?;
926            let (man, _) = eval_decode_double_int64(d);
927            Ok(Value::Lit(Literal::LitInt(man)))
928        }
929        PrimOpKind::DecodeDoubleExponent => {
930            if args.len() != 1 {
931                return Err(EvalError::ArityMismatch {
932                    context: "arguments",
933                    expected: 1,
934                    got: args.len(),
935                });
936            }
937            let d = expect_double(&args[0])?;
938            let (_, exp) = eval_decode_double_int64(d);
939            Ok(Value::Lit(Literal::LitInt(exp)))
940        }
941        PrimOpKind::ShowDoubleAddr => {
942            // Handled in eval_at PrimOp arm (needs heap for deep forcing)
943            unreachable!("ShowDoubleAddr should be intercepted in eval_at")
944        }
945        PrimOpKind::Int2Float => {
946            if args.len() != 1 {
947                return Err(EvalError::ArityMismatch {
948                    context: "arguments",
949                    expected: 1,
950                    got: args.len(),
951                });
952            }
953            let a = expect_int(&args[0])?;
954            Ok(Value::Lit(Literal::LitFloat((a as f32).to_bits() as u64)))
955        }
956        PrimOpKind::Float2Int => {
957            if args.len() != 1 {
958                return Err(EvalError::ArityMismatch {
959                    context: "arguments",
960                    expected: 1,
961                    got: args.len(),
962                });
963            }
964            let a = expect_float(&args[0])?;
965            Ok(Value::Lit(Literal::LitInt(a as i64)))
966        }
967        PrimOpKind::Double2Float => {
968            if args.len() != 1 {
969                return Err(EvalError::ArityMismatch {
970                    context: "arguments",
971                    expected: 1,
972                    got: args.len(),
973                });
974            }
975            let a = expect_double(&args[0])?;
976            Ok(Value::Lit(Literal::LitFloat((a as f32).to_bits() as u64)))
977        }
978        PrimOpKind::Float2Double => {
979            if args.len() != 1 {
980                return Err(EvalError::ArityMismatch {
981                    context: "arguments",
982                    expected: 1,
983                    got: args.len(),
984                });
985            }
986            let a = expect_float(&args[0])?;
987            Ok(Value::Lit(Literal::LitDouble((a as f64).to_bits())))
988        }
989
990        PrimOpKind::SeqOp => {
991            if args.len() != 2 {
992                return Err(EvalError::ArityMismatch {
993                    context: "arguments",
994                    expected: 2,
995                    got: args.len(),
996                });
997            }
998            Ok(args[1].clone())
999        }
1000        PrimOpKind::DataToTag => {
1001            if args.len() != 1 {
1002                return Err(EvalError::ArityMismatch {
1003                    context: "arguments",
1004                    expected: 1,
1005                    got: args.len(),
1006                });
1007            }
1008            if let Value::Con(DataConId(tag), _) = &args[0] {
1009                Ok(Value::Lit(Literal::LitInt(*tag as i64)))
1010            } else {
1011                Err(EvalError::TypeMismatch {
1012                    expected: "Data constructor",
1013                    got: crate::error::ValueKind::Other(format!("{:?}", args[0])),
1014                })
1015            }
1016        }
1017        PrimOpKind::IntQuot => {
1018            let (a, b) = bin_op_int(op, &args)?;
1019            if b == 0 {
1020                return Err(EvalError::InternalError(
1021                    "division by zero (quotInt#)".into(),
1022                ));
1023            }
1024            Ok(Value::Lit(Literal::LitInt(a.wrapping_div(b))))
1025        }
1026        PrimOpKind::IntRem => {
1027            let (a, b) = bin_op_int(op, &args)?;
1028            if b == 0 {
1029                return Err(EvalError::InternalError(
1030                    "division by zero (remInt#)".into(),
1031                ));
1032            }
1033            Ok(Value::Lit(Literal::LitInt(a.wrapping_rem(b))))
1034        }
1035        PrimOpKind::Chr => {
1036            if args.len() != 1 {
1037                return Err(EvalError::ArityMismatch {
1038                    context: "arguments",
1039                    expected: 1,
1040                    got: args.len(),
1041                });
1042            }
1043            let n = expect_int(&args[0])?;
1044            let code = u32::try_from(n).map_err(|_| EvalError::TypeMismatch {
1045                expected: "valid Unicode codepoint (0..=0x10FFFF)",
1046                got: crate::error::ValueKind::Other(format!("out of range: {}", n)),
1047            })?;
1048            let c = char::from_u32(code).ok_or_else(|| EvalError::TypeMismatch {
1049                expected: "valid Unicode codepoint",
1050                got: crate::error::ValueKind::Other(format!("invalid codepoint: {}", n)),
1051            })?;
1052            Ok(Value::Lit(Literal::LitChar(c)))
1053        }
1054        PrimOpKind::Ord => {
1055            if args.len() != 1 {
1056                return Err(EvalError::ArityMismatch {
1057                    context: "arguments",
1058                    expected: 1,
1059                    got: args.len(),
1060                });
1061            }
1062            let c = expect_char(&args[0])?;
1063            Ok(Value::Lit(Literal::LitInt(c as i64)))
1064        }
1065        PrimOpKind::IndexCharOffAddr => {
1066            if args.len() != 2 {
1067                return Err(EvalError::ArityMismatch {
1068                    context: "arguments",
1069                    expected: 2,
1070                    got: args.len(),
1071                });
1072            }
1073            let bytes = match &args[0] {
1074                Value::Lit(Literal::LitString(bs)) => bs,
1075                other => {
1076                    return Err(EvalError::TypeMismatch {
1077                        expected: "Addr# (LitString)",
1078                        got: crate::error::ValueKind::Other(format!("{:?}", other)),
1079                    })
1080                }
1081            };
1082            let offset = expect_int(&args[1])? as usize;
1083            let ch = bytes.get(offset).copied().unwrap_or(0);
1084            Ok(Value::Lit(Literal::LitChar(ch as char)))
1085        }
1086        PrimOpKind::PlusAddr => {
1087            // plusAddr# :: Addr# -> Int# -> Addr#
1088            // In interpreter, Addr# is LitString. plusAddr# slices the byte vec.
1089            if args.len() != 2 {
1090                return Err(EvalError::ArityMismatch {
1091                    context: "arguments",
1092                    expected: 2,
1093                    got: args.len(),
1094                });
1095            }
1096            let bytes = match &args[0] {
1097                Value::Lit(Literal::LitString(bs)) => bs,
1098                other => {
1099                    return Err(EvalError::TypeMismatch {
1100                        expected: "Addr# (LitString)",
1101                        got: crate::error::ValueKind::Other(format!("{:?}", other)),
1102                    })
1103                }
1104            };
1105            let offset = expect_int(&args[1])? as usize;
1106            if offset > bytes.len() {
1107                return Err(EvalError::TypeMismatch {
1108                    expected: "valid byte offset",
1109                    got: crate::error::ValueKind::Other(format!(
1110                        "offset {} exceeds length {}",
1111                        offset,
1112                        bytes.len()
1113                    )),
1114                });
1115            }
1116            Ok(Value::Lit(Literal::LitString(bytes[offset..].to_vec())))
1117        }
1118        PrimOpKind::ReallyUnsafePtrEquality => Ok(Value::Lit(Literal::LitInt(0))),
1119        PrimOpKind::Raise => Err(EvalError::UserError),
1120        PrimOpKind::IndexArray | PrimOpKind::TagToEnum => Err(EvalError::UnsupportedPrimOp(op)),
1121
1122        // --- ByteArray# / MutableByteArray# ---
1123        PrimOpKind::NewByteArray => {
1124            let size = expect_int(&args[0])? as usize;
1125            Ok(Value::ByteArray(std::sync::Arc::new(
1126                std::sync::Mutex::new(vec![0u8; size]),
1127            )))
1128        }
1129        PrimOpKind::ReadWord8Array => {
1130            let ba = expect_byte_array(&args[0])?;
1131            let idx = expect_int(&args[1])? as usize;
1132            let bytes = ba
1133                .lock()
1134                .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?;
1135            let val = *bytes.get(idx).unwrap_or(&0);
1136            Ok(Value::Lit(Literal::LitWord(val as u64)))
1137        }
1138        PrimOpKind::WriteWord8Array => {
1139            let ba = expect_byte_array(&args[0])?;
1140            let idx = expect_int(&args[1])? as usize;
1141            let val = expect_int_like(&args[2])? as u8;
1142            let mut bytes = ba
1143                .lock()
1144                .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?;
1145            if idx < bytes.len() {
1146                bytes[idx] = val;
1147            }
1148            Ok(Value::ByteArray(ba.clone()))
1149        }
1150        PrimOpKind::SizeofMutableByteArray => {
1151            let ba = expect_byte_array(&args[0])?;
1152            let len = ba
1153                .lock()
1154                .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?
1155                .len();
1156            Ok(Value::Lit(Literal::LitInt(len as i64)))
1157        }
1158        PrimOpKind::UnsafeFreezeByteArray => {
1159            // Freeze = identity (same Arc, just typed differently)
1160            Ok(args[0].clone())
1161        }
1162        PrimOpKind::CopyByteArray | PrimOpKind::CopyMutableByteArray => {
1163            // copyByteArray# src src_off dst dst_off len
1164            let src_ba = expect_byte_array(&args[0])?;
1165            let src_off = expect_int(&args[1])? as usize;
1166            let dst_ba = expect_byte_array(&args[2])?;
1167            let dst_off = expect_int(&args[3])? as usize;
1168            let len = expect_int(&args[4])? as usize;
1169            // Clone src data first to avoid double-lock deadlock when src == dst
1170            let src_data: Vec<u8> = {
1171                let src = src_ba
1172                    .lock()
1173                    .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?;
1174                let end = src_off.checked_add(len);
1175                if end.is_none() || end.unwrap() > src.len() {
1176                    return Err(EvalError::TypeMismatch {
1177                        expected: "valid src range",
1178                        got: crate::error::ValueKind::Other(format!(
1179                            "range {}..{} exceeds length {}",
1180                            src_off,
1181                            src_off + len,
1182                            src.len()
1183                        )),
1184                    });
1185                }
1186                src[src_off..end.unwrap()].to_vec()
1187            };
1188            {
1189                let mut dst = dst_ba
1190                    .lock()
1191                    .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?;
1192                let end = dst_off.checked_add(len);
1193                if end.is_none() || end.unwrap() > dst.len() {
1194                    return Err(EvalError::TypeMismatch {
1195                        expected: "valid dst range",
1196                        got: crate::error::ValueKind::Other(format!(
1197                            "range {}..{} exceeds length {}",
1198                            dst_off,
1199                            dst_off + len,
1200                            dst.len()
1201                        )),
1202                    });
1203                }
1204                dst[dst_off..end.unwrap()].copy_from_slice(&src_data);
1205            }
1206            Ok(Value::ByteArray(dst_ba.clone()))
1207        }
1208        PrimOpKind::CopyAddrToByteArray => {
1209            // copyAddrToByteArray# src dst dst_off len
1210            let src_bytes = match &args[0] {
1211                Value::Lit(Literal::LitString(bs)) => bs,
1212                other => {
1213                    return Err(EvalError::TypeMismatch {
1214                        expected: "Addr# (LitString)",
1215                        got: crate::error::ValueKind::Other(format!("{:?}", other)),
1216                    })
1217                }
1218            };
1219            let dst_ba = expect_byte_array(&args[1])?;
1220            let dst_off = expect_int(&args[2])? as usize;
1221            let len = expect_int(&args[3])? as usize;
1222            let mut dst = dst_ba
1223                .lock()
1224                .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?;
1225            let src_end = std::cmp::min(src_bytes.len(), len);
1226            let end = dst_off.checked_add(src_end);
1227            if end.is_none() || end.unwrap() > dst.len() {
1228                return Err(EvalError::TypeMismatch {
1229                    expected: "valid dst range",
1230                    got: crate::error::ValueKind::Other(format!(
1231                        "range {}..{} exceeds length {}",
1232                        dst_off,
1233                        dst_off + src_end,
1234                        dst.len()
1235                    )),
1236                });
1237            }
1238            dst[dst_off..end.unwrap()].copy_from_slice(&src_bytes[..src_end]);
1239            drop(dst);
1240            Ok(Value::ByteArray(dst_ba.clone()))
1241        }
1242        PrimOpKind::ShrinkMutableByteArray => {
1243            let ba = expect_byte_array(&args[0])?;
1244            let new_size = expect_int(&args[1])? as usize;
1245            ba.lock()
1246                .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?
1247                .truncate(new_size);
1248            Ok(Value::ByteArray(ba.clone()))
1249        }
1250        PrimOpKind::ResizeMutableByteArray => {
1251            let ba = expect_byte_array(&args[0])?;
1252            let new_size = expect_int(&args[1])? as usize;
1253            let mut bytes = ba
1254                .lock()
1255                .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?;
1256            bytes.resize(new_size, 0);
1257            drop(bytes);
1258            Ok(Value::ByteArray(ba.clone()))
1259        }
1260        PrimOpKind::Clz8 => {
1261            let w = expect_word(&args[0])?;
1262            Ok(Value::Lit(Literal::LitWord(
1263                (w as u8).leading_zeros() as u64
1264            )))
1265        }
1266        PrimOpKind::IntToInt64 => {
1267            // Identity on 64-bit: Int# == Int64#
1268            Ok(args[0].clone())
1269        }
1270        PrimOpKind::Int64ToWord64 => {
1271            let n = expect_int(&args[0])?;
1272            Ok(Value::Lit(Literal::LitWord(n as u64)))
1273        }
1274        PrimOpKind::TimesInt2Hi | PrimOpKind::TimesInt2Lo | PrimOpKind::TimesInt2Overflow => {
1275            let a = expect_int(&args[0])? as i128;
1276            let b = expect_int(&args[1])? as i128;
1277            let result = a * b;
1278            match op {
1279                PrimOpKind::TimesInt2Overflow => {
1280                    let overflowed = result > i64::MAX as i128 || result < i64::MIN as i128;
1281                    Ok(Value::Lit(Literal::LitInt(if overflowed { 1 } else { 0 })))
1282                }
1283                PrimOpKind::TimesInt2Hi => Ok(Value::Lit(Literal::LitInt((result >> 64) as i64))),
1284                PrimOpKind::TimesInt2Lo => Ok(Value::Lit(Literal::LitInt(result as i64))),
1285                _ => Err(EvalError::InternalError(format!(
1286                    "unexpected TimesInt2 variant: {:?}",
1287                    op
1288                ))),
1289            }
1290        }
1291
1292        PrimOpKind::IndexWord8Array => {
1293            let ba = expect_byte_array(&args[0])?;
1294            let idx_val = expect_int(&args[1])?;
1295            if idx_val < 0 {
1296                return Err(EvalError::TypeMismatch {
1297                    expected: "non-negative array index",
1298                    got: crate::error::ValueKind::Other(format!("negative index: {}", idx_val)),
1299                });
1300            }
1301            let idx = idx_val as usize;
1302            let bytes = ba
1303                .lock()
1304                .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?;
1305            let val = *bytes.get(idx).unwrap_or(&0);
1306            Ok(Value::Lit(Literal::LitWord(val as u64)))
1307        }
1308        PrimOpKind::IndexWord8OffAddr => {
1309            // indexWord8OffAddr# :: Addr# -> Int# -> Word8#
1310            let bytes = match &args[0] {
1311                Value::Lit(Literal::LitString(bs)) => bs,
1312                other => {
1313                    return Err(EvalError::TypeMismatch {
1314                        expected: "Addr# (LitString)",
1315                        got: crate::error::ValueKind::Other(format!("{:?}", other)),
1316                    })
1317                }
1318            };
1319            let idx = expect_int(&args[1])? as usize;
1320            let val = bytes.get(idx).copied().unwrap_or(0);
1321            Ok(Value::Lit(Literal::LitWord(val as u64)))
1322        }
1323        PrimOpKind::CompareByteArrays => {
1324            // compareByteArrays# ba1 off1 ba2 off2 len -> Int#
1325            let ba1 = expect_byte_array(&args[0])?;
1326            let off1 = expect_int(&args[1])? as usize;
1327            let ba2 = expect_byte_array(&args[2])?;
1328            let off2 = expect_int(&args[3])? as usize;
1329            let len = expect_int(&args[4])? as usize;
1330            // Clone to avoid potential double-lock if ba1 == ba2
1331            let slice1: Vec<u8> = {
1332                let b = ba1
1333                    .lock()
1334                    .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?;
1335                let end1 = off1.checked_add(len);
1336                if end1.is_none() || end1.unwrap() > b.len() {
1337                    return Err(EvalError::TypeMismatch {
1338                        expected: "valid byte range for ba1",
1339                        got: crate::error::ValueKind::Other(format!(
1340                            "range {}..{} exceeds length {}",
1341                            off1,
1342                            off1 + len,
1343                            b.len()
1344                        )),
1345                    });
1346                }
1347                b[off1..end1.unwrap()].to_vec()
1348            };
1349            let slice2: Vec<u8> = {
1350                let b = ba2
1351                    .lock()
1352                    .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?;
1353                let end2 = off2.checked_add(len);
1354                if end2.is_none() || end2.unwrap() > b.len() {
1355                    return Err(EvalError::TypeMismatch {
1356                        expected: "valid byte range for ba2",
1357                        got: crate::error::ValueKind::Other(format!(
1358                            "range {}..{} exceeds length {}",
1359                            off2,
1360                            off2 + len,
1361                            b.len()
1362                        )),
1363                    });
1364                }
1365                b[off2..end2.unwrap()].to_vec()
1366            };
1367            let result = slice1.cmp(&slice2);
1368            Ok(Value::Lit(Literal::LitInt(match result {
1369                std::cmp::Ordering::Less => -1,
1370                std::cmp::Ordering::Equal => 0,
1371                std::cmp::Ordering::Greater => 1,
1372            })))
1373        }
1374        PrimOpKind::WordToWord8 => {
1375            // Narrow Word# to Word8# (mask to 8 bits)
1376            let w = expect_word(&args[0])?;
1377            Ok(Value::Lit(Literal::LitWord(w & 0xFF)))
1378        }
1379        PrimOpKind::Word64And => {
1380            let (a, b) = bin_op_word(op, &args)?;
1381            Ok(Value::Lit(Literal::LitWord(a & b)))
1382        }
1383        PrimOpKind::Int64ToInt => {
1384            // Identity on 64-bit
1385            Ok(args[0].clone())
1386        }
1387        PrimOpKind::Word64ToInt64 => {
1388            // Identity on 64-bit (reinterpret Word64 as Int64)
1389            let w = match &args[0] {
1390                Value::Lit(Literal::LitWord(w)) => *w,
1391                Value::Lit(Literal::LitInt(i)) => *i as u64,
1392                other => {
1393                    return Err(EvalError::TypeMismatch {
1394                        expected: "Word64#",
1395                        got: crate::error::ValueKind::Other(format!("{:?}", other)),
1396                    })
1397                }
1398            };
1399            Ok(Value::Lit(Literal::LitInt(w as i64)))
1400        }
1401        PrimOpKind::Word8ToWord => {
1402            // Widen Word8 to Word (identity, already fits)
1403            Ok(args[0].clone())
1404        }
1405        PrimOpKind::Word8Lt => {
1406            let (a, b) = bin_op_word(op, &args)?;
1407            Ok(Value::Lit(Literal::LitInt(if a < b { 1 } else { 0 })))
1408        }
1409        PrimOpKind::Int64Ge => {
1410            let (a, b) = bin_op_int(op, &args)?;
1411            Ok(Value::Lit(Literal::LitInt(if a >= b { 1 } else { 0 })))
1412        }
1413        PrimOpKind::Int64Negate => {
1414            let a = expect_int_like(&args[0])?;
1415            Ok(Value::Lit(Literal::LitInt(-a)))
1416        }
1417        PrimOpKind::Int64Shra => {
1418            let (a, b) = bin_op_int(op, &args)?;
1419            Ok(Value::Lit(Literal::LitInt(a >> (b as u32))))
1420        }
1421        PrimOpKind::Word64Shl => {
1422            let (a, b) = bin_op_word(op, &args)?;
1423            Ok(Value::Lit(Literal::LitWord(a << (b as u32))))
1424        }
1425        PrimOpKind::Word8Ge => {
1426            let (a, b) = bin_op_word(op, &args)?;
1427            Ok(Value::Lit(Literal::LitInt(if a >= b { 1 } else { 0 })))
1428        }
1429        PrimOpKind::Word8Sub => {
1430            let (a, b) = bin_op_word(op, &args)?;
1431            Ok(Value::Lit(Literal::LitWord(a.wrapping_sub(b) & 0xFF)))
1432        }
1433        PrimOpKind::SizeofByteArray => {
1434            let ba = expect_byte_array(&args[0])?;
1435            let len = ba
1436                .lock()
1437                .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?
1438                .len();
1439            Ok(Value::Lit(Literal::LitInt(len as i64)))
1440        }
1441        PrimOpKind::IndexWordArray => {
1442            // indexWordArray# :: ByteArray# -> Int# -> Word#
1443            // Read a machine word (8 bytes on 64-bit) at index i (word-sized offset)
1444            let ba = expect_byte_array(&args[0])?;
1445            let idx_val = expect_int_like(&args[1])?;
1446            if idx_val < 0 {
1447                return Err(EvalError::TypeMismatch {
1448                    expected: "non-negative array index",
1449                    got: crate::error::ValueKind::Other(format!("negative index: {}", idx_val)),
1450                });
1451            }
1452            let idx = idx_val as usize;
1453            let bytes = ba
1454                .lock()
1455                .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?;
1456            let offset = idx.checked_mul(8);
1457            let end = offset.and_then(|o| o.checked_add(8));
1458
1459            if offset.is_none() || end.is_none() || end.unwrap() > bytes.len() {
1460                return Err(EvalError::TypeMismatch {
1461                    expected: "valid IndexWordArray index",
1462                    got: crate::error::ValueKind::Other(format!(
1463                        "index {} out of bounds (len={})",
1464                        idx,
1465                        bytes.len()
1466                    )),
1467                });
1468            }
1469            let offset = offset.unwrap();
1470            let end = end.unwrap();
1471            let word =
1472                u64::from_ne_bytes(bytes[offset..end].try_into().map_err(|_| {
1473                    EvalError::InternalError("8-byte slice conversion failed".into())
1474                })?);
1475            Ok(Value::Lit(Literal::LitWord(word)))
1476        }
1477        PrimOpKind::Int64Mul => {
1478            let (a, b) = bin_op_int(op, &args)?;
1479            Ok(Value::Lit(Literal::LitInt(a.wrapping_mul(b))))
1480        }
1481        PrimOpKind::Word64Or => {
1482            let (a, b) = bin_op_word(op, &args)?;
1483            Ok(Value::Lit(Literal::LitWord(a | b)))
1484        }
1485        PrimOpKind::Word8Le => {
1486            let (a, b) = bin_op_word(op, &args)?;
1487            Ok(Value::Lit(Literal::LitInt(if a <= b { 1 } else { 0 })))
1488        }
1489        PrimOpKind::Int64Add => {
1490            let (a, b) = bin_op_int(op, &args)?;
1491            Ok(Value::Lit(Literal::LitInt(a.wrapping_add(b))))
1492        }
1493        PrimOpKind::Int64Gt => {
1494            let (a, b) = bin_op_int(op, &args)?;
1495            Ok(Value::Lit(Literal::LitInt(if a > b { 1 } else { 0 })))
1496        }
1497        PrimOpKind::Int64Lt => {
1498            let (a, b) = bin_op_int(op, &args)?;
1499            Ok(Value::Lit(Literal::LitInt(if a < b { 1 } else { 0 })))
1500        }
1501        PrimOpKind::Int64Le => {
1502            let (a, b) = bin_op_int(op, &args)?;
1503            Ok(Value::Lit(Literal::LitInt(if a <= b { 1 } else { 0 })))
1504        }
1505        PrimOpKind::Int64Sub => {
1506            let (a, b) = bin_op_int(op, &args)?;
1507            Ok(Value::Lit(Literal::LitInt(a.wrapping_sub(b))))
1508        }
1509        PrimOpKind::Int64Shl => {
1510            let (a, b) = bin_op_int(op, &args)?;
1511            Ok(Value::Lit(Literal::LitInt(a.wrapping_shl(b as u32))))
1512        }
1513        PrimOpKind::Word8Add => {
1514            let (a, b) = bin_op_word(op, &args)?;
1515            Ok(Value::Lit(Literal::LitWord((a.wrapping_add(b)) & 0xFF)))
1516        }
1517        PrimOpKind::WriteWordArray => {
1518            // writeWordArray# :: MutableByteArray# -> Int# -> Word# -> State# -> State#
1519            let ba = expect_byte_array(&args[0])?;
1520            let idx_val = expect_int_like(&args[1])?;
1521            if idx_val < 0 {
1522                return Err(EvalError::TypeMismatch {
1523                    expected: "non-negative array index",
1524                    got: crate::error::ValueKind::Other(format!("negative index: {}", idx_val)),
1525                });
1526            }
1527            let idx = idx_val as usize;
1528            let val = expect_word(&args[2])?;
1529            let mut bytes = ba
1530                .lock()
1531                .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?;
1532
1533            let offset = idx.checked_mul(8);
1534            let end = offset.and_then(|o| o.checked_add(8));
1535
1536            if let (Some(o), Some(e)) = (offset, end) {
1537                if e <= bytes.len() {
1538                    bytes[o..e].copy_from_slice(&val.to_ne_bytes());
1539                }
1540            }
1541            Ok(Value::ByteArray(ba.clone()))
1542        }
1543        PrimOpKind::ReadWordArray => {
1544            // readWordArray# :: MutableByteArray# -> Int# -> State# -> (# State#, Word# #)
1545            let ba = expect_byte_array(&args[0])?;
1546            let idx_val = expect_int_like(&args[1])?;
1547            if idx_val < 0 {
1548                return Err(EvalError::TypeMismatch {
1549                    expected: "non-negative array index",
1550                    got: crate::error::ValueKind::Other(format!("negative index: {}", idx_val)),
1551                });
1552            }
1553            let idx = idx_val as usize;
1554            let bytes = ba
1555                .lock()
1556                .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?;
1557            let offset = idx.checked_mul(8);
1558            let end = offset.and_then(|o| o.checked_add(8));
1559
1560            if offset.is_none() || end.is_none() || end.unwrap() > bytes.len() {
1561                return Err(EvalError::TypeMismatch {
1562                    expected: "valid ReadWordArray index",
1563                    got: crate::error::ValueKind::Other(format!(
1564                        "index {} out of bounds (len={})",
1565                        idx,
1566                        bytes.len()
1567                    )),
1568                });
1569            }
1570            let offset = offset.unwrap();
1571            let end = end.unwrap();
1572            let word =
1573                u64::from_ne_bytes(bytes[offset..end].try_into().map_err(|_| {
1574                    EvalError::InternalError("8-byte slice conversion failed".into())
1575                })?);
1576            Ok(Value::Lit(Literal::LitWord(word)))
1577        }
1578        PrimOpKind::SetByteArray => {
1579            // setByteArray# :: MutableByteArray# -> Int# -> Int# -> Int# -> State# -> State#
1580            // Fill `count` bytes starting at `offset` with byte `val`
1581            let ba = expect_byte_array(&args[0])?;
1582            let offset = expect_int_like(&args[1])? as usize;
1583            let count = expect_int_like(&args[2])? as usize;
1584            let val = expect_int_like(&args[3])? as u8;
1585            let mut bytes = ba
1586                .lock()
1587                .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?;
1588            if offset > bytes.len() {
1589                return Err(EvalError::TypeMismatch {
1590                    expected: "valid byte offset",
1591                    got: crate::error::ValueKind::Other(format!(
1592                        "offset {} exceeds length {}",
1593                        offset,
1594                        bytes.len()
1595                    )),
1596                });
1597            }
1598            let end = offset
1599                .checked_add(count)
1600                .unwrap_or(bytes.len())
1601                .min(bytes.len());
1602            for b in &mut bytes[offset..end] {
1603                *b = val;
1604            }
1605            Ok(Value::ByteArray(ba.clone()))
1606        }
1607        PrimOpKind::AddIntCVal => {
1608            // addIntC# returns (# result, carry #). This is the result component.
1609            let (a, b) = bin_op_int(op, &args)?;
1610            Ok(Value::Lit(Literal::LitInt(a.wrapping_add(b))))
1611        }
1612        PrimOpKind::AddIntCCarry => {
1613            // addIntC# carry flag: 1 if overflow, 0 otherwise
1614            let (a, b) = bin_op_int(op, &args)?;
1615            let overflow = a.checked_add(b).is_none();
1616            Ok(Value::Lit(Literal::LitInt(if overflow { 1 } else { 0 })))
1617        }
1618        PrimOpKind::SubWordCVal => {
1619            // subWordC# result component: a - b (wrapping)
1620            let (a, b) = bin_op_word(op, &args)?;
1621            Ok(Value::Lit(Literal::LitWord(a.wrapping_sub(b))))
1622        }
1623        PrimOpKind::SubWordCCarry => {
1624            // subWordC# carry: 1 if borrow (a < b), 0 otherwise
1625            let (a, b) = bin_op_word(op, &args)?;
1626            Ok(Value::Lit(Literal::LitInt(if a < b { 1 } else { 0 })))
1627        }
1628        PrimOpKind::AddWordCVal => {
1629            let (a, b) = bin_op_word(op, &args)?;
1630            Ok(Value::Lit(Literal::LitWord(a.wrapping_add(b))))
1631        }
1632        PrimOpKind::AddWordCCarry => {
1633            let (a, b) = bin_op_word(op, &args)?;
1634            let carry = a.checked_add(b).is_none();
1635            Ok(Value::Lit(Literal::LitInt(if carry { 1 } else { 0 })))
1636        }
1637        PrimOpKind::TimesWord2Hi => {
1638            // timesWord2# high word: (a * b) >> 64
1639            let (a, b) = bin_op_word(op, &args)?;
1640            let product = (a as u128) * (b as u128);
1641            Ok(Value::Lit(Literal::LitWord((product >> 64) as u64)))
1642        }
1643        PrimOpKind::TimesWord2Lo => {
1644            // timesWord2# low word: (a * b) & 0xFFFFFFFFFFFFFFFF
1645            let (a, b) = bin_op_word(op, &args)?;
1646            Ok(Value::Lit(Literal::LitWord(a.wrapping_mul(b))))
1647        }
1648        PrimOpKind::QuotRemWordVal => {
1649            // quotRemWord# quotient: a / b
1650            let (a, b) = bin_op_word(op, &args)?;
1651            if b == 0 {
1652                return Err(EvalError::InternalError(
1653                    "division by zero (quotRemWord# quot)".into(),
1654                ));
1655            }
1656            Ok(Value::Lit(Literal::LitWord(a / b)))
1657        }
1658        PrimOpKind::QuotRemWordRem => {
1659            // quotRemWord# remainder: a % b
1660            let (a, b) = bin_op_word(op, &args)?;
1661            if b == 0 {
1662                return Err(EvalError::InternalError(
1663                    "division by zero (quotRemWord# rem)".into(),
1664                ));
1665            }
1666            Ok(Value::Lit(Literal::LitWord(a % b)))
1667        }
1668
1669        // --- FFI intrinsics ---
1670        PrimOpKind::FfiStrlen => {
1671            // strlen(Addr#) -> Int#: count bytes until null terminator
1672            let bytes = match &args[0] {
1673                Value::Lit(Literal::LitString(bs)) => bs,
1674                other => {
1675                    return Err(EvalError::TypeMismatch {
1676                        expected: "Addr# (LitString)",
1677                        got: crate::error::ValueKind::Other(format!("{:?}", other)),
1678                    })
1679                }
1680            };
1681            let len = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
1682            Ok(Value::Lit(Literal::LitInt(len as i64)))
1683        }
1684        PrimOpKind::FfiTextMeasureOff => {
1685            // _hs_text_measure_off(ByteArray#, off, len) -> Int#
1686            // Walk UTF-8 bytes counting `len` chars, return byte offset
1687            let ba = expect_byte_array(&args[0])?;
1688            let off_raw = expect_int_like(&args[1])?;
1689            let n_chars_raw = expect_int_like(&args[2])?;
1690            if off_raw < 0 || n_chars_raw < 0 {
1691                return Err(EvalError::TypeMismatch {
1692                    expected: "non-negative offset and length",
1693                    got: crate::error::ValueKind::Other(format!(
1694                        "off={}, n_chars={}",
1695                        off_raw, n_chars_raw
1696                    )),
1697                });
1698            }
1699            let off = off_raw as usize;
1700            let n_chars = n_chars_raw as usize;
1701            let bytes = ba
1702                .lock()
1703                .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?;
1704            if off > bytes.len() {
1705                return Err(EvalError::TypeMismatch {
1706                    expected: "valid byte offset",
1707                    got: crate::error::ValueKind::Other(format!(
1708                        "offset {} exceeds length {}",
1709                        off,
1710                        bytes.len()
1711                    )),
1712                });
1713            }
1714            let slice = &bytes[off..];
1715            let mut byte_count = 0usize;
1716            let mut chars_counted = 0usize;
1717            while chars_counted < n_chars && byte_count < slice.len() {
1718                let b = slice[byte_count];
1719                let char_len = if b < 0x80 {
1720                    1
1721                } else if b < 0xE0 {
1722                    2
1723                } else if b < 0xF0 {
1724                    3
1725                } else {
1726                    4
1727                };
1728                byte_count += char_len;
1729                chars_counted += 1;
1730            }
1731            Ok(Value::Lit(Literal::LitInt(byte_count as i64)))
1732        }
1733        PrimOpKind::FfiTextMemchr => {
1734            // _hs_text_memchr(ByteArray#, off, len, byte) -> Int#
1735            // Find byte in array starting at off, return RELATIVE offset from off, or -1
1736            // Matches C: ptr - (arr + off), NOT absolute position
1737            let ba = expect_byte_array(&args[0])?;
1738            let off_raw = expect_int_like(&args[1])?;
1739            let len_raw = expect_int_like(&args[2])?;
1740            if off_raw < 0 || len_raw < 0 {
1741                return Err(EvalError::TypeMismatch {
1742                    expected: "non-negative offset and length",
1743                    got: crate::error::ValueKind::Other(format!(
1744                        "off={}, len={}",
1745                        off_raw, len_raw
1746                    )),
1747                });
1748            }
1749            let off = off_raw as usize;
1750            let len = len_raw as usize;
1751            let needle = expect_int_like(&args[3])? as u8;
1752            let bytes = ba
1753                .lock()
1754                .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?;
1755            if off > bytes.len() {
1756                return Err(EvalError::TypeMismatch {
1757                    expected: "valid byte offset",
1758                    got: crate::error::ValueKind::Other(format!(
1759                        "offset {} exceeds length {}",
1760                        off,
1761                        bytes.len()
1762                    )),
1763                });
1764            }
1765            let end = off.checked_add(len).unwrap_or(bytes.len()).min(bytes.len());
1766            let result = bytes[off..end].iter().position(|&b| b == needle);
1767            Ok(Value::Lit(Literal::LitInt(match result {
1768                Some(pos) => pos as i64,
1769                None => -1,
1770            })))
1771        }
1772        PrimOpKind::FfiTextReverse => {
1773            // _hs_text_reverse(dst, src, off, len) -> void
1774            // Reverse UTF-8 chars from src[off..off+len] into dst
1775            let dst_ba = expect_byte_array(&args[0])?;
1776            let src_ba = expect_byte_array(&args[1])?;
1777            let off_raw = expect_int_like(&args[2])?;
1778            let len_raw = expect_int_like(&args[3])?;
1779            if off_raw < 0 || len_raw < 0 {
1780                return Err(EvalError::TypeMismatch {
1781                    expected: "non-negative offset and length",
1782                    got: crate::error::ValueKind::Other(format!(
1783                        "off={}, len={}",
1784                        off_raw, len_raw
1785                    )),
1786                });
1787            }
1788            let off = off_raw as usize;
1789            let len = len_raw as usize;
1790            // Clone src to avoid double-lock if src == dst
1791            let src_slice: Vec<u8> = {
1792                let src = src_ba
1793                    .lock()
1794                    .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?;
1795                let end = off.checked_add(len);
1796                if end.is_none() || end.unwrap() > src.len() {
1797                    return Err(EvalError::TypeMismatch {
1798                        expected: "valid byte range",
1799                        got: crate::error::ValueKind::Other(format!(
1800                            "range {}..{} exceeds length {}",
1801                            off,
1802                            off + len,
1803                            src.len()
1804                        )),
1805                    });
1806                }
1807                src[off..end.unwrap()].to_vec()
1808            };
1809            // Parse UTF-8 chars and reverse
1810            let mut chars: Vec<&[u8]> = Vec::new();
1811            let mut i = 0;
1812            while i < src_slice.len() {
1813                let b = src_slice[i];
1814                let char_len = if b < 0x80 {
1815                    1
1816                } else if b < 0xE0 {
1817                    2
1818                } else if b < 0xF0 {
1819                    3
1820                } else {
1821                    4
1822                };
1823                let end = std::cmp::min(i + char_len, src_slice.len());
1824                chars.push(&src_slice[i..end]);
1825                i = end;
1826            }
1827            chars.reverse();
1828            let reversed: Vec<u8> = chars.into_iter().flatten().copied().collect();
1829            {
1830                let mut dst = dst_ba
1831                    .lock()
1832                    .map_err(|e| EvalError::InternalError(format!("mutex poisoned: {e}")))?;
1833                let copy_len = std::cmp::min(reversed.len(), dst.len());
1834                dst[..copy_len].copy_from_slice(&reversed[..copy_len]);
1835            }
1836            Ok(Value::ByteArray(dst_ba.clone()))
1837        }
1838
1839        // SmallArray# / Array# / PopCnt / Ctz — only used by JIT, not tree-walker
1840        PrimOpKind::NewSmallArray
1841        | PrimOpKind::ReadSmallArray
1842        | PrimOpKind::WriteSmallArray
1843        | PrimOpKind::IndexSmallArray
1844        | PrimOpKind::SizeofSmallArray
1845        | PrimOpKind::SizeofSmallMutableArray
1846        | PrimOpKind::UnsafeFreezeSmallArray
1847        | PrimOpKind::UnsafeThawSmallArray
1848        | PrimOpKind::CopySmallArray
1849        | PrimOpKind::CopySmallMutableArray
1850        | PrimOpKind::CloneSmallArray
1851        | PrimOpKind::CloneSmallMutableArray
1852        | PrimOpKind::ShrinkSmallMutableArray
1853        | PrimOpKind::CasSmallArray
1854        | PrimOpKind::NewArray
1855        | PrimOpKind::ReadArray
1856        | PrimOpKind::WriteArray
1857        | PrimOpKind::SizeofArray
1858        | PrimOpKind::SizeofMutableArray
1859        | PrimOpKind::UnsafeFreezeArray
1860        | PrimOpKind::UnsafeThawArray
1861        | PrimOpKind::CopyArray
1862        | PrimOpKind::CopyMutableArray
1863        | PrimOpKind::CloneArray
1864        | PrimOpKind::CloneMutableArray
1865        | PrimOpKind::PopCnt
1866        | PrimOpKind::PopCnt8
1867        | PrimOpKind::PopCnt16
1868        | PrimOpKind::PopCnt32
1869        | PrimOpKind::PopCnt64
1870        | PrimOpKind::Ctz
1871        | PrimOpKind::Ctz8
1872        | PrimOpKind::Ctz16
1873        | PrimOpKind::Ctz32
1874        | PrimOpKind::Ctz64 => Err(EvalError::UnsupportedPrimOp(op)),
1875    }
1876}
1877
1878fn expect_byte_array(v: &Value) -> Result<&crate::value::SharedByteArray, EvalError> {
1879    if let Value::ByteArray(ba) = v {
1880        Ok(ba)
1881    } else {
1882        Err(EvalError::TypeMismatch {
1883            expected: "ByteArray#",
1884            got: crate::error::ValueKind::Other(format!("{:?}", v)),
1885        })
1886    }
1887}
1888
1889/// Accept both LitInt and LitWord — FFI args go through Int→Int64→Word64 conversions
1890fn expect_int_like(v: &Value) -> Result<i64, EvalError> {
1891    match v {
1892        Value::Lit(Literal::LitInt(n)) => Ok(*n),
1893        Value::Lit(Literal::LitWord(n)) => Ok(*n as i64),
1894        _ => Err(EvalError::TypeMismatch {
1895            expected: "Int# or Word#",
1896            got: crate::error::ValueKind::Other(format!("{:?}", v)),
1897        }),
1898    }
1899}
1900
1901fn expect_int(v: &Value) -> Result<i64, EvalError> {
1902    if let Value::Lit(Literal::LitInt(n)) = v {
1903        Ok(*n)
1904    } else {
1905        Err(EvalError::TypeMismatch {
1906            expected: "Int#",
1907            got: crate::error::ValueKind::Other(format!("{:?}", v)),
1908        })
1909    }
1910}
1911
1912fn expect_word(v: &Value) -> Result<u64, EvalError> {
1913    match v {
1914        Value::Lit(Literal::LitWord(n)) => Ok(*n),
1915        Value::Lit(Literal::LitInt(n)) => Ok(*n as u64),
1916        _ => Err(EvalError::TypeMismatch {
1917            expected: "Word#",
1918            got: crate::error::ValueKind::Other(format!("{:?}", v)),
1919        }),
1920    }
1921}
1922
1923fn expect_double(v: &Value) -> Result<f64, EvalError> {
1924    if let Value::Lit(Literal::LitDouble(bits)) = v {
1925        Ok(f64::from_bits(*bits))
1926    } else {
1927        Err(EvalError::TypeMismatch {
1928            expected: "Double#",
1929            got: crate::error::ValueKind::Other(format!("{:?}", v)),
1930        })
1931    }
1932}
1933
1934fn expect_float(v: &Value) -> Result<f32, EvalError> {
1935    if let Value::Lit(Literal::LitFloat(bits)) = v {
1936        Ok(f32::from_bits(*bits as u32))
1937    } else {
1938        Err(EvalError::TypeMismatch {
1939            expected: "Float#",
1940            got: crate::error::ValueKind::Other(format!("{:?}", v)),
1941        })
1942    }
1943}
1944
1945fn expect_char(v: &Value) -> Result<char, EvalError> {
1946    if let Value::Lit(Literal::LitChar(c)) = v {
1947        Ok(*c)
1948    } else {
1949        Err(EvalError::TypeMismatch {
1950            expected: "Char#",
1951            got: crate::error::ValueKind::Other(format!("{:?}", v)),
1952        })
1953    }
1954}
1955
1956macro_rules! bin_op {
1957    ($name:ident, $extract:ident, $t:ty) => {
1958        fn $name(_op: PrimOpKind, args: &[Value]) -> Result<($t, $t), EvalError> {
1959            if args.len() != 2 {
1960                return Err(EvalError::ArityMismatch {
1961                    context: "arguments",
1962                    expected: 2,
1963                    got: args.len(),
1964                });
1965            }
1966            Ok(($extract(&args[0])?, $extract(&args[1])?))
1967        }
1968    };
1969}
1970
1971bin_op!(bin_op_int, expect_int, i64);
1972bin_op!(bin_op_word, expect_word, u64);
1973
1974fn eval_decode_double_int64(d: f64) -> (i64, i64) {
1975    if d == 0.0 || d.is_nan() {
1976        return (0, 0);
1977    }
1978    if d.is_infinite() {
1979        return (if d > 0.0 { 1 } else { -1 }, 0);
1980    }
1981    let bits = d.to_bits();
1982    let sign: i64 = if bits >> 63 == 0 { 1 } else { -1 };
1983    let raw_exp = ((bits >> 52) & 0x7ff) as i32;
1984    let raw_man = (bits & 0x000f_ffff_ffff_ffff) as i64;
1985    let (man, exp) = if raw_exp == 0 {
1986        (raw_man, 1 - 1023 - 52)
1987    } else {
1988        (raw_man | (1i64 << 52), raw_exp - 1023 - 52)
1989    };
1990    let man = sign * man;
1991    if man != 0 {
1992        let tz = man.unsigned_abs().trailing_zeros();
1993        (man >> tz, (exp + tz as i32) as i64)
1994    } else {
1995        (0, 0)
1996    }
1997}
1998
1999/// Format a Double matching Haskell's `show` output.
2000fn eval_haskell_show_double(d: f64) -> String {
2001    if d.is_nan() {
2002        return "NaN".to_string();
2003    }
2004    if d.is_infinite() {
2005        return if d > 0.0 { "Infinity" } else { "-Infinity" }.to_string();
2006    }
2007    if d == 0.0 {
2008        return if d.is_sign_negative() { "-0.0" } else { "0.0" }.to_string();
2009    }
2010    let abs = d.abs();
2011    if (0.1..1.0e7).contains(&abs) {
2012        let s = format!("{}", d);
2013        if s.contains('.') {
2014            s
2015        } else {
2016            format!("{}.0", s)
2017        }
2018    } else {
2019        format!("{:e}", d)
2020    }
2021}
2022
2023bin_op!(bin_op_double, expect_double, f64);
2024bin_op!(bin_op_float, expect_float, f32);
2025bin_op!(bin_op_char, expect_char, char);
2026
2027macro_rules! cmp_fn {
2028    ($name:ident, $bin_op:ident, $t:ty) => {
2029        fn $name(
2030            op: PrimOpKind,
2031            args: &[Value],
2032            f: impl Fn($t, $t) -> bool,
2033        ) -> Result<Value, EvalError> {
2034            let (a, b) = $bin_op(op, args)?;
2035            Ok(Value::Lit(Literal::LitInt(if f(a, b) { 1 } else { 0 })))
2036        }
2037    };
2038}
2039
2040cmp_fn!(cmp_int, bin_op_int, i64);
2041cmp_fn!(cmp_word, bin_op_word, u64);
2042cmp_fn!(cmp_double, bin_op_double, f64);
2043cmp_fn!(cmp_float, bin_op_float, f32);
2044cmp_fn!(cmp_char, bin_op_char, char);
2045
2046#[cfg(test)]
2047mod tests {
2048    use super::*;
2049    use tidepool_repr::{Alt, AltCon, CoreFrame, DataConId, JoinId, Literal, RecursiveTree, VarId};
2050
2051    #[test]
2052    fn test_eval_lit() {
2053        let expr = RecursiveTree {
2054            nodes: vec![CoreFrame::Lit(Literal::LitInt(42))],
2055        };
2056        let mut heap = crate::heap::VecHeap::new();
2057        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2058        if let Value::Lit(Literal::LitInt(n)) = res {
2059            assert_eq!(n, 42);
2060        } else {
2061            panic!("Expected LitInt(42), got {:?}", res);
2062        }
2063    }
2064
2065    #[test]
2066    fn test_eval_var() {
2067        let expr = RecursiveTree {
2068            nodes: vec![CoreFrame::Var(VarId(1))],
2069        };
2070        let mut env = Env::new();
2071        env.insert(VarId(1), Value::Lit(Literal::LitInt(42)));
2072        let mut heap = crate::heap::VecHeap::new();
2073        let res = eval(&expr, &env, &mut heap).unwrap();
2074        if let Value::Lit(Literal::LitInt(n)) = res {
2075            assert_eq!(n, 42);
2076        } else {
2077            panic!("Expected LitInt(42), got {:?}", res);
2078        }
2079    }
2080
2081    #[test]
2082    fn test_eval_unbound_var() {
2083        let expr = RecursiveTree {
2084            nodes: vec![CoreFrame::Var(VarId(1))],
2085        };
2086        let mut heap = crate::heap::VecHeap::new();
2087        let res = eval(&expr, &Env::new(), &mut heap);
2088        assert!(matches!(res, Err(EvalError::UnboundVar(VarId(1)))));
2089    }
2090
2091    #[test]
2092    fn test_eval_lam_identity() {
2093        let nodes = vec![
2094            CoreFrame::Var(VarId(1)),
2095            CoreFrame::Lam {
2096                binder: VarId(1),
2097                body: 0,
2098            },
2099            CoreFrame::Lit(Literal::LitInt(42)),
2100            CoreFrame::App { fun: 1, arg: 2 },
2101        ];
2102        let expr = CoreExpr { nodes };
2103        let mut heap = crate::heap::VecHeap::new();
2104        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2105        if let Value::Lit(Literal::LitInt(n)) = res {
2106            assert_eq!(n, 42);
2107        } else {
2108            panic!("Expected LitInt(42), got {:?}", res);
2109        }
2110    }
2111
2112    #[test]
2113    fn test_eval_let_nonrec() {
2114        let nodes = vec![
2115            CoreFrame::Lit(Literal::LitInt(1)),
2116            CoreFrame::Var(VarId(1)),
2117            CoreFrame::LetNonRec {
2118                binder: VarId(1),
2119                rhs: 0,
2120                body: 1,
2121            },
2122        ];
2123        let expr = CoreExpr { nodes };
2124        let mut heap = crate::heap::VecHeap::new();
2125        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2126        if let Value::Lit(Literal::LitInt(n)) = res {
2127            assert_eq!(n, 1);
2128        } else {
2129            panic!("Expected LitInt(1), got {:?}", res);
2130        }
2131    }
2132
2133    #[test]
2134    fn test_eval_con() {
2135        let nodes = vec![
2136            CoreFrame::Lit(Literal::LitInt(42)),
2137            CoreFrame::Con {
2138                tag: DataConId(1),
2139                fields: vec![0],
2140            },
2141        ];
2142        let expr = CoreExpr { nodes };
2143        let mut heap = crate::heap::VecHeap::new();
2144        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2145        if let Value::Con(tag, fields) = res {
2146            assert_eq!(tag.0, 1);
2147            assert_eq!(fields.len(), 1);
2148            if let Value::Lit(Literal::LitInt(n)) = fields[0] {
2149                assert_eq!(n, 42);
2150            } else {
2151                panic!("Expected LitInt(42)");
2152            }
2153        } else {
2154            panic!("Expected Con");
2155        }
2156    }
2157
2158    #[test]
2159    fn test_eval_primop_add() {
2160        let nodes = vec![
2161            CoreFrame::Lit(Literal::LitInt(1)),
2162            CoreFrame::Lit(Literal::LitInt(2)),
2163            CoreFrame::PrimOp {
2164                op: PrimOpKind::IntAdd,
2165                args: vec![0, 1],
2166            },
2167        ];
2168        let expr = CoreExpr { nodes };
2169        let mut heap = crate::heap::VecHeap::new();
2170        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2171        if let Value::Lit(Literal::LitInt(n)) = res {
2172            assert_eq!(n, 3);
2173        } else {
2174            panic!("Expected LitInt(3)");
2175        }
2176    }
2177
2178    #[test]
2179    fn test_eval_case_data() {
2180        let nodes = vec![
2181            CoreFrame::Lit(Literal::LitInt(42)),
2182            CoreFrame::Con {
2183                tag: DataConId(1),
2184                fields: vec![0],
2185            },
2186            CoreFrame::Var(VarId(10)),
2187            CoreFrame::Case {
2188                scrutinee: 1,
2189                binder: VarId(11),
2190                alts: vec![Alt {
2191                    con: AltCon::DataAlt(DataConId(1)),
2192                    binders: vec![VarId(10)],
2193                    body: 2,
2194                }],
2195            },
2196        ];
2197        let expr = CoreExpr { nodes };
2198        let mut heap = crate::heap::VecHeap::new();
2199        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2200        if let Value::Lit(Literal::LitInt(n)) = res {
2201            assert_eq!(n, 42);
2202        } else {
2203            panic!("Expected LitInt(42)");
2204        }
2205    }
2206
2207    #[test]
2208    fn test_eval_case_binder() {
2209        let nodes = vec![
2210            CoreFrame::Lit(Literal::LitInt(42)),
2211            CoreFrame::Con {
2212                tag: DataConId(1),
2213                fields: vec![0],
2214            },
2215            CoreFrame::Var(VarId(11)),
2216            CoreFrame::Case {
2217                scrutinee: 1,
2218                binder: VarId(11),
2219                alts: vec![Alt {
2220                    con: AltCon::DataAlt(DataConId(1)),
2221                    binders: vec![VarId(10)],
2222                    body: 2,
2223                }],
2224            },
2225        ];
2226        let expr = CoreExpr { nodes };
2227        let mut heap = crate::heap::VecHeap::new();
2228        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2229        if let Value::Con(tag, _) = res {
2230            assert_eq!(tag.0, 1);
2231        } else {
2232            panic!("Expected Con");
2233        }
2234    }
2235
2236    #[test]
2237    fn test_eval_case_lit_default() {
2238        let nodes = vec![
2239            CoreFrame::Lit(Literal::LitInt(3)),
2240            CoreFrame::Lit(Literal::LitInt(10)),
2241            CoreFrame::Lit(Literal::LitInt(20)),
2242            CoreFrame::Lit(Literal::LitInt(30)),
2243            CoreFrame::Case {
2244                scrutinee: 0,
2245                binder: VarId(10),
2246                alts: vec![
2247                    Alt {
2248                        con: AltCon::LitAlt(Literal::LitInt(1)),
2249                        binders: vec![],
2250                        body: 1,
2251                    },
2252                    Alt {
2253                        con: AltCon::LitAlt(Literal::LitInt(2)),
2254                        binders: vec![],
2255                        body: 2,
2256                    },
2257                    Alt {
2258                        con: AltCon::Default,
2259                        binders: vec![],
2260                        body: 3,
2261                    },
2262                ],
2263            },
2264        ];
2265        let expr = CoreExpr { nodes };
2266        let mut heap = crate::heap::VecHeap::new();
2267        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2268        if let Value::Lit(Literal::LitInt(n)) = res {
2269            assert_eq!(n, 30);
2270        } else {
2271            panic!("Expected LitInt(30)");
2272        }
2273    }
2274
2275    #[test]
2276    fn test_eval_data_to_tag() {
2277        let nodes = vec![
2278            CoreFrame::Con {
2279                tag: DataConId(5),
2280                fields: vec![],
2281            },
2282            CoreFrame::PrimOp {
2283                op: PrimOpKind::DataToTag,
2284                args: vec![0],
2285            },
2286        ];
2287        let expr = CoreExpr { nodes };
2288        let mut heap = crate::heap::VecHeap::new();
2289        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2290        if let Value::Lit(Literal::LitInt(n)) = res {
2291            assert_eq!(n, 5);
2292        } else {
2293            panic!("Expected LitInt(5)");
2294        }
2295    }
2296
2297    #[test]
2298    fn test_eval_let_rec_forward_refs() {
2299        // let { x = 1; y = x } in y
2300        let nodes = vec![
2301            CoreFrame::Lit(Literal::LitInt(1)), // 0
2302            CoreFrame::Var(VarId(1)),           // 1: x
2303            CoreFrame::Var(VarId(2)),           // 2: y
2304            CoreFrame::LetRec {
2305                bindings: vec![(VarId(1), 0), (VarId(2), 1)],
2306                body: 2,
2307            }, // 3
2308        ];
2309        let expr = CoreExpr { nodes };
2310        let mut heap = crate::heap::VecHeap::new();
2311        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2312        if let Value::Lit(Literal::LitInt(n)) = res {
2313            assert_eq!(n, 1);
2314        } else {
2315            panic!("Expected LitInt(1)");
2316        }
2317    }
2318
2319    #[test]
2320    fn test_eval_join_simple() {
2321        // join j(x) = x in jump j(42)
2322        let nodes = vec![
2323            CoreFrame::Var(VarId(10)),           // 0: x
2324            CoreFrame::Lit(Literal::LitInt(42)), // 1
2325            CoreFrame::Jump {
2326                label: JoinId(1),
2327                args: vec![1],
2328            }, // 2
2329            CoreFrame::Join {
2330                label: JoinId(1),
2331                params: vec![VarId(10)],
2332                rhs: 0,
2333                body: 2,
2334            }, // 3
2335        ];
2336        let expr = CoreExpr { nodes };
2337        let mut heap = crate::heap::VecHeap::new();
2338        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2339        if let Value::Lit(Literal::LitInt(n)) = res {
2340            assert_eq!(n, 42);
2341        } else {
2342            panic!("Expected LitInt(42), got {:?}", res);
2343        }
2344    }
2345
2346    #[test]
2347    fn test_eval_join_multi_param() {
2348        // join j(x, y) = x + y in jump j(1, 2)
2349        let nodes = vec![
2350            CoreFrame::Var(VarId(10)), // 0: x
2351            CoreFrame::Var(VarId(11)), // 1: y
2352            CoreFrame::PrimOp {
2353                op: PrimOpKind::IntAdd,
2354                args: vec![0, 1],
2355            }, // 2: x + y
2356            CoreFrame::Lit(Literal::LitInt(1)), // 3
2357            CoreFrame::Lit(Literal::LitInt(2)), // 4
2358            CoreFrame::Jump {
2359                label: JoinId(1),
2360                args: vec![3, 4],
2361            }, // 5
2362            CoreFrame::Join {
2363                label: JoinId(1),
2364                params: vec![VarId(10), VarId(11)],
2365                rhs: 2,
2366                body: 5,
2367            }, // 6
2368        ];
2369        let expr = CoreExpr { nodes };
2370        let mut heap = crate::heap::VecHeap::new();
2371        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2372        if let Value::Lit(Literal::LitInt(n)) = res {
2373            assert_eq!(n, 3);
2374        } else {
2375            panic!("Expected LitInt(3)");
2376        }
2377    }
2378
2379    #[test]
2380    fn test_eval_unbound_jump() {
2381        let nodes = vec![CoreFrame::Jump {
2382            label: JoinId(1),
2383            args: vec![],
2384        }];
2385        let expr = CoreExpr { nodes };
2386        let mut heap = crate::heap::VecHeap::new();
2387        let res = eval(&expr, &Env::new(), &mut heap);
2388        assert!(matches!(res, Err(EvalError::UnboundJoin(JoinId(1)))));
2389    }
2390
2391    #[test]
2392    fn test_thunk_lazy() {
2393        // let x = <unbound> in 42
2394        let nodes = vec![
2395            CoreFrame::Var(VarId(999)),          // 0: unbound
2396            CoreFrame::Lit(Literal::LitInt(42)), // 1
2397            CoreFrame::LetNonRec {
2398                binder: VarId(1),
2399                rhs: 0,
2400                body: 1,
2401            }, // 2
2402        ];
2403        let expr = CoreExpr { nodes };
2404        let mut heap = crate::heap::VecHeap::new();
2405        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2406        if let Value::Lit(Literal::LitInt(n)) = res {
2407            assert_eq!(n, 42);
2408        } else {
2409            panic!("Expected LitInt(42)");
2410        }
2411    }
2412
2413    #[test]
2414    fn test_thunk_caching() {
2415        // let x = 1 + 1 in x + x
2416        let nodes = vec![
2417            CoreFrame::Lit(Literal::LitInt(1)), // 0
2418            CoreFrame::PrimOp {
2419                op: PrimOpKind::IntAdd,
2420                args: vec![0, 0],
2421            }, // 1: 1 + 1
2422            CoreFrame::Var(VarId(1)),           // 2: x
2423            CoreFrame::PrimOp {
2424                op: PrimOpKind::IntAdd,
2425                args: vec![2, 2],
2426            }, // 3: x + x
2427            CoreFrame::LetNonRec {
2428                binder: VarId(1),
2429                rhs: 1,
2430                body: 3,
2431            }, // 4
2432        ];
2433        let expr = CoreExpr { nodes };
2434        let mut heap = crate::heap::VecHeap::new();
2435        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2436        if let Value::Lit(Literal::LitInt(n)) = res {
2437            assert_eq!(n, 4);
2438        } else {
2439            panic!("Expected LitInt(4)");
2440        }
2441    }
2442
2443    #[test]
2444    fn test_thunk_blackhole() {
2445        // letrec x = x in x
2446        let nodes = vec![
2447            CoreFrame::Var(VarId(1)), // 0: x
2448            CoreFrame::LetRec {
2449                bindings: vec![(VarId(1), 0)],
2450                body: 0,
2451            }, // 1
2452        ];
2453        let expr = CoreExpr { nodes };
2454        let mut heap = crate::heap::VecHeap::new();
2455        let res = eval(&expr, &Env::new(), &mut heap);
2456        assert!(matches!(res, Err(EvalError::InfiniteLoop(_))));
2457    }
2458
2459    #[test]
2460    fn test_letrec_mutual_recursion() {
2461        // letrec { f = \a -> g a; g = \b -> b } in f 42
2462        let nodes = vec![
2463            CoreFrame::Var(VarId(11)),         // 0: a
2464            CoreFrame::Var(VarId(2)),          // 1: g
2465            CoreFrame::App { fun: 1, arg: 0 }, // 2: g a
2466            CoreFrame::Lam {
2467                binder: VarId(11),
2468                body: 2,
2469            }, // 3: \a -> g a (f)
2470            CoreFrame::Var(VarId(12)),         // 4: b
2471            CoreFrame::Lam {
2472                binder: VarId(12),
2473                body: 4,
2474            }, // 5: \b -> b (g)
2475            CoreFrame::Var(VarId(1)),          // 6: f
2476            CoreFrame::Lit(Literal::LitInt(42)), // 7
2477            CoreFrame::App { fun: 6, arg: 7 }, // 8: f 42
2478            CoreFrame::LetRec {
2479                bindings: vec![(VarId(1), 3), (VarId(2), 5)],
2480                body: 8,
2481            }, // 9
2482        ];
2483        let expr = CoreExpr { nodes };
2484        let mut heap = crate::heap::VecHeap::new();
2485        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2486        if let Value::Lit(Literal::LitInt(n)) = res {
2487            assert_eq!(n, 42);
2488        } else {
2489            panic!("Expected LitInt(42)");
2490        }
2491    }
2492
2493    #[test]
2494    fn test_eval_join_scoping() {
2495        // let y = 100 in
2496        // join j(x) = x + y in
2497        // let y = 200 in
2498        // jump j(1)
2499        // Should be 101, not 201.
2500        let nodes = vec![
2501            CoreFrame::Lit(Literal::LitInt(100)), // 0
2502            CoreFrame::Var(VarId(10)),            // 1: x
2503            CoreFrame::Var(VarId(20)),            // 2: y (captured)
2504            CoreFrame::PrimOp {
2505                op: PrimOpKind::IntAdd,
2506                args: vec![1, 2],
2507            }, // 3: x + y
2508            CoreFrame::Lit(Literal::LitInt(200)), // 4
2509            CoreFrame::Lit(Literal::LitInt(1)),   // 5
2510            CoreFrame::Jump {
2511                label: JoinId(1),
2512                args: vec![5],
2513            }, // 6
2514            CoreFrame::LetNonRec {
2515                binder: VarId(20),
2516                rhs: 4,
2517                body: 6,
2518            }, // 7: let y = 200 in jump j(1)
2519            CoreFrame::Join {
2520                label: JoinId(1),
2521                params: vec![VarId(10)],
2522                rhs: 3,
2523                body: 7,
2524            }, // 8: join j(x) = x+y in ...
2525            CoreFrame::LetNonRec {
2526                binder: VarId(20),
2527                rhs: 0,
2528                body: 8,
2529            }, // 9: let y = 100 in join ...
2530        ];
2531        let expr = CoreExpr { nodes };
2532        let mut heap = crate::heap::VecHeap::new();
2533        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2534        if let Value::Lit(Literal::LitInt(n)) = res {
2535            assert_eq!(n, 101);
2536        } else {
2537            panic!("Expected LitInt(101)");
2538        }
2539    }
2540
2541    #[test]
2542    fn test_thunk_poison_restoration() {
2543        // let x = <unbound> in x
2544        let nodes = vec![
2545            CoreFrame::Var(VarId(999)), // 0: unbound
2546            CoreFrame::LetNonRec {
2547                binder: VarId(1),
2548                rhs: 0,
2549                body: 0,
2550            }, // 1: let x = unbound in x
2551        ];
2552        let expr = CoreExpr { nodes };
2553        let mut heap = crate::heap::VecHeap::new();
2554
2555        // First force fails with UnboundVar
2556        let res1 = eval(&expr, &Env::new(), &mut heap);
2557        assert!(matches!(res1, Err(EvalError::UnboundVar(_))));
2558
2559        // Second force should ALSO fail with UnboundVar, NOT InfiniteLoop (BlackHole)
2560        let res2 = eval(&expr, &Env::new(), &mut heap);
2561        assert!(matches!(res2, Err(EvalError::UnboundVar(_))));
2562    }
2563
2564    #[test]
2565    fn test_eval_jump_arity_mismatch() {
2566        // join j(x) = x in jump j(1, 2)
2567        let nodes = vec![
2568            CoreFrame::Var(VarId(10)),          // 0: x
2569            CoreFrame::Lit(Literal::LitInt(1)), // 1
2570            CoreFrame::Lit(Literal::LitInt(2)), // 2
2571            CoreFrame::Jump {
2572                label: JoinId(1),
2573                args: vec![1, 2],
2574            }, // 3: jump j(1, 2)
2575            CoreFrame::Join {
2576                label: JoinId(1),
2577                params: vec![VarId(10)],
2578                rhs: 0,
2579                body: 3,
2580            }, // 4: join j(x) ...
2581        ];
2582        let expr = CoreExpr { nodes };
2583        let mut heap = crate::heap::VecHeap::new();
2584        let res = eval(&expr, &Env::new(), &mut heap);
2585        assert!(matches!(res, Err(EvalError::ArityMismatch { .. })));
2586    }
2587
2588    #[test]
2589    fn test_eval_case_no_match() {
2590        let nodes = vec![
2591            CoreFrame::Lit(Literal::LitInt(42)), // 0
2592            CoreFrame::Case {
2593                scrutinee: 0,
2594                binder: VarId(1),
2595                alts: vec![Alt {
2596                    con: AltCon::LitAlt(Literal::LitInt(1)),
2597                    binders: vec![],
2598                    body: 0,
2599                }],
2600            }, // 1
2601        ];
2602        let expr = CoreExpr { nodes };
2603        let mut heap = crate::heap::VecHeap::new();
2604        let res = eval(&expr, &Env::new(), &mut heap);
2605        assert!(matches!(res, Err(EvalError::NoMatchingAlt)));
2606    }
2607
2608    #[test]
2609    fn test_eval_primop_invalid_char() {
2610        let nodes = vec![
2611            CoreFrame::Lit(Literal::LitInt(0x110000)), // 0: invalid codepoint
2612            CoreFrame::PrimOp {
2613                op: PrimOpKind::Chr,
2614                args: vec![0],
2615            }, // 1: chr# 0x110000
2616        ];
2617        let expr = CoreExpr { nodes };
2618        let mut heap = crate::heap::VecHeap::new();
2619        let res = eval(&expr, &Env::new(), &mut heap);
2620        assert!(matches!(res, Err(EvalError::TypeMismatch { .. })));
2621    }
2622
2623    #[test]
2624    fn test_eval_primop_ffi_bounds() {
2625        let ba = std::sync::Arc::new(std::sync::Mutex::new(vec![0u8; 10]));
2626        let mut env = Env::new();
2627        env.insert(VarId(100), Value::ByteArray(ba));
2628
2629        let nodes = vec![
2630            CoreFrame::Var(VarId(100)),          // 0: ba
2631            CoreFrame::Lit(Literal::LitInt(20)), // 1: off
2632            CoreFrame::Lit(Literal::LitInt(1)),  // 2: n_chars
2633            CoreFrame::PrimOp {
2634                op: PrimOpKind::FfiTextMeasureOff,
2635                args: vec![0, 1, 2],
2636            }, // 3: _hs_text_measure_off ba 20 1
2637        ];
2638        let expr = CoreExpr { nodes };
2639        let mut heap = crate::heap::VecHeap::new();
2640        let res = eval(&expr, &env, &mut heap);
2641        assert!(matches!(res, Err(EvalError::TypeMismatch { .. })));
2642    }
2643
2644    #[test]
2645    fn test_eval_primop_ffi_text_reverse_negative() {
2646        let src = std::sync::Arc::new(std::sync::Mutex::new(vec![b'a', b'b', b'c']));
2647        let dst = std::sync::Arc::new(std::sync::Mutex::new(vec![0u8; 3]));
2648        let mut env = Env::new();
2649        env.insert(VarId(100), Value::ByteArray(dst));
2650        env.insert(VarId(101), Value::ByteArray(src));
2651
2652        let nodes = vec![
2653            CoreFrame::Var(VarId(100)),          // 0: dst
2654            CoreFrame::Var(VarId(101)),          // 1: src
2655            CoreFrame::Lit(Literal::LitInt(-1)), // 2: off (negative!)
2656            CoreFrame::Lit(Literal::LitInt(3)),  // 3: len
2657            CoreFrame::PrimOp {
2658                op: PrimOpKind::FfiTextReverse,
2659                args: vec![0, 1, 2, 3],
2660            },
2661        ];
2662        let expr = CoreExpr { nodes };
2663        let mut heap = crate::heap::VecHeap::new();
2664        let res = eval(&expr, &env, &mut heap);
2665        assert!(matches!(res, Err(EvalError::TypeMismatch { .. })));
2666    }
2667
2668    #[test]
2669    fn test_eval_primop_negative_index() {
2670        let ba = std::sync::Arc::new(std::sync::Mutex::new(vec![0u8; 16]));
2671        let mut env = Env::new();
2672        env.insert(VarId(100), Value::ByteArray(ba));
2673
2674        let nodes = vec![
2675            CoreFrame::Var(VarId(100)),          // 0: ba
2676            CoreFrame::Lit(Literal::LitInt(-1)), // 1: idx
2677            CoreFrame::PrimOp {
2678                op: PrimOpKind::IndexWordArray,
2679                args: vec![0, 1],
2680            }, // 2: indexWordArray# ba (-1)
2681        ];
2682        let expr = CoreExpr { nodes };
2683        let mut heap = crate::heap::VecHeap::new();
2684        let res = eval(&expr, &env, &mut heap);
2685        assert!(matches!(res, Err(EvalError::TypeMismatch { .. })));
2686    }
2687
2688    #[test]
2689    fn test_eval_primop_arity_mismatch() {
2690        let nodes = vec![
2691            CoreFrame::Lit(Literal::LitInt(1)), // 0
2692            CoreFrame::PrimOp {
2693                op: PrimOpKind::IntAdd,
2694                args: vec![0], // IntAdd expects 2 args
2695            }, // 1
2696        ];
2697        let expr = CoreExpr { nodes };
2698        let mut heap = crate::heap::VecHeap::new();
2699        let res = eval(&expr, &Env::new(), &mut heap);
2700        assert!(matches!(res, Err(EvalError::ArityMismatch { .. })));
2701    }
2702
2703    #[test]
2704    fn test_eval_force_non_thunk() {
2705        let mut heap = crate::heap::VecHeap::new();
2706        let val = Value::Lit(Literal::LitInt(42));
2707        let res = force(val, &mut heap).unwrap();
2708        if let Value::Lit(Literal::LitInt(n)) = res {
2709            assert_eq!(n, 42);
2710        } else {
2711            panic!("Expected LitInt(42)");
2712        }
2713    }
2714
2715    #[test]
2716    fn test_eval_deeply_nested_con() {
2717        // Con(2, [Con(1, [Lit(42)])])
2718        let nodes = vec![
2719            CoreFrame::Lit(Literal::LitInt(42)), // 0
2720            CoreFrame::Con {
2721                tag: DataConId(1),
2722                fields: vec![0],
2723            }, // 1
2724            CoreFrame::Con {
2725                tag: DataConId(2),
2726                fields: vec![1],
2727            }, // 2
2728        ];
2729        let expr = CoreExpr { nodes };
2730        let mut heap = crate::heap::VecHeap::new();
2731        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2732        match res {
2733            Value::Con(tag, fields) => {
2734                assert_eq!(tag.0, 2);
2735                assert_eq!(fields.len(), 1);
2736                match &fields[0] {
2737                    Value::Con(tag2, fields2) => {
2738                        assert_eq!(tag2.0, 1);
2739                        assert_eq!(fields2.len(), 1);
2740                        match &fields2[0] {
2741                            Value::Lit(Literal::LitInt(n)) => assert_eq!(*n, 42),
2742                            _ => panic!("Expected LitInt(42)"),
2743                        }
2744                    }
2745                    _ => panic!("Expected inner Con"),
2746                }
2747            }
2748            _ => panic!("Expected outer Con"),
2749        }
2750    }
2751
2752    #[test]
2753    fn test_eval_case_default_binder() {
2754        // case 42 of x { DEFAULT -> x }
2755        let nodes = vec![
2756            CoreFrame::Lit(Literal::LitInt(42)), // 0
2757            CoreFrame::Var(VarId(1)),            // 1: x
2758            CoreFrame::Case {
2759                scrutinee: 0,
2760                binder: VarId(1),
2761                alts: vec![Alt {
2762                    con: AltCon::Default,
2763                    binders: vec![],
2764                    body: 1,
2765                }],
2766            }, // 2
2767        ];
2768        let expr = CoreExpr { nodes };
2769        let mut heap = crate::heap::VecHeap::new();
2770        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2771        if let Value::Lit(Literal::LitInt(n)) = res {
2772            assert_eq!(n, 42);
2773        } else {
2774            panic!("Expected LitInt(42)");
2775        }
2776    }
2777
2778    #[test]
2779    fn test_eval_empty_con() {
2780        let nodes = vec![CoreFrame::Con {
2781            tag: DataConId(1),
2782            fields: vec![],
2783        }];
2784        let expr = CoreExpr { nodes };
2785        let mut heap = crate::heap::VecHeap::new();
2786        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2787        if let Value::Con(tag, fields) = res {
2788            assert_eq!(tag.0, 1);
2789            assert!(fields.is_empty());
2790        } else {
2791            panic!("Expected empty Con");
2792        }
2793    }
2794
2795    #[test]
2796    fn test_eval_unbound_var_lazy() {
2797        // let x = 1 in let y = unbound in x
2798        let nodes = vec![
2799            CoreFrame::Lit(Literal::LitInt(1)), // 0
2800            CoreFrame::Var(VarId(999)),         // 1: unbound
2801            CoreFrame::Var(VarId(1)),           // 2: x
2802            CoreFrame::LetNonRec {
2803                binder: VarId(2),
2804                rhs: 1,
2805                body: 2,
2806            }, // 3: let y = unbound in x
2807            CoreFrame::LetNonRec {
2808                binder: VarId(1),
2809                rhs: 0,
2810                body: 3,
2811            }, // 4: let x = 1 in ...
2812        ];
2813        let expr = CoreExpr { nodes };
2814        let mut heap = crate::heap::VecHeap::new();
2815        // Since y is not forced, this should SUCCEED and return 1
2816        let res = eval(&expr, &Env::new(), &mut heap).unwrap();
2817        if let Value::Lit(Literal::LitInt(n)) = res {
2818            assert_eq!(n, 1);
2819        } else {
2820            panic!("Expected LitInt(1)");
2821        }
2822    }
2823
2824    #[test]
2825    fn test_eval_unbound_var_forced_err() {
2826        // let x = 1 in let y = unbound in y
2827        let nodes = vec![
2828            CoreFrame::Lit(Literal::LitInt(1)), // 0
2829            CoreFrame::Var(VarId(999)),         // 1: unbound
2830            CoreFrame::Var(VarId(2)),           // 2: y
2831            CoreFrame::LetNonRec {
2832                binder: VarId(2),
2833                rhs: 1,
2834                body: 2,
2835            }, // 3: let y = unbound in y
2836            CoreFrame::LetNonRec {
2837                binder: VarId(1),
2838                rhs: 0,
2839                body: 3,
2840            }, // 4: let x = 1 in ...
2841        ];
2842        let expr = CoreExpr { nodes };
2843        let mut heap = crate::heap::VecHeap::new();
2844        // Now y is forced, should FAIL
2845        let res = eval(&expr, &Env::new(), &mut heap);
2846        assert!(matches!(res, Err(EvalError::UnboundVar(VarId(999)))));
2847    }
2848}