Skip to main content

cairn_core/
wasm.rs

1//! WASM lowering, and running the result under `wasmtime`.
2//!
3//! Lowering is the final stage; everything before it (store, check, render)
4//! is target-independent (`docs/design.md` Section 7). This is step 3's
5//! pure-subset slice: `Lit`, `Ref`, `Step`, `Call`, `Function`, `Module`,
6//! `i64` numerics. Lowering presumes the tree already checked clean — it does
7//! not re-check. Out of scope here and rejected explicitly: holes (incomplete
8//! programs), effectful functions, and anything the seed model does not yet
9//! have (operators, non-numeric types).
10//!
11//! Functions are lowered in two passes: pass A reserves a `FunctionId`
12//! (and export) for every function with an empty body, pass B fills the
13//! bodies with all ids known. Definition order is therefore irrelevant —
14//! self, forward, and mutual recursion all resolve. Each Cairn function is
15//! exported under its own name.
16
17use crate::node::{Node, NodeHash};
18use crate::store::Store;
19use std::cell::RefCell;
20use std::collections::{HashMap, HashSet};
21use walrus::ir::{BinaryOp, InstrSeqType, LoadKind, MemArg, StoreKind, UnaryOp, Value};
22use walrus::{
23    ConstExpr, ElementItems, ElementKind, FunctionBuilder, FunctionId, GlobalId, InstrSeqBuilder,
24    LocalId, MemoryId, Module, RefType, TableId, TypeId, ValType,
25};
26
27/// Why a tree could not be lowered. These are "not supported yet" or "not
28/// lowerable" conditions, distinct from store errors.
29#[derive(Debug)]
30pub enum LowerError {
31    Store(crate::store::Error),
32    /// The root was not a `Module`.
33    NotAModule,
34    /// A hole was reached; an incomplete program cannot be lowered.
35    Hole,
36    /// A function declares effects; only pure functions lower in step 3.
37    Effectful(String),
38    /// A call named a function the module does not define.
39    UnknownCallee(String),
40    /// A name did not resolve to a parameter or prior binding.
41    UnresolvedRef(String),
42    /// A construct the seed model does not lower yet.
43    Unsupported(&'static str),
44}
45
46impl std::fmt::Display for LowerError {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match self {
49            LowerError::Store(e) => write!(f, "store error: {e}"),
50            LowerError::NotAModule => write!(f, "lowering root must be a Module"),
51            LowerError::Hole => write!(f, "cannot lower an incomplete program (hole)"),
52            LowerError::Effectful(n) => {
53                write!(f, "function `{n}` declares effects; step 3 lowers pure functions only")
54            }
55            LowerError::UnknownCallee(n) => write!(f, "call to unknown function `{n}`"),
56            LowerError::UnresolvedRef(n) => write!(f, "unresolved reference: `{n}`"),
57            LowerError::Unsupported(w) => write!(f, "not lowerable yet: {w}"),
58        }
59    }
60}
61
62impl std::error::Error for LowerError {}
63
64impl From<crate::store::Error> for LowerError {
65    fn from(e: crate::store::Error) -> Self {
66        LowerError::Store(e)
67    }
68}
69
70type LowerResult<T> = std::result::Result<T, LowerError>;
71
72/// Lower a Cairn `Module` node to a WebAssembly binary. Every function is an
73/// `i64`-typed export under its own name.
74/// Immutable lowering context: the store, resolved function ids, the record
75/// field-order table, and the three memory helper functions.
76struct Ctx<'a> {
77    store: &'a Store,
78    fns: &'a HashMap<String, FunctionId>,
79    /// Function name -> its thunk's index in `func_table`, for `FuncRef`.
80    fn_table_idx: &'a HashMap<String, i64>,
81    /// Lambda node hash -> (its `func_table` index, ordered capture names).
82    lambdas: &'a HashMap<NodeHash, (i64, Vec<String>)>,
83    /// The single funcref table every function value indexes into.
84    func_table: TableId,
85    /// User arity -> the `(i64×n, i64 env) -> i64` type, for
86    /// `call_indirect` (every table entry takes a trailing env i64).
87    indirect_types: &'a HashMap<usize, TypeId>,
88    recs: &'a HashMap<String, Vec<String>>,
89    /// Variant name -> ordered (case, payload field names).
90    vars: &'a HashMap<String, Vec<(String, Vec<String>)>>,
91    /// Module-wide failure name -> tag (1-based; 0 means ok).
92    ftags: &'a HashMap<String, i64>,
93    /// Names of functions that are fallible (non-empty `on_failure`): they
94    /// return a `[tag,val]` block instead of a raw i64.
95    fallible: &'a HashSet<String>,
96    /// Function locals, behind a cell so a `&Ctx` can mint a local (for the
97    /// scrutinee in `match`) and the `if_else` closures can too.
98    locals: &'a RefCell<&'a mut walrus::ModuleLocals>,
99    alloc: FunctionId,
100    set: FunctionId,
101    get: FunctionId,
102    map_get: FunctionId,
103    map_try_get: FunctionId,
104    /// Host import `host::now : () -> i64` (the Time effect). Other effects
105    /// will add their own host imports the same way.
106    now: FunctionId,
107    /// Host import `host::log : (i64) -> i64` (the Log effect): records the
108    /// value and echoes it back (pass-through).
109    log: FunctionId,
110    /// Host import `host::publish : (topic_ptr) -> i64` (the Live effect):
111    /// records the published topic for the live runtime; returns 0.
112    publish: FunctionId,
113    /// Host import `host::set_header : (name_ptr, value_ptr) -> i64`
114    /// (the Resp effect): buffers an extra response header line for the
115    /// server to emit; returns 0.
116    set_header: FunctionId,
117    /// Host import `host::rand : () -> i64` (the Rand effect).
118    rand: FunctionId,
119    /// Host imports for the Disk effect: `disk_write : (path,content)->i64`
120    /// (bytes written) and `disk_read : (path)->i64` (file byte length).
121    disk_write: FunctionId,
122    disk_read: FunctionId,
123    /// Host import `host::net_get : (url) -> i64` (the Net effect).
124    net_get: FunctionId,
125    /// Host import `host::db_query : (sql) -> i64` (the Db effect).
126    db_query: FunctionId,
127    /// String helpers: concat, slice, content-equality, substring search,
128    /// prefix test, and Number<->String conversion.
129    str_concat: FunctionId,
130    str_slice: FunctionId,
131    /// `__str_lower(s) -> s'` — ASCII lowercase (the case-insensitive
132    /// `header`/`cookie` match the live Cloudflare deploy forced).
133    str_lower: FunctionId,
134    /// `__str_from_code(code) -> s` — a 1-byte string from a code
135    /// (percent-decoding; design.md §9).
136    str_from_code: FunctionId,
137    str_eq: FunctionId,
138    str_contains: FunctionId,
139    str_starts_with: FunctionId,
140    str_index_of: FunctionId,
141    num_to_str: FunctionId,
142    str_to_num: FunctionId,
143    /// `__str_to_num_opt(s) -> Option` block (None if not a valid int).
144    str_to_num_opt: FunctionId,
145    /// `__list_cons(head, tail) -> ptr`: prepend, as a fresh array copy.
146    list_cons: FunctionId,
147    /// `__list_get(ptr, idx) -> elem`, bounds-checked (traps on OOB).
148    list_get: FunctionId,
149    /// `__list_try_get(ptr, idx) -> Option` block (None on OOB).
150    list_try_get: FunctionId,
151}
152
153/// Where a name's value lives: a wasm local, or a field at `off` in the
154/// record/variant `base` points at (a `match` payload binding).
155#[derive(Clone, Copy)]
156enum Slot {
157    Local(LocalId),
158    Member { base: LocalId, off: i64 },
159}
160
161/// `__alloc(size) -> ptr`: a bump allocator over linear memory. No free (an
162/// arena for the run); honest v0.2 limitation, documented.
163fn build_alloc(wasm: &mut Module, bump: GlobalId, mem: MemoryId) -> FunctionId {
164    let mut fb = FunctionBuilder::new(&mut wasm.types, &[ValType::I64], &[ValType::I64]);
165    let size = wasm.locals.add(ValType::I64);
166    let old = wasm.locals.add(ValType::I64);
167    let new = wasm.locals.add(ValType::I64);
168    let cur = wasm.locals.add(ValType::I64);
169    {
170        let mut b = fb.func_body();
171        // old = bump ; new = old + size ; bump = new
172        b.global_get(bump).local_set(old);
173        b.local_get(old)
174            .local_get(size)
175            .binop(BinaryOp::I64Add)
176            .local_set(new);
177        b.local_get(new).global_set(bump);
178        // Grow linear memory if `new` would exceed it, so allocation is
179        // not capped at the initial page (no silent OOB / trap).
180        // cur = memory.size() pages * 64 KiB
181        b.memory_size(mem)
182            .unop(UnaryOp::I64ExtendUI32)
183            .i64_const(65536)
184            .binop(BinaryOp::I64Mul)
185            .local_set(cur);
186        b.local_get(new).local_get(cur).binop(BinaryOp::I64GtS);
187        b.if_else(
188            InstrSeqType::Simple(None),
189            |t| {
190                // grow by ceil((new - cur) / 64 KiB) pages
191                t.local_get(new)
192                    .local_get(cur)
193                    .binop(BinaryOp::I64Sub)
194                    .i64_const(65535)
195                    .binop(BinaryOp::I64Add)
196                    .i64_const(65536)
197                    .binop(BinaryOp::I64DivS)
198                    .unop(UnaryOp::I32WrapI64);
199                t.memory_grow(mem);
200                t.drop(); // ignore old-size / -1
201            },
202            |_| {},
203        );
204        b.local_get(old); // return the (pre-bump) pointer
205    }
206    fb.finish(vec![size], &mut wasm.funcs)
207}
208
209/// `__set(ptr, off, val) -> ptr`: store `val` at `ptr+off`, return `ptr` so
210/// record construction chains through the stack without scratch locals.
211fn build_set(wasm: &mut Module, mem: MemoryId) -> FunctionId {
212    let mut fb = FunctionBuilder::new(
213        &mut wasm.types,
214        &[ValType::I64, ValType::I64, ValType::I64],
215        &[ValType::I64],
216    );
217    let p = wasm.locals.add(ValType::I64);
218    let off = wasm.locals.add(ValType::I64);
219    let v = wasm.locals.add(ValType::I64);
220    {
221        let mut b = fb.func_body();
222        b.local_get(p)
223            .local_get(off)
224            .binop(BinaryOp::I64Add)
225            .unop(UnaryOp::I32WrapI64) // memory address is i32
226            .local_get(v)
227            .store(
228                mem,
229                StoreKind::I64 { atomic: false },
230                MemArg { align: 3, offset: 0 },
231            )
232            .local_get(p); // return ptr
233    }
234    fb.finish(vec![p, off, v], &mut wasm.funcs)
235}
236
237/// `__get(ptr, off) -> val`: load from `ptr+off`.
238fn build_get(wasm: &mut Module, mem: MemoryId) -> FunctionId {
239    let mut fb = FunctionBuilder::new(
240        &mut wasm.types,
241        &[ValType::I64, ValType::I64],
242        &[ValType::I64],
243    );
244    let p = wasm.locals.add(ValType::I64);
245    let off = wasm.locals.add(ValType::I64);
246    {
247        let mut b = fb.func_body();
248        b.local_get(p)
249            .local_get(off)
250            .binop(BinaryOp::I64Add)
251            .unop(UnaryOp::I32WrapI64)
252            .load(
253                mem,
254                LoadKind::I64 { atomic: false },
255                MemArg { align: 3, offset: 0 },
256            );
257    }
258    fb.finish(vec![p, off], &mut wasm.funcs)
259}
260
261/// `__map_get(p, key) -> val`: linear scan of `[count, k0,v0, k1,v1, ...]`.
262/// Returns the value of the first key equal (i64) to `key`, else 0. The one
263/// runtime loop in the codegen, isolated here.
264fn build_map_get(wasm: &mut Module, get: FunctionId) -> FunctionId {
265    let mut fb = FunctionBuilder::new(
266        &mut wasm.types,
267        &[ValType::I64, ValType::I64],
268        &[ValType::I64],
269    );
270    let p = wasm.locals.add(ValType::I64);
271    let key = wasm.locals.add(ValType::I64);
272    let i = wasm.locals.add(ValType::I64);
273    let count = wasm.locals.add(ValType::I64);
274    let k = wasm.locals.add(ValType::I64);
275    let val = wasm.locals.add(ValType::I64);
276    {
277        let mut b = fb.func_body();
278        b.local_get(p).i64_const(0).call(get).local_set(count);
279        b.i64_const(0).local_set(i);
280        b.i64_const(0).local_set(val);
281        b.block(InstrSeqType::Simple(None), |outer| {
282            let oid = outer.id();
283            outer.loop_(InstrSeqType::Simple(None), |lp| {
284                let lid = lp.id();
285                // if i >= count: break
286                lp.local_get(i).local_get(count).binop(BinaryOp::I64GeS);
287                lp.br_if(oid);
288                // k = get(p, 8 + i*16)
289                lp.local_get(p);
290                lp.local_get(i)
291                    .i64_const(16)
292                    .binop(BinaryOp::I64Mul)
293                    .i64_const(8)
294                    .binop(BinaryOp::I64Add);
295                lp.call(get);
296                lp.local_set(k);
297                // if k == key { val = get(p, 16 + i*16); break }
298                lp.local_get(k).local_get(key).binop(BinaryOp::I64Eq);
299                lp.if_else(
300                    InstrSeqType::Simple(None),
301                    |t| {
302                        t.local_get(p);
303                        t.local_get(i)
304                            .i64_const(16)
305                            .binop(BinaryOp::I64Mul)
306                            .i64_const(16)
307                            .binop(BinaryOp::I64Add);
308                        t.call(get);
309                        t.local_set(val);
310                        t.br(oid);
311                    },
312                    |_| {},
313                );
314                // i += 1; continue
315                lp.local_get(i)
316                    .i64_const(1)
317                    .binop(BinaryOp::I64Add)
318                    .local_set(i);
319                lp.br(lid);
320            });
321        });
322        b.local_get(val);
323    }
324    fb.finish(vec![p, key], &mut wasm.funcs)
325}
326
327/// `__map_try_get(p, key) -> Option block`: the checked counterpart to
328/// `__map_get` — `Some(v)` for the first key equal (i64) to `key`, else
329/// `None`. Same documented key model (i64/identity, exact for Number).
330fn build_map_try_get(
331    wasm: &mut Module,
332    get: FunctionId,
333    set: FunctionId,
334    alloc: FunctionId,
335) -> FunctionId {
336    let mut fb = FunctionBuilder::new(
337        &mut wasm.types,
338        &[ValType::I64, ValType::I64],
339        &[ValType::I64],
340    );
341    let p = wasm.locals.add(ValType::I64);
342    let key = wasm.locals.add(ValType::I64);
343    let i = wasm.locals.add(ValType::I64);
344    let count = wasm.locals.add(ValType::I64);
345    let k = wasm.locals.add(ValType::I64);
346    let val = wasm.locals.add(ValType::I64);
347    let found = wasm.locals.add(ValType::I64);
348    let opt = wasm.locals.add(ValType::I64);
349    {
350        let mut b = fb.func_body();
351        b.local_get(p).i64_const(0).call(get).local_set(count);
352        b.i64_const(0).local_set(i);
353        b.i64_const(0).local_set(found);
354        b.i64_const(0).local_set(val);
355        b.block(InstrSeqType::Simple(None), |outer| {
356            let oid = outer.id();
357            outer.loop_(InstrSeqType::Simple(None), |lp| {
358                let lid = lp.id();
359                lp.local_get(i).local_get(count).binop(BinaryOp::I64GeS);
360                lp.br_if(oid);
361                lp.local_get(p);
362                lp.local_get(i)
363                    .i64_const(16)
364                    .binop(BinaryOp::I64Mul)
365                    .i64_const(8)
366                    .binop(BinaryOp::I64Add);
367                lp.call(get);
368                lp.local_set(k);
369                lp.local_get(k).local_get(key).binop(BinaryOp::I64Eq);
370                lp.if_else(
371                    InstrSeqType::Simple(None),
372                    |t| {
373                        t.local_get(p);
374                        t.local_get(i)
375                            .i64_const(16)
376                            .binop(BinaryOp::I64Mul)
377                            .i64_const(16)
378                            .binop(BinaryOp::I64Add);
379                        t.call(get);
380                        t.local_set(val);
381                        t.i64_const(1).local_set(found);
382                        t.br(oid);
383                    },
384                    |_| {},
385                );
386                lp.local_get(i)
387                    .i64_const(1)
388                    .binop(BinaryOp::I64Add)
389                    .local_set(i);
390                lp.br(lid);
391            });
392        });
393        // Box the result as an Option block (set returns the ptr; drop it).
394        b.i64_const(16).call(alloc).local_set(opt);
395        b.local_get(found).i64_const(1).binop(BinaryOp::I64Eq);
396        b.if_else(
397            InstrSeqType::Simple(None),
398            |t| {
399                t.local_get(opt).i64_const(0).i64_const(1).call(set).drop();
400                t.local_get(opt).i64_const(8).local_get(val).call(set).drop();
401            },
402            |e| {
403                e.local_get(opt).i64_const(0).i64_const(0).call(set).drop();
404            },
405        );
406        b.local_get(opt);
407    }
408    fb.finish(vec![p, key], &mut wasm.funcs)
409}
410
411// Strings are [len, b0, b1, ...], one i64 slot each; byte k is at the i64
412// offset (k+1)*8. The `set` helper returns the pointer, so every store in a
413// loop body is followed by `drop` to keep the block stack-neutral.
414
415/// `__str_concat(a, b) -> dst`.
416fn build_str_concat(
417    wasm: &mut Module,
418    get: FunctionId,
419    set: FunctionId,
420    alloc: FunctionId,
421) -> FunctionId {
422    let mut fb = FunctionBuilder::new(
423        &mut wasm.types,
424        &[ValType::I64, ValType::I64],
425        &[ValType::I64],
426    );
427    let a = wasm.locals.add(ValType::I64);
428    let b = wasm.locals.add(ValType::I64);
429    let la = wasm.locals.add(ValType::I64);
430    let lb = wasm.locals.add(ValType::I64);
431    let i = wasm.locals.add(ValType::I64);
432    let dst = wasm.locals.add(ValType::I64);
433    {
434        let mut bd = fb.func_body();
435        bd.local_get(a).i64_const(0).call(get).local_set(la);
436        bd.local_get(b).i64_const(0).call(get).local_set(lb);
437        bd.local_get(la)
438            .local_get(lb)
439            .binop(BinaryOp::I64Add)
440            .i64_const(1)
441            .binop(BinaryOp::I64Add)
442            .i64_const(8)
443            .binop(BinaryOp::I64Mul)
444            .call(alloc)
445            .local_set(dst);
446        bd.local_get(dst)
447            .i64_const(0)
448            .local_get(la)
449            .local_get(lb)
450            .binop(BinaryOp::I64Add)
451            .call(set)
452            .drop();
453        // copy a
454        bd.i64_const(0).local_set(i);
455        bd.block(InstrSeqType::Simple(None), |o| {
456            let oid = o.id();
457            o.loop_(InstrSeqType::Simple(None), |l| {
458                let lid = l.id();
459                l.local_get(i).local_get(la).binop(BinaryOp::I64GeS);
460                l.br_if(oid);
461                l.local_get(dst);
462                l.local_get(i)
463                    .i64_const(1)
464                    .binop(BinaryOp::I64Add)
465                    .i64_const(8)
466                    .binop(BinaryOp::I64Mul);
467                l.local_get(a);
468                l.local_get(i)
469                    .i64_const(1)
470                    .binop(BinaryOp::I64Add)
471                    .i64_const(8)
472                    .binop(BinaryOp::I64Mul);
473                l.call(get);
474                l.call(set);
475                l.drop();
476                l.local_get(i)
477                    .i64_const(1)
478                    .binop(BinaryOp::I64Add)
479                    .local_set(i);
480                l.br(lid);
481            });
482        });
483        // copy b at dst position la+i
484        bd.i64_const(0).local_set(i);
485        bd.block(InstrSeqType::Simple(None), |o| {
486            let oid = o.id();
487            o.loop_(InstrSeqType::Simple(None), |l| {
488                let lid = l.id();
489                l.local_get(i).local_get(lb).binop(BinaryOp::I64GeS);
490                l.br_if(oid);
491                l.local_get(dst);
492                l.local_get(la)
493                    .local_get(i)
494                    .binop(BinaryOp::I64Add)
495                    .i64_const(1)
496                    .binop(BinaryOp::I64Add)
497                    .i64_const(8)
498                    .binop(BinaryOp::I64Mul);
499                l.local_get(b);
500                l.local_get(i)
501                    .i64_const(1)
502                    .binop(BinaryOp::I64Add)
503                    .i64_const(8)
504                    .binop(BinaryOp::I64Mul);
505                l.call(get);
506                l.call(set);
507                l.drop();
508                l.local_get(i)
509                    .i64_const(1)
510                    .binop(BinaryOp::I64Add)
511                    .local_set(i);
512                l.br(lid);
513            });
514        });
515        bd.local_get(dst);
516    }
517    fb.finish(vec![a, b], &mut wasm.funcs)
518}
519
520/// `__str_slice(s, start, n) -> dst` (no bounds check — v0.3).
521fn build_str_slice(
522    wasm: &mut Module,
523    get: FunctionId,
524    set: FunctionId,
525    alloc: FunctionId,
526) -> FunctionId {
527    let mut fb = FunctionBuilder::new(
528        &mut wasm.types,
529        &[ValType::I64, ValType::I64, ValType::I64],
530        &[ValType::I64],
531    );
532    let s = wasm.locals.add(ValType::I64);
533    let start = wasm.locals.add(ValType::I64);
534    let n = wasm.locals.add(ValType::I64);
535    let i = wasm.locals.add(ValType::I64);
536    let dst = wasm.locals.add(ValType::I64);
537    let ls = wasm.locals.add(ValType::I64);
538    {
539        let mut bd = fb.func_body();
540        // Bounds check: trap if start<0 || n<0 || start+n > len(s).
541        bd.local_get(s).i64_const(0).call(get).local_set(ls);
542        bd.local_get(start).i64_const(0).binop(BinaryOp::I64LtS);
543        bd.local_get(n).i64_const(0).binop(BinaryOp::I64LtS);
544        bd.binop(BinaryOp::I32Or);
545        bd.local_get(start)
546            .local_get(n)
547            .binop(BinaryOp::I64Add)
548            .local_get(ls)
549            .binop(BinaryOp::I64GtS);
550        bd.binop(BinaryOp::I32Or);
551        bd.if_else(
552            InstrSeqType::Simple(None),
553            |t| {
554                t.unreachable();
555            },
556            |_| {},
557        );
558        bd.local_get(n)
559            .i64_const(1)
560            .binop(BinaryOp::I64Add)
561            .i64_const(8)
562            .binop(BinaryOp::I64Mul)
563            .call(alloc)
564            .local_set(dst);
565        bd.local_get(dst)
566            .i64_const(0)
567            .local_get(n)
568            .call(set)
569            .drop();
570        bd.i64_const(0).local_set(i);
571        bd.block(InstrSeqType::Simple(None), |o| {
572            let oid = o.id();
573            o.loop_(InstrSeqType::Simple(None), |l| {
574                let lid = l.id();
575                l.local_get(i).local_get(n).binop(BinaryOp::I64GeS);
576                l.br_if(oid);
577                // set(dst, (i+1)*8, get(s, (start+i+1)*8)); drop
578                l.local_get(dst);
579                l.local_get(i)
580                    .i64_const(1)
581                    .binop(BinaryOp::I64Add)
582                    .i64_const(8)
583                    .binop(BinaryOp::I64Mul);
584                l.local_get(s);
585                l.local_get(start)
586                    .local_get(i)
587                    .binop(BinaryOp::I64Add)
588                    .i64_const(1)
589                    .binop(BinaryOp::I64Add)
590                    .i64_const(8)
591                    .binop(BinaryOp::I64Mul);
592                l.call(get);
593                l.call(set);
594                l.drop();
595                l.local_get(i)
596                    .i64_const(1)
597                    .binop(BinaryOp::I64Add)
598                    .local_set(i);
599                l.br(lid);
600            });
601        });
602        bd.local_get(dst);
603    }
604    fb.finish(vec![s, start, n], &mut wasm.funcs)
605}
606
607/// `__str_lower(s) -> s'`: a fresh string with `A`–`Z` (ASCII 65–90)
608/// folded to `a`–`z` (+32), every other byte copied as-is. Each char
609/// is its own i64 slot (low byte the byte), like every Cairn string.
610fn build_str_lower(
611    wasm: &mut Module,
612    get: FunctionId,
613    set: FunctionId,
614    alloc: FunctionId,
615) -> FunctionId {
616    let mut fb = FunctionBuilder::new(
617        &mut wasm.types,
618        &[ValType::I64],
619        &[ValType::I64],
620    );
621    let s = wasm.locals.add(ValType::I64);
622    let n = wasm.locals.add(ValType::I64);
623    let i = wasm.locals.add(ValType::I64);
624    let dst = wasm.locals.add(ValType::I64);
625    let ch = wasm.locals.add(ValType::I64);
626    {
627        let mut bd = fb.func_body();
628        bd.local_get(s).i64_const(0).call(get).local_set(n);
629        bd.local_get(n)
630            .i64_const(1)
631            .binop(BinaryOp::I64Add)
632            .i64_const(8)
633            .binop(BinaryOp::I64Mul)
634            .call(alloc)
635            .local_set(dst);
636        bd.local_get(dst)
637            .i64_const(0)
638            .local_get(n)
639            .call(set)
640            .drop();
641        bd.i64_const(0).local_set(i);
642        bd.block(InstrSeqType::Simple(None), |o| {
643            let oid = o.id();
644            o.loop_(InstrSeqType::Simple(None), |l| {
645                let lid = l.id();
646                l.local_get(i).local_get(n).binop(BinaryOp::I64GeS);
647                l.br_if(oid);
648                // ch = get(s, (i+1)*8)
649                l.local_get(s);
650                l.local_get(i)
651                    .i64_const(1)
652                    .binop(BinaryOp::I64Add)
653                    .i64_const(8)
654                    .binop(BinaryOp::I64Mul);
655                l.call(get).local_set(ch);
656                // if ch>=65 && ch<=90 { ch += 32 }
657                l.local_get(ch).i64_const(65).binop(BinaryOp::I64GeS);
658                l.local_get(ch).i64_const(90).binop(BinaryOp::I64LeS);
659                l.binop(BinaryOp::I32And);
660                l.if_else(
661                    InstrSeqType::Simple(None),
662                    |t| {
663                        t.local_get(ch)
664                            .i64_const(32)
665                            .binop(BinaryOp::I64Add)
666                            .local_set(ch);
667                    },
668                    |_| {},
669                );
670                // set(dst, (i+1)*8, ch); drop
671                l.local_get(dst);
672                l.local_get(i)
673                    .i64_const(1)
674                    .binop(BinaryOp::I64Add)
675                    .i64_const(8)
676                    .binop(BinaryOp::I64Mul);
677                l.local_get(ch);
678                l.call(set);
679                l.drop();
680                l.local_get(i)
681                    .i64_const(1)
682                    .binop(BinaryOp::I64Add)
683                    .local_set(i);
684                l.br(lid);
685            });
686        });
687        bd.local_get(dst);
688    }
689    fb.finish(vec![s], &mut wasm.funcs)
690}
691
692/// `__str_from_code(code) -> s`: a one-byte string whose single slot
693/// holds `code` (the reader masks `& 0xff`, so only the low byte
694/// matters). The inverse of byte-indexing; the minimal primitive
695/// percent-decoding needs.
696fn build_str_from_code(
697    wasm: &mut Module,
698    set: FunctionId,
699    alloc: FunctionId,
700) -> FunctionId {
701    let mut fb = FunctionBuilder::new(
702        &mut wasm.types,
703        &[ValType::I64],
704        &[ValType::I64],
705    );
706    let code = wasm.locals.add(ValType::I64);
707    let dst = wasm.locals.add(ValType::I64);
708    {
709        let mut bd = fb.func_body();
710        // dst = alloc((1+1)*8); set(dst,0,1) [len]; set(dst,8,code)
711        bd.i64_const(16).call(alloc).local_set(dst);
712        bd.local_get(dst)
713            .i64_const(0)
714            .i64_const(1)
715            .call(set)
716            .drop();
717        bd.local_get(dst)
718            .i64_const(8)
719            .local_get(code)
720            .call(set)
721            .drop();
722        bd.local_get(dst);
723    }
724    fb.finish(vec![code], &mut wasm.funcs)
725}
726
727/// `__list_get(ptr, idx) -> elem` with a bounds check: traps
728/// (`unreachable`) when `idx < 0 || idx >= len`, turning what was a
729/// silent out-of-bounds read into a clean deterministic failure. This
730/// is the intentional trapping fast path (Rust-like `[]`); the typed,
731/// recoverable counterpart — `ListTryGet` → `Option` — is shipped, so
732/// out-of-bounds is in-program recoverable without trapping when wanted.
733fn build_list_get(wasm: &mut Module, get: FunctionId) -> FunctionId {
734    let mut fb = FunctionBuilder::new(
735        &mut wasm.types,
736        &[ValType::I64, ValType::I64],
737        &[ValType::I64],
738    );
739    let ptr = wasm.locals.add(ValType::I64);
740    let idx = wasm.locals.add(ValType::I64);
741    let len = wasm.locals.add(ValType::I64);
742    {
743        let mut bd = fb.func_body();
744        bd.local_get(ptr).i64_const(0).call(get).local_set(len);
745        // idx < 0 || idx >= len  ->  trap
746        bd.local_get(idx).i64_const(0).binop(BinaryOp::I64LtS);
747        bd.local_get(idx).local_get(len).binop(BinaryOp::I64GeS);
748        bd.binop(BinaryOp::I32Or);
749        bd.if_else(
750            InstrSeqType::Simple(None),
751            |t| {
752                t.unreachable();
753            },
754            |_| {},
755        );
756        bd.local_get(ptr);
757        bd.local_get(idx)
758            .i64_const(1)
759            .binop(BinaryOp::I64Add)
760            .i64_const(8)
761            .binop(BinaryOp::I64Mul);
762        bd.call(get); // [elem]
763    }
764    fb.finish(vec![ptr, idx], &mut wasm.funcs)
765}
766
767/// `__list_try_get(ptr, idx) -> option_ptr`: a `[tag,val]` block —
768/// `[0,_]` (None) when out of bounds, `[1,elem]` (Some) otherwise. The
769/// handle-able counterpart to `__list_get`'s trap.
770fn build_list_try_get(
771    wasm: &mut Module,
772    get: FunctionId,
773    set: FunctionId,
774    alloc: FunctionId,
775) -> FunctionId {
776    let mut fb = FunctionBuilder::new(
777        &mut wasm.types,
778        &[ValType::I64, ValType::I64],
779        &[ValType::I64],
780    );
781    let ptr = wasm.locals.add(ValType::I64);
782    let idx = wasm.locals.add(ValType::I64);
783    let len = wasm.locals.add(ValType::I64);
784    let opt = wasm.locals.add(ValType::I64);
785    {
786        let mut bd = fb.func_body();
787        bd.local_get(ptr).i64_const(0).call(get).local_set(len);
788        bd.i64_const(16).call(alloc).local_set(opt);
789        bd.local_get(idx).i64_const(0).binop(BinaryOp::I64LtS);
790        bd.local_get(idx).local_get(len).binop(BinaryOp::I64GeS);
791        bd.binop(BinaryOp::I32Or);
792        bd.if_else(
793            InstrSeqType::Simple(None),
794            |t| {
795                // None: tag = 0
796                t.local_get(opt).i64_const(0).i64_const(0).call(set).drop();
797            },
798            |e| {
799                // Some: tag = 1, val = ptr[(idx+1)*8]
800                e.local_get(opt).i64_const(0).i64_const(1).call(set).drop();
801                e.local_get(opt).i64_const(8);
802                e.local_get(ptr);
803                e.local_get(idx)
804                    .i64_const(1)
805                    .binop(BinaryOp::I64Add)
806                    .i64_const(8)
807                    .binop(BinaryOp::I64Mul);
808                e.call(get);
809                e.call(set).drop();
810            },
811        );
812        bd.local_get(opt);
813    }
814    fb.finish(vec![ptr, idx], &mut wasm.funcs)
815}
816
817/// `__str_eq(a, b) -> 0|1`.
818fn build_str_eq(wasm: &mut Module, get: FunctionId) -> FunctionId {
819    let mut fb = FunctionBuilder::new(
820        &mut wasm.types,
821        &[ValType::I64, ValType::I64],
822        &[ValType::I64],
823    );
824    let a = wasm.locals.add(ValType::I64);
825    let b = wasm.locals.add(ValType::I64);
826    let la = wasm.locals.add(ValType::I64);
827    let lb = wasm.locals.add(ValType::I64);
828    let i = wasm.locals.add(ValType::I64);
829    let res = wasm.locals.add(ValType::I64);
830    {
831        let mut bd = fb.func_body();
832        bd.local_get(a).i64_const(0).call(get).local_set(la);
833        bd.local_get(b).i64_const(0).call(get).local_set(lb);
834        bd.i64_const(0).local_set(i);
835        bd.local_get(la).local_get(lb).binop(BinaryOp::I64Ne);
836        bd.if_else(
837            InstrSeqType::Simple(None),
838            |t| {
839                t.i64_const(0).local_set(res);
840            },
841            |e| {
842                e.i64_const(1).local_set(res);
843                e.block(InstrSeqType::Simple(None), |o| {
844                    let oid = o.id();
845                    o.loop_(InstrSeqType::Simple(None), |l| {
846                        let lid = l.id();
847                        l.local_get(i).local_get(la).binop(BinaryOp::I64GeS);
848                        l.br_if(oid);
849                        l.local_get(a);
850                        l.local_get(i)
851                            .i64_const(1)
852                            .binop(BinaryOp::I64Add)
853                            .i64_const(8)
854                            .binop(BinaryOp::I64Mul);
855                        l.call(get);
856                        l.local_get(b);
857                        l.local_get(i)
858                            .i64_const(1)
859                            .binop(BinaryOp::I64Add)
860                            .i64_const(8)
861                            .binop(BinaryOp::I64Mul);
862                        l.call(get);
863                        l.binop(BinaryOp::I64Ne);
864                        l.if_else(
865                            InstrSeqType::Simple(None),
866                            |t2| {
867                                t2.i64_const(0).local_set(res);
868                                t2.br(oid);
869                            },
870                            |_| {},
871                        );
872                        l.local_get(i)
873                            .i64_const(1)
874                            .binop(BinaryOp::I64Add)
875                            .local_set(i);
876                        l.br(lid);
877                    });
878                });
879            },
880        );
881        bd.local_get(res);
882    }
883    fb.finish(vec![a, b], &mut wasm.funcs)
884}
885
886/// `__str_contains(h, n) -> 0|1` — naive O(lh*ln) substring search.
887fn build_str_contains(wasm: &mut Module, get: FunctionId) -> FunctionId {
888    let mut fb = FunctionBuilder::new(
889        &mut wasm.types,
890        &[ValType::I64, ValType::I64],
891        &[ValType::I64],
892    );
893    let h = wasm.locals.add(ValType::I64);
894    let n = wasm.locals.add(ValType::I64);
895    let lh = wasm.locals.add(ValType::I64);
896    let ln = wasm.locals.add(ValType::I64);
897    let i = wasm.locals.add(ValType::I64);
898    let j = wasm.locals.add(ValType::I64);
899    let m = wasm.locals.add(ValType::I64);
900    let res = wasm.locals.add(ValType::I64);
901    {
902        let mut bd = fb.func_body();
903        bd.local_get(h).i64_const(0).call(get).local_set(lh);
904        bd.local_get(n).i64_const(0).call(get).local_set(ln);
905        // res defaults to 0 (no match / empty haystack with non-empty needle)
906        bd.local_get(ln).i64_const(0).binop(BinaryOp::I64Eq);
907        bd.if_else(
908            InstrSeqType::Simple(None),
909            |t| {
910                // empty needle is always contained
911                t.i64_const(1).local_set(res);
912            },
913            |e| {
914                // only search if the needle can fit
915                e.local_get(ln).local_get(lh).binop(BinaryOp::I64LeS);
916                e.if_else(
917                    InstrSeqType::Simple(None),
918                    |s2| {
919                        s2.i64_const(0).local_set(i);
920                        s2.block(InstrSeqType::Simple(None), |o| {
921                            let oid = o.id();
922                            o.loop_(InstrSeqType::Simple(None), |l| {
923                                let lid = l.id();
924                                // if i + ln > lh: no positions left
925                                l.local_get(i)
926                                    .local_get(ln)
927                                    .binop(BinaryOp::I64Add)
928                                    .local_get(lh)
929                                    .binop(BinaryOp::I64GtS);
930                                l.br_if(oid);
931                                // assume match; scan needle
932                                l.i64_const(1).local_set(m);
933                                l.i64_const(0).local_set(j);
934                                l.block(InstrSeqType::Simple(None), |ib| {
935                                    let ibid = ib.id();
936                                    ib.loop_(InstrSeqType::Simple(None), |il| {
937                                        let ilid = il.id();
938                                        il.local_get(j)
939                                            .local_get(ln)
940                                            .binop(BinaryOp::I64GeS);
941                                        il.br_if(ibid);
942                                        // h[(i+j+1)*8] vs n[(j+1)*8]
943                                        il.local_get(h);
944                                        il.local_get(i)
945                                            .local_get(j)
946                                            .binop(BinaryOp::I64Add)
947                                            .i64_const(1)
948                                            .binop(BinaryOp::I64Add)
949                                            .i64_const(8)
950                                            .binop(BinaryOp::I64Mul);
951                                        il.call(get);
952                                        il.local_get(n);
953                                        il.local_get(j)
954                                            .i64_const(1)
955                                            .binop(BinaryOp::I64Add)
956                                            .i64_const(8)
957                                            .binop(BinaryOp::I64Mul);
958                                        il.call(get);
959                                        il.binop(BinaryOp::I64Ne);
960                                        il.if_else(
961                                            InstrSeqType::Simple(None),
962                                            |mm| {
963                                                mm.i64_const(0).local_set(m);
964                                                mm.br(ibid);
965                                            },
966                                            |_| {},
967                                        );
968                                        il.local_get(j)
969                                            .i64_const(1)
970                                            .binop(BinaryOp::I64Add)
971                                            .local_set(j);
972                                        il.br(ilid);
973                                    });
974                                });
975                                // matched the whole needle?
976                                l.local_get(m).i64_const(1).binop(BinaryOp::I64Eq);
977                                l.if_else(
978                                    InstrSeqType::Simple(None),
979                                    |fm| {
980                                        fm.i64_const(1).local_set(res);
981                                        fm.br(oid);
982                                    },
983                                    |_| {},
984                                );
985                                l.local_get(i)
986                                    .i64_const(1)
987                                    .binop(BinaryOp::I64Add)
988                                    .local_set(i);
989                                l.br(lid);
990                            });
991                        });
992                    },
993                    |_| {},
994                );
995            },
996        );
997        bd.local_get(res);
998    }
999    fb.finish(vec![h, n], &mut wasm.funcs)
1000}
1001
1002/// `__str_index_of(h, n) -> i64` — `__str_contains`'s scan, returning the
1003/// first match position instead of a bool. Default `-1`; an empty needle
1004/// is at `0`.
1005fn build_str_index_of(wasm: &mut Module, get: FunctionId) -> FunctionId {
1006    let mut fb = FunctionBuilder::new(
1007        &mut wasm.types,
1008        &[ValType::I64, ValType::I64],
1009        &[ValType::I64],
1010    );
1011    let h = wasm.locals.add(ValType::I64);
1012    let n = wasm.locals.add(ValType::I64);
1013    let lh = wasm.locals.add(ValType::I64);
1014    let ln = wasm.locals.add(ValType::I64);
1015    let i = wasm.locals.add(ValType::I64);
1016    let j = wasm.locals.add(ValType::I64);
1017    let m = wasm.locals.add(ValType::I64);
1018    let res = wasm.locals.add(ValType::I64);
1019    {
1020        let mut bd = fb.func_body();
1021        bd.local_get(h).i64_const(0).call(get).local_set(lh);
1022        bd.local_get(n).i64_const(0).call(get).local_set(ln);
1023        bd.i64_const(-1).local_set(res); // default: not found
1024        bd.local_get(ln).i64_const(0).binop(BinaryOp::I64Eq);
1025        bd.if_else(
1026            InstrSeqType::Simple(None),
1027            |t| {
1028                // empty needle is found at index 0
1029                t.i64_const(0).local_set(res);
1030            },
1031            |e| {
1032                e.local_get(ln).local_get(lh).binop(BinaryOp::I64LeS);
1033                e.if_else(
1034                    InstrSeqType::Simple(None),
1035                    |s2| {
1036                        s2.i64_const(0).local_set(i);
1037                        s2.block(InstrSeqType::Simple(None), |o| {
1038                            let oid = o.id();
1039                            o.loop_(InstrSeqType::Simple(None), |l| {
1040                                let lid = l.id();
1041                                l.local_get(i)
1042                                    .local_get(ln)
1043                                    .binop(BinaryOp::I64Add)
1044                                    .local_get(lh)
1045                                    .binop(BinaryOp::I64GtS);
1046                                l.br_if(oid);
1047                                l.i64_const(1).local_set(m);
1048                                l.i64_const(0).local_set(j);
1049                                l.block(InstrSeqType::Simple(None), |ib| {
1050                                    let ibid = ib.id();
1051                                    ib.loop_(InstrSeqType::Simple(None), |il| {
1052                                        let ilid = il.id();
1053                                        il.local_get(j)
1054                                            .local_get(ln)
1055                                            .binop(BinaryOp::I64GeS);
1056                                        il.br_if(ibid);
1057                                        il.local_get(h);
1058                                        il.local_get(i)
1059                                            .local_get(j)
1060                                            .binop(BinaryOp::I64Add)
1061                                            .i64_const(1)
1062                                            .binop(BinaryOp::I64Add)
1063                                            .i64_const(8)
1064                                            .binop(BinaryOp::I64Mul);
1065                                        il.call(get);
1066                                        il.local_get(n);
1067                                        il.local_get(j)
1068                                            .i64_const(1)
1069                                            .binop(BinaryOp::I64Add)
1070                                            .i64_const(8)
1071                                            .binop(BinaryOp::I64Mul);
1072                                        il.call(get);
1073                                        il.binop(BinaryOp::I64Ne);
1074                                        il.if_else(
1075                                            InstrSeqType::Simple(None),
1076                                            |mm| {
1077                                                mm.i64_const(0).local_set(m);
1078                                                mm.br(ibid);
1079                                            },
1080                                            |_| {},
1081                                        );
1082                                        il.local_get(j)
1083                                            .i64_const(1)
1084                                            .binop(BinaryOp::I64Add)
1085                                            .local_set(j);
1086                                        il.br(ilid);
1087                                    });
1088                                });
1089                                l.local_get(m).i64_const(1).binop(BinaryOp::I64Eq);
1090                                l.if_else(
1091                                    InstrSeqType::Simple(None),
1092                                    |fm| {
1093                                        fm.local_get(i).local_set(res);
1094                                        fm.br(oid);
1095                                    },
1096                                    |_| {},
1097                                );
1098                                l.local_get(i)
1099                                    .i64_const(1)
1100                                    .binop(BinaryOp::I64Add)
1101                                    .local_set(i);
1102                                l.br(lid);
1103                            });
1104                        });
1105                    },
1106                    |_| {},
1107                );
1108            },
1109        );
1110        bd.local_get(res);
1111    }
1112    fb.finish(vec![h, n], &mut wasm.funcs)
1113}
1114
1115/// `__str_starts_with(s, p) -> 0|1`. Same shape as `__str_eq` but compares
1116/// only the first `lp` characters and requires `lp <= ls` (an empty prefix
1117/// is always a prefix).
1118fn build_str_starts_with(wasm: &mut Module, get: FunctionId) -> FunctionId {
1119    let mut fb = FunctionBuilder::new(
1120        &mut wasm.types,
1121        &[ValType::I64, ValType::I64],
1122        &[ValType::I64],
1123    );
1124    let s = wasm.locals.add(ValType::I64);
1125    let p = wasm.locals.add(ValType::I64);
1126    let ls = wasm.locals.add(ValType::I64);
1127    let lp = wasm.locals.add(ValType::I64);
1128    let i = wasm.locals.add(ValType::I64);
1129    let res = wasm.locals.add(ValType::I64);
1130    {
1131        let mut bd = fb.func_body();
1132        bd.local_get(s).i64_const(0).call(get).local_set(ls);
1133        bd.local_get(p).i64_const(0).call(get).local_set(lp);
1134        bd.i64_const(0).local_set(i);
1135        // lp > ls -> cannot be a prefix; otherwise assume yes then scan.
1136        bd.local_get(lp).local_get(ls).binop(BinaryOp::I64GtS);
1137        bd.if_else(
1138            InstrSeqType::Simple(None),
1139            |t| {
1140                t.i64_const(0).local_set(res);
1141            },
1142            |e| {
1143                e.i64_const(1).local_set(res);
1144                e.block(InstrSeqType::Simple(None), |o| {
1145                    let oid = o.id();
1146                    o.loop_(InstrSeqType::Simple(None), |l| {
1147                        let lid = l.id();
1148                        l.local_get(i).local_get(lp).binop(BinaryOp::I64GeS);
1149                        l.br_if(oid);
1150                        l.local_get(s);
1151                        l.local_get(i)
1152                            .i64_const(1)
1153                            .binop(BinaryOp::I64Add)
1154                            .i64_const(8)
1155                            .binop(BinaryOp::I64Mul);
1156                        l.call(get);
1157                        l.local_get(p);
1158                        l.local_get(i)
1159                            .i64_const(1)
1160                            .binop(BinaryOp::I64Add)
1161                            .i64_const(8)
1162                            .binop(BinaryOp::I64Mul);
1163                        l.call(get);
1164                        l.binop(BinaryOp::I64Ne);
1165                        l.if_else(
1166                            InstrSeqType::Simple(None),
1167                            |t2| {
1168                                t2.i64_const(0).local_set(res);
1169                                t2.br(oid);
1170                            },
1171                            |_| {},
1172                        );
1173                        l.local_get(i)
1174                            .i64_const(1)
1175                            .binop(BinaryOp::I64Add)
1176                            .local_set(i);
1177                        l.br(lid);
1178                    });
1179                });
1180            },
1181        );
1182        bd.local_get(res);
1183    }
1184    fb.finish(vec![s, p], &mut wasm.funcs)
1185}
1186
1187/// `__num_to_str(n) -> ptr`: decimal rendering. Counts digits, allocates
1188/// `[len, c0..]`, then fills from least-significant. Digits are taken in
1189/// the non-positive domain, so the full i64 range — including
1190/// `i64::MIN`, whose magnitude is not representable — renders correctly.
1191fn build_num_to_str(
1192    wasm: &mut Module,
1193    set: FunctionId,
1194    alloc: FunctionId,
1195) -> FunctionId {
1196    let mut fb =
1197        FunctionBuilder::new(&mut wasm.types, &[ValType::I64], &[ValType::I64]);
1198    let n = wasm.locals.add(ValType::I64);
1199    let neg = wasm.locals.add(ValType::I64);
1200    let m = wasm.locals.add(ValType::I64);
1201    let cnt = wasm.locals.add(ValType::I64);
1202    let len = wasm.locals.add(ValType::I64);
1203    let dst = wasm.locals.add(ValType::I64);
1204    let i = wasm.locals.add(ValType::I64);
1205    let q = wasm.locals.add(ValType::I64);
1206    {
1207        let mut bd = fb.func_body();
1208        // neg = (n < 0) ? 1 : 0  (an i64 flag; the compare yields i32, so
1209        // it cannot be stored into the i64 local directly)
1210        bd.local_get(n).i64_const(0).binop(BinaryOp::I64LtS);
1211        bd.if_else(
1212            InstrSeqType::Simple(None),
1213            |t| {
1214                t.i64_const(1).local_set(neg);
1215            },
1216            |e| {
1217                e.i64_const(0).local_set(neg);
1218            },
1219        );
1220        // m = (n > 0) ? -n : n  — the *non-positive* magnitude. Only a
1221        // positive n is negated (the positive i64 range always negates
1222        // safely); n <= 0, including i64::MIN, is kept as-is, so there
1223        // is no overflow. Digits are then taken in the negative domain.
1224        bd.local_get(n).i64_const(0).binop(BinaryOp::I64GtS);
1225        bd.if_else(
1226            InstrSeqType::Simple(None),
1227            |t| {
1228                t.i64_const(0)
1229                    .local_get(n)
1230                    .binop(BinaryOp::I64Sub)
1231                    .local_set(m);
1232            },
1233            |e| {
1234                e.local_get(n).local_set(m);
1235            },
1236        );
1237        bd.local_get(m).i64_const(0).binop(BinaryOp::I64Eq);
1238        bd.if_else(
1239            InstrSeqType::Simple(None),
1240            |t| {
1241                // "0": [len=1, '0']
1242                t.i64_const(16).call(alloc).local_set(dst);
1243                t.local_get(dst).i64_const(0).i64_const(1).call(set).drop();
1244                t.local_get(dst).i64_const(8).i64_const(48).call(set).drop();
1245            },
1246            |e| {
1247                // count digits of m
1248                e.i64_const(0).local_set(cnt);
1249                e.local_get(m).local_set(q);
1250                e.block(InstrSeqType::Simple(None), |o| {
1251                    let oid = o.id();
1252                    o.loop_(InstrSeqType::Simple(None), |l| {
1253                        let lid = l.id();
1254                        l.local_get(cnt)
1255                            .i64_const(1)
1256                            .binop(BinaryOp::I64Add)
1257                            .local_set(cnt);
1258                        l.local_get(q)
1259                            .i64_const(10)
1260                            .binop(BinaryOp::I64DivS)
1261                            .local_set(q);
1262                        l.local_get(q).i64_const(0).binop(BinaryOp::I64Eq);
1263                        l.br_if(oid);
1264                        l.br(lid);
1265                    });
1266                });
1267                e.local_get(cnt)
1268                    .local_get(neg)
1269                    .binop(BinaryOp::I64Add)
1270                    .local_set(len);
1271                e.local_get(len)
1272                    .i64_const(1)
1273                    .binop(BinaryOp::I64Add)
1274                    .i64_const(8)
1275                    .binop(BinaryOp::I64Mul)
1276                    .call(alloc)
1277                    .local_set(dst);
1278                e.local_get(dst).i64_const(0).local_get(len).call(set).drop();
1279                // fill from least-significant: i = len - 1 ; q = m
1280                e.local_get(len)
1281                    .i64_const(1)
1282                    .binop(BinaryOp::I64Sub)
1283                    .local_set(i);
1284                e.local_get(m).local_set(q);
1285                e.block(InstrSeqType::Simple(None), |o| {
1286                    let oid = o.id();
1287                    o.loop_(InstrSeqType::Simple(None), |l| {
1288                        let lid = l.id();
1289                        l.local_get(dst);
1290                        l.local_get(i)
1291                            .i64_const(1)
1292                            .binop(BinaryOp::I64Add)
1293                            .i64_const(8)
1294                            .binop(BinaryOp::I64Mul);
1295                        // q <= 0, so q % 10 <= 0; negate to the 0..9
1296                        // digit, then to ASCII.
1297                        l.local_get(q)
1298                            .i64_const(10)
1299                            .binop(BinaryOp::I64RemS)
1300                            .i64_const(-1)
1301                            .binop(BinaryOp::I64Mul)
1302                            .i64_const(48)
1303                            .binop(BinaryOp::I64Add);
1304                        l.call(set).drop();
1305                        l.local_get(q)
1306                            .i64_const(10)
1307                            .binop(BinaryOp::I64DivS)
1308                            .local_set(q);
1309                        l.local_get(i)
1310                            .i64_const(1)
1311                            .binop(BinaryOp::I64Sub)
1312                            .local_set(i);
1313                        l.local_get(q).i64_const(0).binop(BinaryOp::I64Eq);
1314                        l.br_if(oid);
1315                        l.br(lid);
1316                    });
1317                });
1318                // leading '-' for negatives
1319                e.local_get(neg).i64_const(0).binop(BinaryOp::I64Ne);
1320                e.if_else(
1321                    InstrSeqType::Simple(None),
1322                    |t| {
1323                        t.local_get(dst)
1324                            .i64_const(8)
1325                            .i64_const(45)
1326                            .call(set)
1327                            .drop();
1328                    },
1329                    |_| {},
1330                );
1331            },
1332        );
1333        bd.local_get(dst);
1334    }
1335    fb.finish(vec![n], &mut wasm.funcs)
1336}
1337
1338/// `__str_to_num(s) -> i64`: optional leading `-`, then leading decimal
1339/// digits; stops at the first non-digit. Empty / non-numeric yields 0.
1340fn build_str_to_num(wasm: &mut Module, get: FunctionId) -> FunctionId {
1341    let mut fb =
1342        FunctionBuilder::new(&mut wasm.types, &[ValType::I64], &[ValType::I64]);
1343    let s = wasm.locals.add(ValType::I64);
1344    let ls = wasm.locals.add(ValType::I64);
1345    let i = wasm.locals.add(ValType::I64);
1346    let neg = wasm.locals.add(ValType::I64);
1347    let acc = wasm.locals.add(ValType::I64);
1348    let ch = wasm.locals.add(ValType::I64);
1349    let res = wasm.locals.add(ValType::I64);
1350    {
1351        let mut bd = fb.func_body();
1352        bd.local_get(s).i64_const(0).call(get).local_set(ls);
1353        bd.i64_const(0).local_set(i);
1354        bd.i64_const(0).local_set(neg);
1355        bd.i64_const(0).local_set(acc);
1356        // leading '-' ?
1357        bd.local_get(ls).i64_const(0).binop(BinaryOp::I64GtS);
1358        bd.if_else(
1359            InstrSeqType::Simple(None),
1360            |t| {
1361                t.local_get(s).i64_const(8).call(get).i64_const(45);
1362                t.binop(BinaryOp::I64Eq);
1363                t.if_else(
1364                    InstrSeqType::Simple(None),
1365                    |t2| {
1366                        t2.i64_const(1).local_set(neg);
1367                        t2.i64_const(1).local_set(i);
1368                    },
1369                    |_| {},
1370                );
1371            },
1372            |_| {},
1373        );
1374        bd.block(InstrSeqType::Simple(None), |o| {
1375            let oid = o.id();
1376            o.loop_(InstrSeqType::Simple(None), |l| {
1377                let lid = l.id();
1378                l.local_get(i).local_get(ls).binop(BinaryOp::I64GeS);
1379                l.br_if(oid);
1380                l.local_get(s);
1381                l.local_get(i)
1382                    .i64_const(1)
1383                    .binop(BinaryOp::I64Add)
1384                    .i64_const(8)
1385                    .binop(BinaryOp::I64Mul);
1386                l.call(get).local_set(ch);
1387                l.local_get(ch).i64_const(48).binop(BinaryOp::I64LtS);
1388                l.local_get(ch).i64_const(57).binop(BinaryOp::I64GtS);
1389                l.binop(BinaryOp::I32Or); // both operands are i32 (compare results)
1390                l.br_if(oid);
1391                l.local_get(acc).i64_const(10).binop(BinaryOp::I64Mul);
1392                l.local_get(ch).i64_const(48).binop(BinaryOp::I64Sub);
1393                l.binop(BinaryOp::I64Add).local_set(acc);
1394                l.local_get(i)
1395                    .i64_const(1)
1396                    .binop(BinaryOp::I64Add)
1397                    .local_set(i);
1398                l.br(lid);
1399            });
1400        });
1401        bd.local_get(neg).i64_const(1).binop(BinaryOp::I64Eq);
1402        bd.if_else(
1403            InstrSeqType::Simple(None),
1404            |t| {
1405                t.i64_const(0)
1406                    .local_get(acc)
1407                    .binop(BinaryOp::I64Sub)
1408                    .local_set(res);
1409            },
1410            |e| {
1411                e.local_get(acc).local_set(res);
1412            },
1413        );
1414        bd.local_get(res);
1415    }
1416    fb.finish(vec![s], &mut wasm.funcs)
1417}
1418
1419/// `__str_to_num_opt(s) -> Option` block: `Some(n)` only if the whole
1420/// string is a valid integer (optional `-`, then ≥1 digits, nothing
1421/// else), else `None`. Like `__str_to_num` but tracks validity and
1422/// returns a `[tag,val]` block instead of a lenient 0.
1423fn build_str_to_num_opt(
1424    wasm: &mut Module,
1425    get: FunctionId,
1426    set: FunctionId,
1427    alloc: FunctionId,
1428) -> FunctionId {
1429    let mut fb =
1430        FunctionBuilder::new(&mut wasm.types, &[ValType::I64], &[ValType::I64]);
1431    let s = wasm.locals.add(ValType::I64);
1432    let ls = wasm.locals.add(ValType::I64);
1433    let i = wasm.locals.add(ValType::I64);
1434    let neg = wasm.locals.add(ValType::I64);
1435    let acc = wasm.locals.add(ValType::I64);
1436    let ch = wasm.locals.add(ValType::I64);
1437    let ok = wasm.locals.add(ValType::I64);
1438    let dc = wasm.locals.add(ValType::I64);
1439    let res = wasm.locals.add(ValType::I64);
1440    let p = wasm.locals.add(ValType::I64);
1441    {
1442        let mut bd = fb.func_body();
1443        bd.local_get(s).i64_const(0).call(get).local_set(ls);
1444        bd.i64_const(0).local_set(i);
1445        bd.i64_const(0).local_set(neg);
1446        bd.i64_const(0).local_set(acc);
1447        bd.i64_const(1).local_set(ok);
1448        bd.i64_const(0).local_set(dc);
1449        // optional leading '-'
1450        bd.local_get(ls).i64_const(0).binop(BinaryOp::I64GtS);
1451        bd.if_else(
1452            InstrSeqType::Simple(None),
1453            |t| {
1454                t.local_get(s).i64_const(8).call(get).i64_const(45);
1455                t.binop(BinaryOp::I64Eq);
1456                t.if_else(
1457                    InstrSeqType::Simple(None),
1458                    |t2| {
1459                        t2.i64_const(1).local_set(neg);
1460                        t2.i64_const(1).local_set(i);
1461                    },
1462                    |_| {},
1463                );
1464            },
1465            |_| {},
1466        );
1467        bd.block(InstrSeqType::Simple(None), |o| {
1468            let oid = o.id();
1469            o.loop_(InstrSeqType::Simple(None), |l| {
1470                let lid = l.id();
1471                l.local_get(i).local_get(ls).binop(BinaryOp::I64GeS);
1472                l.br_if(oid);
1473                l.local_get(s);
1474                l.local_get(i)
1475                    .i64_const(1)
1476                    .binop(BinaryOp::I64Add)
1477                    .i64_const(8)
1478                    .binop(BinaryOp::I64Mul);
1479                l.call(get).local_set(ch);
1480                l.local_get(ch).i64_const(48).binop(BinaryOp::I64LtS);
1481                l.local_get(ch).i64_const(57).binop(BinaryOp::I64GtS);
1482                l.binop(BinaryOp::I32Or);
1483                l.if_else(
1484                    InstrSeqType::Simple(None),
1485                    |bad| {
1486                        // non-digit -> invalid, stop scanning
1487                        bad.i64_const(0).local_set(ok);
1488                        bad.br(oid);
1489                    },
1490                    |_| {},
1491                );
1492                l.local_get(acc).i64_const(10).binop(BinaryOp::I64Mul);
1493                l.local_get(ch).i64_const(48).binop(BinaryOp::I64Sub);
1494                l.binop(BinaryOp::I64Add).local_set(acc);
1495                l.local_get(dc).i64_const(1).binop(BinaryOp::I64Add).local_set(dc);
1496                l.local_get(i)
1497                    .i64_const(1)
1498                    .binop(BinaryOp::I64Add)
1499                    .local_set(i);
1500                l.br(lid);
1501            });
1502        });
1503        // need at least one digit
1504        bd.local_get(dc).i64_const(0).binop(BinaryOp::I64Eq);
1505        bd.if_else(
1506            InstrSeqType::Simple(None),
1507            |z| {
1508                z.i64_const(0).local_set(ok);
1509            },
1510            |_| {},
1511        );
1512        // res = neg ? -acc : acc
1513        bd.local_get(neg).i64_const(1).binop(BinaryOp::I64Eq);
1514        bd.if_else(
1515            InstrSeqType::Simple(None),
1516            |t| {
1517                t.i64_const(0)
1518                    .local_get(acc)
1519                    .binop(BinaryOp::I64Sub)
1520                    .local_set(res);
1521            },
1522            |e| {
1523                e.local_get(acc).local_set(res);
1524            },
1525        );
1526        // Option block: [1,res] if ok else [0,_]
1527        bd.i64_const(16).call(alloc).local_set(p);
1528        bd.local_get(ok).i64_const(0).binop(BinaryOp::I64Ne);
1529        bd.if_else(
1530            InstrSeqType::Simple(None),
1531            |t| {
1532                t.local_get(p).i64_const(0).i64_const(1).call(set).drop();
1533                t.local_get(p).i64_const(8).local_get(res).call(set).drop();
1534            },
1535            |e| {
1536                e.local_get(p).i64_const(0).i64_const(0).call(set).drop();
1537            },
1538        );
1539        bd.local_get(p);
1540    }
1541    fb.finish(vec![s], &mut wasm.funcs)
1542}
1543
1544/// `__list_cons(head, tail) -> ptr`: a new `[len+1, head, tail0, tail1..]`
1545/// block (O(n) copy; immutable, no structural sharing — the bump
1546/// allocator never frees, a documented v0.3 simplification).
1547fn build_list_cons(
1548    wasm: &mut Module,
1549    get: FunctionId,
1550    set: FunctionId,
1551    alloc: FunctionId,
1552) -> FunctionId {
1553    let mut fb = FunctionBuilder::new(
1554        &mut wasm.types,
1555        &[ValType::I64, ValType::I64],
1556        &[ValType::I64],
1557    );
1558    let head = wasm.locals.add(ValType::I64);
1559    let tail = wasm.locals.add(ValType::I64);
1560    let tl = wasm.locals.add(ValType::I64);
1561    let nl = wasm.locals.add(ValType::I64);
1562    let dst = wasm.locals.add(ValType::I64);
1563    let i = wasm.locals.add(ValType::I64);
1564    {
1565        let mut bd = fb.func_body();
1566        bd.local_get(tail).i64_const(0).call(get).local_set(tl);
1567        bd.local_get(tl).i64_const(1).binop(BinaryOp::I64Add).local_set(nl);
1568        bd.local_get(nl)
1569            .i64_const(1)
1570            .binop(BinaryOp::I64Add)
1571            .i64_const(8)
1572            .binop(BinaryOp::I64Mul)
1573            .call(alloc)
1574            .local_set(dst);
1575        bd.local_get(dst).i64_const(0).local_get(nl).call(set).drop();
1576        // element 0 = head, at slot 1 (byte offset 8)
1577        bd.local_get(dst).i64_const(8).local_get(head).call(set).drop();
1578        bd.i64_const(0).local_set(i);
1579        bd.block(InstrSeqType::Simple(None), |o| {
1580            let oid = o.id();
1581            o.loop_(InstrSeqType::Simple(None), |l| {
1582                let lid = l.id();
1583                l.local_get(i).local_get(tl).binop(BinaryOp::I64GeS);
1584                l.br_if(oid);
1585                // dst[(i+2)*8] = tail[(i+1)*8]
1586                l.local_get(dst);
1587                l.local_get(i)
1588                    .i64_const(2)
1589                    .binop(BinaryOp::I64Add)
1590                    .i64_const(8)
1591                    .binop(BinaryOp::I64Mul);
1592                l.local_get(tail);
1593                l.local_get(i)
1594                    .i64_const(1)
1595                    .binop(BinaryOp::I64Add)
1596                    .i64_const(8)
1597                    .binop(BinaryOp::I64Mul);
1598                l.call(get);
1599                l.call(set);
1600                l.drop();
1601                l.local_get(i)
1602                    .i64_const(1)
1603                    .binop(BinaryOp::I64Add)
1604                    .local_set(i);
1605                l.br(lid);
1606            });
1607        });
1608        bd.local_get(dst);
1609    }
1610    fb.finish(vec![head, tail], &mut wasm.funcs)
1611}
1612
1613/// Free-variable names in `hash`'s subtree not in `bound`, in
1614/// first-occurrence order. Binders are `Lambda` params and `Match` arm
1615/// payload bindings; `Call`/`FuncRef` names are module functions, never
1616/// captures.
1617fn free_vars(
1618    store: &Store,
1619    hash: &NodeHash,
1620    bound: &HashSet<String>,
1621    acc: &mut Vec<String>,
1622) -> LowerResult<()> {
1623    let Some(node) = store.get(hash)? else {
1624        return Ok(());
1625    };
1626    match node {
1627        Node::Ref(n) => {
1628            if !bound.contains(&n) && !acc.contains(&n) {
1629                acc.push(n);
1630            }
1631        }
1632        Node::Lambda { params, body } => {
1633            let mut b2 = bound.clone();
1634            for p in &params {
1635                b2.insert(p.name.clone());
1636            }
1637            free_vars(store, &body, &b2, acc)?;
1638        }
1639        Node::Match {
1640            scrutinee, arms, ..
1641        } => {
1642            free_vars(store, &scrutinee, bound, acc)?;
1643            for arm in &arms {
1644                let mut b2 = bound.clone();
1645                for x in &arm.bindings {
1646                    b2.insert(x.clone());
1647                }
1648                free_vars(store, &arm.body, &b2, acc)?;
1649            }
1650        }
1651        other => {
1652            for ch in crate::check::child_hashes(&other) {
1653                free_vars(store, ch, bound, acc)?;
1654            }
1655        }
1656    }
1657    Ok(())
1658}
1659
1660/// A lambda lifted to a top-level wasm function: its own structure plus
1661/// the ordered names it captures from the enclosing scope.
1662struct LamInfo {
1663    hash: NodeHash,
1664    params: Vec<crate::node::Param>,
1665    body: NodeHash,
1666    captures: Vec<String>,
1667}
1668
1669/// Collect every `Lambda` reachable from `hash` (content-addressed, so
1670/// structurally identical lambdas are one lifted function — the env is
1671/// still built per creation site).
1672fn collect_lambdas(
1673    store: &Store,
1674    hash: &NodeHash,
1675    seen: &mut HashSet<NodeHash>,
1676    out: &mut Vec<LamInfo>,
1677) -> LowerResult<()> {
1678    let Some(node) = store.get(hash)? else {
1679        return Ok(());
1680    };
1681    if let Node::Lambda { params, body } = &node {
1682        if seen.insert(hash.clone()) {
1683            let mut bound = HashSet::new();
1684            for p in params {
1685                bound.insert(p.name.clone());
1686            }
1687            let mut caps = Vec::new();
1688            free_vars(store, body, &bound, &mut caps)?;
1689            out.push(LamInfo {
1690                hash: hash.clone(),
1691                params: params.clone(),
1692                body: body.clone(),
1693                captures: caps,
1694            });
1695        }
1696        return collect_lambdas(store, body, seen, out);
1697    }
1698    for ch in crate::check::child_hashes(&node) {
1699        collect_lambdas(store, ch, seen, out)?;
1700    }
1701    Ok(())
1702}
1703
1704pub fn lower(store: &Store, module_hash: &NodeHash) -> LowerResult<Vec<u8>> {
1705    let Some(Node::Module {
1706        types, functions, ..
1707    }) = store.get(module_hash)?
1708    else {
1709        return Err(LowerError::NotAModule);
1710    };
1711
1712    let mut wasm = Module::default();
1713
1714    // Record field-order and variant case/payload tables (declaration order
1715    // = layout order).
1716    let mut recs: HashMap<String, Vec<String>> = HashMap::new();
1717    let mut vars: HashMap<String, Vec<(String, Vec<String>)>> = HashMap::new();
1718    for th in &types {
1719        match store.get(th)? {
1720            Some(Node::RecordDef { name, fields }) => {
1721                recs.insert(name, fields.into_iter().map(|(n, _)| n).collect());
1722            }
1723            Some(Node::VariantDef { name, cases }) => {
1724                vars.insert(
1725                    name,
1726                    cases
1727                        .into_iter()
1728                        .map(|(c, p)| (c, p.into_iter().map(|(n, _)| n).collect()))
1729                        .collect(),
1730                );
1731            }
1732            _ => {}
1733        }
1734    }
1735
1736    // Linear memory + bump pointer + the three helpers.
1737    let mem = wasm.memories.add_local(false, false, 1, None, None);
1738    let bump = wasm.globals.add_local(
1739        ValType::I64,
1740        true,
1741        false,
1742        ConstExpr::Value(Value::I64(16)),
1743    );
1744    let alloc = build_alloc(&mut wasm, bump, mem);
1745    let set = build_set(&mut wasm, mem);
1746    let get = build_get(&mut wasm, mem);
1747    let map_get = build_map_get(&mut wasm, get);
1748    let map_try_get = build_map_try_get(&mut wasm, get, set, alloc);
1749    let str_concat = build_str_concat(&mut wasm, get, set, alloc);
1750    let str_slice = build_str_slice(&mut wasm, get, set, alloc);
1751    let str_lower = build_str_lower(&mut wasm, get, set, alloc);
1752    let str_from_code = build_str_from_code(&mut wasm, set, alloc);
1753    let str_eq = build_str_eq(&mut wasm, get);
1754    let str_contains = build_str_contains(&mut wasm, get);
1755    let str_starts_with = build_str_starts_with(&mut wasm, get);
1756    let str_index_of = build_str_index_of(&mut wasm, get);
1757    let num_to_str = build_num_to_str(&mut wasm, set, alloc);
1758    let str_to_num = build_str_to_num(&mut wasm, get);
1759    let str_to_num_opt = build_str_to_num_opt(&mut wasm, get, set, alloc);
1760    let list_cons = build_list_cons(&mut wasm, get, set, alloc);
1761    let list_get = build_list_get(&mut wasm, get);
1762    let list_try_get = build_list_try_get(&mut wasm, get, set, alloc);
1763    // Exported so the fallible-run harness can read a returned [tag,val],
1764    // and so host effects can build a Cairn string in wasm memory:
1765    // call `__alloc` from the Caller, then write the bytes directly.
1766    wasm.exports.add("mem", mem);
1767    wasm.exports.add("__alloc", alloc);
1768
1769    // Host import for the Time effect: `host::now : () -> i64`. Always
1770    // imported; the run harnesses supply it (default 0 when unused), so a
1771    // module never has an unsatisfied import.
1772    let now_ty = wasm.types.add(&[], &[ValType::I64]);
1773    let (now, _) = wasm.add_import_func("host", "now", now_ty);
1774    let log_ty = wasm.types.add(&[ValType::I64], &[ValType::I64]);
1775    let (log, _) = wasm.add_import_func("host", "log", log_ty);
1776    let publish_ty = wasm.types.add(&[ValType::I64], &[ValType::I64]);
1777    let (publish, _) = wasm.add_import_func("host", "publish", publish_ty);
1778    let set_header_ty =
1779        wasm.types.add(&[ValType::I64, ValType::I64], &[ValType::I64]);
1780    let (set_header, _) =
1781        wasm.add_import_func("host", "set_header", set_header_ty);
1782    let rand_ty = wasm.types.add(&[], &[ValType::I64]);
1783    let (rand, _) = wasm.add_import_func("host", "rand", rand_ty);
1784    let dw_ty = wasm.types.add(&[ValType::I64, ValType::I64], &[ValType::I64]);
1785    let (disk_write, _) = wasm.add_import_func("host", "disk_write", dw_ty);
1786    let dr_ty = wasm.types.add(&[ValType::I64], &[ValType::I64]);
1787    let (disk_read, _) = wasm.add_import_func("host", "disk_read", dr_ty);
1788    let ng_ty = wasm.types.add(&[ValType::I64], &[ValType::I64]);
1789    let (net_get, _) = wasm.add_import_func("host", "net_get", ng_ty);
1790    let dq_ty = wasm.types.add(&[ValType::I64, ValType::I64], &[ValType::I64]);
1791    let (db_query, _) = wasm.add_import_func("host", "db_query", dq_ty);
1792
1793    // Module-wide failure -> tag (1-based; 0 = ok) and the set of fallible
1794    // functions (non-empty on_failure → returns a [tag,val] block).
1795    let mut ftags: HashMap<String, i64> = HashMap::new();
1796    let mut fallible: HashSet<String> = HashSet::new();
1797    for fh in &functions {
1798        if let Some(Node::Function {
1799            name, on_failure, ..
1800        }) = store.get(fh)?
1801        {
1802            if !on_failure.is_empty() {
1803                fallible.insert(name.clone());
1804            }
1805            for f in on_failure {
1806                let next = (ftags.len() as i64) + 1;
1807                ftags.entry(f).or_insert(next);
1808            }
1809        }
1810    }
1811
1812    // Everything pass B needs to fill one function's body, once every
1813    // function's id is known.
1814    struct Pending {
1815        is_fallible: bool,
1816        body: Vec<NodeHash>,
1817        result: NodeHash,
1818        /// Parameter name -> local; step bindings are added during pass B.
1819        scope: HashMap<String, Slot>,
1820        id: FunctionId,
1821    }
1822
1823    let mut fn_ids: HashMap<String, FunctionId> = HashMap::new();
1824    // Function-value support. A function value is a heap block
1825    // `[code_idx, env]`; `code_idx` selects an entry in one funcref
1826    // table. Every table entry has the closure ABI `(i64×n, i64 env) ->
1827    // i64`: for a top-level function that entry is a *thunk* that drops
1828    // env and calls the real (env-free) function — so direct `Call` and
1829    // the run/serve harnesses keep the unchanged top-level ABI, and only
1830    // the indirect path carries env. A lambda's entry is its lifted body.
1831    let mut table_ids: Vec<FunctionId> = Vec::new();
1832    let mut fn_table_idx: HashMap<String, i64> = HashMap::new();
1833    let mut lam_table: HashMap<NodeHash, (i64, Vec<String>)> = HashMap::new();
1834    let mut pending: Vec<Pending> = Vec::with_capacity(functions.len());
1835    // (name, real id, arity) captured in pass A to build thunks after it.
1836    let mut top_level: Vec<(String, FunctionId, usize)> = Vec::new();
1837
1838    // Discover every lambda (anywhere in any function) up front so its
1839    // lifted function can be reserved before pass B.
1840    let mut lam_infos: Vec<LamInfo> = Vec::new();
1841    {
1842        let mut seen = HashSet::new();
1843        for fh in &functions {
1844            if let Some(Node::Function { body, result, .. }) = store.get(fh)? {
1845                for s in &body {
1846                    collect_lambdas(store, s, &mut seen, &mut lam_infos)?;
1847                }
1848                collect_lambdas(store, &result, &mut seen, &mut lam_infos)?;
1849            }
1850        }
1851    }
1852
1853    // Pass A: reserve a `FunctionId` (and export) for every function with an
1854    // empty body. `FunctionBuilder::finish` does not validate, so an
1855    // empty-body finish is a safe placeholder — it just allocates the id and
1856    // its (param-typed) signature. With every name resolved up front, pass B
1857    // can lower any call regardless of definition order: self, forward, and
1858    // mutual recursion all work.
1859    //
1860    // Effectful functions lower fine: a declared effect only matters at the
1861    // operation that performs it (e.g. `Now` -> host::now), and the checker
1862    // has already verified `requires` covers what's performed. Generics need
1863    // no monomorphization: every Cairn value has the uniform i64
1864    // representation (scalars and record/variant pointers alike), so a
1865    // generic body is type-agnostic at the wasm level — type erasure is
1866    // sound and complete. `type_params` is thus irrelevant to codegen.
1867    for fh in &functions {
1868        let Some(Node::Function {
1869            name,
1870            params,
1871            on_failure,
1872            body,
1873            result,
1874            ..
1875        }) = store.get(fh)?
1876        else {
1877            continue;
1878        };
1879        let param_tys = vec![ValType::I64; params.len()];
1880        let fb = FunctionBuilder::new(&mut wasm.types, &param_tys, &[ValType::I64]);
1881
1882        // Parameters are i64 locals; step bindings are added in pass B.
1883        let mut scope: HashMap<String, Slot> = HashMap::new();
1884        let mut param_locals = Vec::with_capacity(params.len());
1885        for p in &params {
1886            let l = wasm.locals.add(ValType::I64);
1887            scope.insert(p.name.clone(), Slot::Local(l));
1888            param_locals.push(l);
1889        }
1890
1891        let id = fb.finish(param_locals, &mut wasm.funcs);
1892        wasm.exports.add(&name, id);
1893        top_level.push((name.clone(), id, params.len()));
1894        fn_ids.insert(name, id);
1895        pending.push(Pending {
1896            is_fallible: !on_failure.is_empty(),
1897            body,
1898            result,
1899            scope,
1900            id,
1901        });
1902    }
1903
1904    // A thunk per top-level function: `(i64×n, i64 env) -> i64`, drops
1905    // env, tail-calls the real function. This is the funcref-table entry
1906    // for `&name`, keeping the direct-call ABI untouched.
1907    let mut max_arity = 0usize;
1908    for (name, real_id, arity) in &top_level {
1909        max_arity = max_arity.max(*arity);
1910        let sig: Vec<ValType> = vec![ValType::I64; arity + 1];
1911        let fb = FunctionBuilder::new(&mut wasm.types, &sig, &[ValType::I64]);
1912        let arg_locals: Vec<LocalId> =
1913            (0..arity + 1).map(|_| wasm.locals.add(ValType::I64)).collect();
1914        let tid = {
1915            let mut b = fb;
1916            {
1917                let mut body = b.func_body();
1918                for l in arg_locals.iter().take(*arity) {
1919                    body.local_get(*l);
1920                }
1921                body.call(*real_id); // env (last local) intentionally unused
1922            }
1923            b.finish(arg_locals, &mut wasm.funcs)
1924        };
1925        let idx = table_ids.len() as i64;
1926        table_ids.push(tid);
1927        fn_table_idx.insert(name.clone(), idx);
1928    }
1929
1930    // Reserve each lambda's lifted function `(i64×n, i64 env) -> i64`.
1931    // Params are i64 locals; captures read from the trailing env via
1932    // `Slot::Member` (the same machinery `match` bindings use).
1933    for li in &lam_infos {
1934        let arity = li.params.len();
1935        max_arity = max_arity.max(arity);
1936        let sig: Vec<ValType> = vec![ValType::I64; arity + 1];
1937        let fb = FunctionBuilder::new(&mut wasm.types, &sig, &[ValType::I64]);
1938        let mut scope: HashMap<String, Slot> = HashMap::new();
1939        let mut locals = Vec::with_capacity(arity + 1);
1940        for p in &li.params {
1941            let l = wasm.locals.add(ValType::I64);
1942            scope.insert(p.name.clone(), Slot::Local(l));
1943            locals.push(l);
1944        }
1945        let env = wasm.locals.add(ValType::I64);
1946        locals.push(env);
1947        for (j, cap) in li.captures.iter().enumerate() {
1948            scope.insert(
1949                cap.clone(),
1950                Slot::Member {
1951                    base: env,
1952                    off: (j as i64) * 8,
1953                },
1954            );
1955        }
1956        let id = fb.finish(locals, &mut wasm.funcs);
1957        let idx = table_ids.len() as i64;
1958        table_ids.push(id);
1959        lam_table.insert(li.hash.clone(), (idx, li.captures.clone()));
1960        pending.push(Pending {
1961            is_fallible: false,
1962            body: vec![],
1963            result: li.body.clone(),
1964            scope,
1965            id,
1966        });
1967    }
1968
1969    // The single funcref table (thunks then lambdas, in push order) and
1970    // one closure-ABI type `(i64×k, i64 env) -> i64` per arity present.
1971    let tlen = table_ids.len() as u64;
1972    let func_table =
1973        wasm.tables
1974            .add_local(false, tlen, Some(tlen), RefType::Funcref);
1975    if !table_ids.is_empty() {
1976        wasm.elements.add(
1977            ElementKind::Active {
1978                table: func_table,
1979                offset: ConstExpr::Value(Value::I32(0)),
1980            },
1981            ElementItems::Functions(table_ids.clone()),
1982        );
1983    }
1984    let mut indirect_types: HashMap<usize, TypeId> = HashMap::new();
1985    for k in 0..=max_arity {
1986        let sig = vec![ValType::I64; k + 1];
1987        let ty = wasm.types.add(&sig, &[ValType::I64]);
1988        indirect_types.insert(k, ty);
1989    }
1990
1991    // Pass B: fill each reserved function's body. `fn_ids` is now complete,
1992    // so `lower_expr` resolves every call.
1993    for mut p in pending {
1994        let locals_cell = RefCell::new(&mut wasm.locals);
1995        let ctx = Ctx {
1996            store,
1997            fns: &fn_ids,
1998            fn_table_idx: &fn_table_idx,
1999            lambdas: &lam_table,
2000            func_table,
2001            indirect_types: &indirect_types,
2002            recs: &recs,
2003            vars: &vars,
2004            ftags: &ftags,
2005            fallible: &fallible,
2006            locals: &locals_cell,
2007            alloc,
2008            set,
2009            get,
2010            map_get,
2011            map_try_get,
2012            now,
2013            log,
2014            publish,
2015            set_header,
2016            rand,
2017            disk_write,
2018            disk_read,
2019            net_get,
2020            db_query,
2021            str_concat,
2022            str_slice,
2023            str_lower,
2024            str_from_code,
2025            str_eq,
2026            str_contains,
2027            str_starts_with,
2028            str_index_of,
2029            num_to_str,
2030            str_to_num,
2031            str_to_num_opt,
2032            list_cons,
2033            list_get,
2034            list_try_get,
2035        };
2036        let lf = wasm.funcs.get_mut(p.id).kind.unwrap_local_mut();
2037        let mut seq = lf.builder_mut().func_body();
2038        for step_hash in &p.body {
2039            match store.get(step_hash)? {
2040                Some(Node::Step { binding, value }) => {
2041                    lower_expr(&ctx, &p.scope, &mut seq, &value)?;
2042                    let l = locals_cell.borrow_mut().add(ValType::I64);
2043                    seq.local_set(l);
2044                    p.scope.insert(binding, Slot::Local(l));
2045                }
2046                Some(Node::Hole { .. }) => return Err(LowerError::Hole),
2047                _ => return Err(LowerError::Unsupported("non-step in function body")),
2048            }
2049        }
2050        lower_expr(&ctx, &p.scope, &mut seq, &p.result)?;
2051        if p.is_fallible {
2052            // Wrap the ok value into a [tag=0, val] block (failure paths
2053            // already `return`ed their own block before reaching here).
2054            let tmp = locals_cell.borrow_mut().add(ValType::I64);
2055            seq.local_set(tmp);
2056            seq.i64_const(16);
2057            seq.call(alloc); // [p]
2058            seq.i64_const(0);
2059            seq.i64_const(0);
2060            seq.call(set); // __set(p,0,0) tag=ok
2061            seq.i64_const(8);
2062            seq.local_get(tmp);
2063            seq.call(set); // __set(p,8,val)
2064        }
2065    }
2066
2067    Ok(wasm.emit_wasm())
2068}
2069
2070fn lower_expr(
2071    c: &Ctx,
2072    scope: &HashMap<String, Slot>,
2073    seq: &mut InstrSeqBuilder,
2074    hash: &NodeHash,
2075) -> LowerResult<()> {
2076    match c.store.get(hash)? {
2077        Some(Node::Lit(v)) => {
2078            seq.i64_const(v);
2079        }
2080        Some(Node::FloatLit(bits)) => {
2081            // The uniform i64 slot *is* the f64 bit pattern.
2082            seq.i64_const(bits as i64);
2083        }
2084        Some(Node::FloatOp { op, lhs, rhs }) => {
2085            use crate::node::BinOp::*;
2086            lower_expr(c, scope, seq, &lhs)?;
2087            seq.unop(UnaryOp::F64ReinterpretI64);
2088            lower_expr(c, scope, seq, &rhs)?;
2089            seq.unop(UnaryOp::F64ReinterpretI64);
2090            match op {
2091                Add => {
2092                    seq.binop(BinaryOp::F64Add)
2093                        .unop(UnaryOp::I64ReinterpretF64);
2094                }
2095                Sub => {
2096                    seq.binop(BinaryOp::F64Sub)
2097                        .unop(UnaryOp::I64ReinterpretF64);
2098                }
2099                Mul => {
2100                    seq.binop(BinaryOp::F64Mul)
2101                        .unop(UnaryOp::I64ReinterpretF64);
2102                }
2103                Div => {
2104                    seq.binop(BinaryOp::F64Div)
2105                        .unop(UnaryOp::I64ReinterpretF64);
2106                }
2107                Eq => {
2108                    seq.binop(BinaryOp::F64Eq).unop(UnaryOp::I64ExtendUI32);
2109                }
2110                Lt => {
2111                    seq.binop(BinaryOp::F64Lt).unop(UnaryOp::I64ExtendUI32);
2112                }
2113                Le => {
2114                    seq.binop(BinaryOp::F64Le).unop(UnaryOp::I64ExtendUI32);
2115                }
2116                Gt => {
2117                    seq.binop(BinaryOp::F64Gt).unop(UnaryOp::I64ExtendUI32);
2118                }
2119                Ge => {
2120                    seq.binop(BinaryOp::F64Ge).unop(UnaryOp::I64ExtendUI32);
2121                }
2122                // Rejected by the checker; unreachable in valid programs.
2123                Mod | Neq | And | Or => {
2124                    return Err(LowerError::Unsupported(
2125                        "operator not defined on Float",
2126                    ));
2127                }
2128            }
2129        }
2130        Some(Node::IntToFloat(a)) => {
2131            lower_expr(c, scope, seq, &a)?;
2132            seq.unop(UnaryOp::F64ConvertSI64)
2133                .unop(UnaryOp::I64ReinterpretF64);
2134        }
2135        Some(Node::FloatToInt(a)) => {
2136            lower_expr(c, scope, seq, &a)?;
2137            seq.unop(UnaryOp::F64ReinterpretI64)
2138                .unop(UnaryOp::I64TruncSF64);
2139        }
2140        Some(Node::DecimalLit(v)) => {
2141            // The slot value is the pre-scaled i64.
2142            seq.i64_const(v);
2143        }
2144        Some(Node::DecimalOp { op, lhs, rhs }) => {
2145            use crate::node::BinOp::*;
2146            match op {
2147                // (a*b) and (a/b) must rescale by 10_000.
2148                Mul => {
2149                    lower_expr(c, scope, seq, &lhs)?;
2150                    lower_expr(c, scope, seq, &rhs)?;
2151                    seq.binop(BinaryOp::I64Mul)
2152                        .i64_const(10_000)
2153                        .binop(BinaryOp::I64DivS);
2154                }
2155                Div => {
2156                    lower_expr(c, scope, seq, &lhs)?;
2157                    seq.i64_const(10_000).binop(BinaryOp::I64Mul);
2158                    lower_expr(c, scope, seq, &rhs)?;
2159                    seq.binop(BinaryOp::I64DivS);
2160                }
2161                Add => {
2162                    lower_expr(c, scope, seq, &lhs)?;
2163                    lower_expr(c, scope, seq, &rhs)?;
2164                    seq.binop(BinaryOp::I64Add);
2165                }
2166                Sub => {
2167                    lower_expr(c, scope, seq, &lhs)?;
2168                    lower_expr(c, scope, seq, &rhs)?;
2169                    seq.binop(BinaryOp::I64Sub);
2170                }
2171                Eq | Neq | Lt | Le | Gt | Ge => {
2172                    lower_expr(c, scope, seq, &lhs)?;
2173                    lower_expr(c, scope, seq, &rhs)?;
2174                    match op {
2175                        Eq => seq.binop(BinaryOp::I64Eq),
2176                        Neq => seq.binop(BinaryOp::I64Ne),
2177                        Lt => seq.binop(BinaryOp::I64LtS),
2178                        Le => seq.binop(BinaryOp::I64LeS),
2179                        Gt => seq.binop(BinaryOp::I64GtS),
2180                        Ge => seq.binop(BinaryOp::I64GeS),
2181                        _ => unreachable!(),
2182                    };
2183                    seq.unop(UnaryOp::I64ExtendUI32);
2184                }
2185                Mod | And | Or => {
2186                    return Err(LowerError::Unsupported(
2187                        "operator not defined on Decimal",
2188                    ));
2189                }
2190            }
2191        }
2192        Some(Node::IntToDecimal(a)) => {
2193            lower_expr(c, scope, seq, &a)?;
2194            seq.i64_const(10_000).binop(BinaryOp::I64Mul);
2195        }
2196        Some(Node::DecimalToInt(a)) => {
2197            lower_expr(c, scope, seq, &a)?;
2198            seq.i64_const(10_000).binop(BinaryOp::I64DivS);
2199        }
2200        Some(Node::DecimalRaw(a)) => {
2201            // Identity: a Decimal already *is* its scaled i64 mantissa.
2202            lower_expr(c, scope, seq, &a)?;
2203        }
2204        Some(Node::Bool(b)) => {
2205            seq.i64_const(if b { 1 } else { 0 });
2206        }
2207        Some(Node::Not(a)) => {
2208            // Operand is the 0/1 Bool ABI; `i64.eqz` is exactly logical
2209            // not (1 iff 0), then widen back to the uniform i64.
2210            lower_expr(c, scope, seq, &a)?;
2211            seq.unop(UnaryOp::I64Eqz).unop(UnaryOp::I64ExtendUI32);
2212        }
2213        Some(Node::Ref(name)) => {
2214            match scope
2215                .get(&name)
2216                .ok_or(LowerError::UnresolvedRef(name.clone()))?
2217            {
2218                Slot::Local(l) => {
2219                    seq.local_get(*l);
2220                }
2221                Slot::Member { base, off } => {
2222                    // A `match` payload binding: load from base+off.
2223                    seq.local_get(*base);
2224                    seq.i64_const(*off);
2225                    seq.call(c.get);
2226                }
2227            }
2228        }
2229        Some(Node::Call { func, args }) => {
2230            for a in &args {
2231                lower_expr(c, scope, seq, a)?;
2232            }
2233            let id = *c
2234                .fns
2235                .get(&func)
2236                .ok_or(LowerError::UnknownCallee(func.clone()))?;
2237            seq.call(id);
2238            if c.fallible.contains(&func) {
2239                // Callee returned a [tag,val] block. Propagate on failure
2240                // (the caller is fallible by the checker's coverage rule),
2241                // otherwise unwrap the value.
2242                let r = c.locals.borrow_mut().add(ValType::I64);
2243                seq.local_set(r);
2244                seq.local_get(r);
2245                seq.i64_const(0);
2246                seq.call(c.get); // tag
2247                seq.i64_const(0);
2248                seq.binop(BinaryOp::I64Ne); // tag != 0  (i32)
2249                seq.if_else(
2250                    InstrSeqType::Simple(None),
2251                    |t| {
2252                        t.local_get(r);
2253                        t.return_();
2254                    },
2255                    |_| {},
2256                );
2257                seq.local_get(r);
2258                seq.i64_const(8);
2259                seq.call(c.get); // ok value
2260            }
2261        }
2262        Some(Node::FuncRef(name)) => {
2263            // A function value is a 16-byte block `[code_idx, env]`. A
2264            // top-level function captures nothing, so env = 0; `code_idx`
2265            // selects its thunk in the funcref table.
2266            let idx = *c
2267                .fn_table_idx
2268                .get(&name)
2269                .ok_or(LowerError::UnknownCallee(name.clone()))?;
2270            let blk = c.locals.borrow_mut().add(ValType::I64);
2271            seq.i64_const(16);
2272            seq.call(c.alloc);
2273            seq.local_set(blk);
2274            seq.local_get(blk).i64_const(0).i64_const(idx).call(c.set);
2275            seq.drop();
2276            seq.local_get(blk).i64_const(8).i64_const(0).call(c.set);
2277            seq.drop();
2278            seq.local_get(blk);
2279        }
2280        Some(Node::Lambda { .. }) => {
2281            // The lift target and ordered captures were registered before
2282            // pass B. Read each capture from the *current* scope into a
2283            // fresh env record, then box `[code_idx, env]`.
2284            let (idx, captures) = c
2285                .lambdas
2286                .get(hash)
2287                .ok_or(LowerError::Unsupported("lambda not lifted"))?;
2288            let env = c.locals.borrow_mut().add(ValType::I64);
2289            if captures.is_empty() {
2290                seq.i64_const(0);
2291                seq.local_set(env);
2292            } else {
2293                seq.i64_const((8 * captures.len()) as i64);
2294                seq.call(c.alloc);
2295                seq.local_set(env);
2296                for (j, cap) in captures.iter().enumerate() {
2297                    seq.local_get(env);
2298                    seq.i64_const((j * 8) as i64);
2299                    match scope
2300                        .get(cap)
2301                        .ok_or(LowerError::UnresolvedRef(cap.clone()))?
2302                    {
2303                        Slot::Local(l) => {
2304                            seq.local_get(*l);
2305                        }
2306                        Slot::Member { base, off } => {
2307                            seq.local_get(*base);
2308                            seq.i64_const(*off);
2309                            seq.call(c.get);
2310                        }
2311                    }
2312                    seq.call(c.set);
2313                    seq.drop();
2314                }
2315            }
2316            let idx = *idx;
2317            let blk = c.locals.borrow_mut().add(ValType::I64);
2318            seq.i64_const(16);
2319            seq.call(c.alloc);
2320            seq.local_set(blk);
2321            seq.local_get(blk).i64_const(0).i64_const(idx).call(c.set);
2322            seq.drop();
2323            seq.local_get(blk).i64_const(8).local_get(env).call(c.set);
2324            seq.drop();
2325            seq.local_get(blk);
2326        }
2327        Some(Node::CallValue { callee, args }) => {
2328            // callee -> [code_idx, env] block; pass env as the trailing
2329            // arg, then the table selector (i32) on top.
2330            lower_expr(c, scope, seq, &callee)?;
2331            let p = c.locals.borrow_mut().add(ValType::I64);
2332            seq.local_set(p);
2333            for a in &args {
2334                lower_expr(c, scope, seq, a)?;
2335            }
2336            seq.local_get(p).i64_const(8).call(c.get); // env
2337            seq.local_get(p).i64_const(0).call(c.get); // code_idx
2338            seq.unop(UnaryOp::I32WrapI64);
2339            let ty = *c.indirect_types.get(&args.len()).ok_or(
2340                LowerError::Unsupported("call_indirect arity not prepared"),
2341            )?;
2342            seq.call_indirect(ty, c.func_table);
2343        }
2344        Some(Node::Step { value, .. }) => {
2345            lower_expr(c, scope, seq, &value)?;
2346        }
2347        Some(Node::BinOp { op, lhs, rhs }) => {
2348            use crate::node::BinOp::*;
2349            match op {
2350                // Short-circuit: the right operand is lowered inside a
2351                // branch so its effects only fire when reached. `And` is
2352                // `if lhs then rhs else false`; `Or` is `if lhs then true
2353                // else rhs` (Bool is 0/1 i64; `if` wants an i32 cond).
2354                And | Or => {
2355                    lower_expr(c, scope, seq, &lhs)?;
2356                    seq.unop(UnaryOp::I32WrapI64);
2357                    let is_and = matches!(op, And);
2358                    // Separate error cells per closure (see the `If` arm):
2359                    // `if_else` borrows both closures, so they cannot share
2360                    // one captured `Option`.
2361                    let mut terr: Option<LowerError> = None;
2362                    let mut eerr: Option<LowerError> = None;
2363                    seq.if_else(
2364                        InstrSeqType::Simple(Some(ValType::I64)),
2365                        |t| {
2366                            if is_and {
2367                                if let Err(e) = lower_expr(c, scope, t, &rhs) {
2368                                    terr = Some(e);
2369                                }
2370                            } else {
2371                                t.i64_const(1);
2372                            }
2373                        },
2374                        |e| {
2375                            if is_and {
2376                                e.i64_const(0);
2377                            } else if let Err(er) =
2378                                lower_expr(c, scope, e, &rhs)
2379                            {
2380                                eerr = Some(er);
2381                            }
2382                        },
2383                    );
2384                    if let Some(e) = terr.or(eerr) {
2385                        return Err(e);
2386                    }
2387                }
2388                _ => {
2389                    lower_expr(c, scope, seq, &lhs)?;
2390                    lower_expr(c, scope, seq, &rhs)?;
2391                    match op {
2392                        Add => seq.binop(BinaryOp::I64Add),
2393                        Sub => seq.binop(BinaryOp::I64Sub),
2394                        Mul => seq.binop(BinaryOp::I64Mul),
2395                        Div => seq.binop(BinaryOp::I64DivS),
2396                        Mod => seq.binop(BinaryOp::I64RemS),
2397                        // Comparisons produce an i32; widen to the uniform
2398                        // i64 ABI (Bool is 0/1 i64 in lowered code).
2399                        Eq => seq
2400                            .binop(BinaryOp::I64Eq)
2401                            .unop(UnaryOp::I64ExtendUI32),
2402                        Neq => seq
2403                            .binop(BinaryOp::I64Ne)
2404                            .unop(UnaryOp::I64ExtendUI32),
2405                        Lt => seq
2406                            .binop(BinaryOp::I64LtS)
2407                            .unop(UnaryOp::I64ExtendUI32),
2408                        Le => seq
2409                            .binop(BinaryOp::I64LeS)
2410                            .unop(UnaryOp::I64ExtendUI32),
2411                        Gt => seq
2412                            .binop(BinaryOp::I64GtS)
2413                            .unop(UnaryOp::I64ExtendUI32),
2414                        Ge => seq
2415                            .binop(BinaryOp::I64GeS)
2416                            .unop(UnaryOp::I64ExtendUI32),
2417                        And | Or => unreachable!("handled above"),
2418                    };
2419                }
2420            }
2421        }
2422        Some(Node::If {
2423            cond,
2424            then_branch,
2425            else_branch,
2426        }) => {
2427            // Bool is i64 0/1; wasm `if` consumes an i32 condition.
2428            lower_expr(c, scope, seq, &cond)?;
2429            seq.unop(UnaryOp::I32WrapI64);
2430            // The branch closures cannot return Result, so capture any lower
2431            // error and propagate after the block.
2432            let mut terr: Option<LowerError> = None;
2433            let mut eerr: Option<LowerError> = None;
2434            seq.if_else(
2435                InstrSeqType::Simple(Some(ValType::I64)),
2436                |t| {
2437                    if let Err(e) = lower_expr(c, scope, t, &then_branch) {
2438                        terr = Some(e);
2439                    }
2440                },
2441                |e| {
2442                    if let Err(er) = lower_expr(c, scope, e, &else_branch) {
2443                        eerr = Some(er);
2444                    }
2445                },
2446            );
2447            if let Some(e) = terr {
2448                return Err(e);
2449            }
2450            if let Some(e) = eerr {
2451                return Err(e);
2452            }
2453        }
2454        Some(Node::Record { type_name, fields }) => {
2455            let order = c
2456                .recs
2457                .get(&type_name)
2458                .ok_or(LowerError::Unsupported("unknown record type in lowering"))?
2459                .clone();
2460            // __alloc(n*8) -> ptr
2461            seq.i64_const((order.len() as i64) * 8);
2462            seq.call(c.alloc);
2463            // Per field, in declaration order: __set(ptr, off, val) -> ptr.
2464            for (i, fname) in order.iter().enumerate() {
2465                let fexpr = fields
2466                    .iter()
2467                    .find(|(n, _)| n == fname)
2468                    .map(|(_, h)| h.clone())
2469                    .ok_or(LowerError::Unsupported("missing record field in lowering"))?;
2470                seq.i64_const((i as i64) * 8);
2471                lower_expr(c, scope, seq, &fexpr)?;
2472                seq.call(c.set);
2473            }
2474            // [ptr] remains on the stack as the record value.
2475        }
2476        Some(Node::Field {
2477            base,
2478            type_name,
2479            field,
2480        }) => {
2481            let order = c
2482                .recs
2483                .get(&type_name)
2484                .ok_or(LowerError::Unsupported("unknown record type in lowering"))?;
2485            let idx = order
2486                .iter()
2487                .position(|n| n == &field)
2488                .ok_or(LowerError::Unsupported("unknown field in lowering"))?;
2489            lower_expr(c, scope, seq, &base)?; // [ptr]
2490            seq.i64_const((idx as i64) * 8); // [ptr, off]
2491            seq.call(c.get); // [val]
2492        }
2493        Some(Node::Fail(name)) => {
2494            // Build a [tag, 0] failure block and return it from the function
2495            // (short-circuit). Tags are module-wide so propagation is valid.
2496            let tag = *c
2497                .ftags
2498                .get(&name)
2499                .ok_or(LowerError::Unsupported("unknown failure in lowering"))?;
2500            seq.i64_const(16);
2501            seq.call(c.alloc); // [p]
2502            seq.i64_const(0);
2503            seq.i64_const(tag);
2504            seq.call(c.set); // __set(p,0,tag)
2505            seq.i64_const(8);
2506            seq.i64_const(0);
2507            seq.call(c.set); // __set(p,8,0)
2508            seq.return_(); // exits the (fallible) function with the block
2509        }
2510        Some(Node::Handle { body, handlers }) => {
2511            // v0.2 scope: the body must be a fallible call (the Section 5
2512            // `step x = call(...) on F -> ...` shape). Lower it raw — no
2513            // auto short-circuit — so we can inspect the tag and dispatch.
2514            let Some(Node::Call { func, args }) = c.store.get(&body)? else {
2515                return Err(LowerError::Unsupported(
2516                    "Handle body must be a fallible call (v0.2)",
2517                ));
2518            };
2519            if !c.fallible.contains(&func) {
2520                return Err(LowerError::Unsupported(
2521                    "Handle body must be a fallible call (v0.2)",
2522                ));
2523            }
2524            for a in &args {
2525                lower_expr(c, scope, seq, a)?;
2526            }
2527            let id = *c
2528                .fns
2529                .get(&func)
2530                .ok_or(LowerError::UnknownCallee(func.clone()))?;
2531            seq.call(id); // [block ptr]
2532
2533            let hb = c.locals.borrow_mut().add(ValType::I64);
2534            let tg = c.locals.borrow_mut().add(ValType::I64);
2535            let res = c.locals.borrow_mut().add(ValType::I64);
2536            let handled = c.locals.borrow_mut().add(ValType::I64);
2537            seq.local_set(hb);
2538            seq.local_get(hb);
2539            seq.i64_const(0);
2540            seq.call(c.get);
2541            seq.local_set(tg); // tag
2542            seq.i64_const(0);
2543            seq.local_set(handled);
2544
2545            // ok: tag == 0 -> res = payload
2546            seq.local_get(tg);
2547            seq.i64_const(0);
2548            seq.binop(BinaryOp::I64Eq);
2549            seq.if_else(
2550                InstrSeqType::Simple(None),
2551                |t| {
2552                    t.local_get(hb);
2553                    t.i64_const(8);
2554                    t.call(c.get);
2555                    t.local_set(res);
2556                    t.i64_const(1);
2557                    t.local_set(handled);
2558                },
2559                |_| {},
2560            );
2561
2562            // each handler: tag == ftag(F) -> res = recover
2563            for (fname, recover) in &handlers {
2564                let tag = *c
2565                    .ftags
2566                    .get(fname)
2567                    .ok_or(LowerError::Unsupported("unknown handled failure"))?;
2568                seq.local_get(tg);
2569                seq.i64_const(tag);
2570                seq.binop(BinaryOp::I64Eq);
2571                let mut herr: Option<LowerError> = None;
2572                seq.if_else(
2573                    InstrSeqType::Simple(None),
2574                    |t| match lower_expr(c, scope, t, recover) {
2575                        Ok(()) => {
2576                            t.local_set(res);
2577                            t.i64_const(1);
2578                            t.local_set(handled);
2579                        }
2580                        Err(e) => herr = Some(e),
2581                    },
2582                    |_| {},
2583                );
2584                if let Some(e) = herr {
2585                    return Err(e);
2586                }
2587            }
2588
2589            // unhandled failure -> propagate the original block
2590            seq.local_get(handled);
2591            seq.i64_const(0);
2592            seq.binop(BinaryOp::I64Eq);
2593            seq.if_else(
2594                InstrSeqType::Simple(None),
2595                |t| {
2596                    t.local_get(hb);
2597                    t.return_();
2598                },
2599                |_| {},
2600            );
2601
2602            seq.local_get(res);
2603        }
2604        Some(Node::Variant {
2605            type_name,
2606            case,
2607            fields,
2608        }) => {
2609            let cases = c
2610                .vars
2611                .get(&type_name)
2612                .ok_or(LowerError::Unsupported("unknown variant type in lowering"))?
2613                .clone();
2614            let k = cases
2615                .iter()
2616                .position(|(cn, _)| cn == &case)
2617                .ok_or(LowerError::Unsupported("unknown case in lowering"))?;
2618            let payload = cases[k].1.clone();
2619            // Layout: [tag, payload...]; 8 bytes each.
2620            seq.i64_const(((1 + payload.len()) as i64) * 8);
2621            seq.call(c.alloc); // [ptr]
2622            seq.i64_const(0);
2623            seq.i64_const(k as i64);
2624            seq.call(c.set); // __set(ptr,0,tag) -> [ptr]
2625            for (i, fname) in payload.iter().enumerate() {
2626                let fe = fields
2627                    .iter()
2628                    .find(|(n, _)| n == fname)
2629                    .map(|(_, h)| h.clone())
2630                    .ok_or(LowerError::Unsupported("missing variant field in lowering"))?;
2631                seq.i64_const(((1 + i) as i64) * 8);
2632                lower_expr(c, scope, seq, &fe)?;
2633                seq.call(c.set); // -> [ptr]
2634            }
2635            // [ptr] is the variant value.
2636        }
2637        Some(Node::Match {
2638            scrutinee,
2639            type_name,
2640            arms,
2641        }) => {
2642            let cases = c
2643                .vars
2644                .get(&type_name)
2645                .ok_or(LowerError::Unsupported("unknown variant type in lowering"))?
2646                .clone();
2647            lower_expr(c, scope, seq, &scrutinee)?; // [ptr]
2648            let sc = c.locals.borrow_mut().add(ValType::I64);
2649            seq.local_set(sc);
2650            // tag = __get(sc, 0)
2651            seq.local_get(sc);
2652            seq.i64_const(0);
2653            seq.call(c.get);
2654            let tg = c.locals.borrow_mut().add(ValType::I64);
2655            seq.local_set(tg);
2656            let res = c.locals.borrow_mut().add(ValType::I64);
2657            for arm in &arms {
2658                let k = cases
2659                    .iter()
2660                    .position(|(cn, _)| cn == &arm.case)
2661                    .ok_or(LowerError::Unsupported("unknown case in match lowering"))?;
2662                let mut s2 = scope.clone();
2663                for (i, b) in arm.bindings.iter().enumerate() {
2664                    s2.insert(
2665                        b.clone(),
2666                        Slot::Member {
2667                            base: sc,
2668                            off: ((1 + i) as i64) * 8,
2669                        },
2670                    );
2671                }
2672                // if tag == k { res = body }   (exactly one arm fires)
2673                seq.local_get(tg);
2674                seq.i64_const(k as i64);
2675                seq.binop(BinaryOp::I64Eq); // i32 condition
2676                let mut berr: Option<LowerError> = None;
2677                seq.if_else(
2678                    InstrSeqType::Simple(None),
2679                    |t| match lower_expr(c, &s2, t, &arm.body) {
2680                        Ok(()) => {
2681                            t.local_set(res);
2682                        }
2683                        Err(e) => berr = Some(e),
2684                    },
2685                    |_| {},
2686                );
2687                if let Some(e) = berr {
2688                    return Err(e);
2689                }
2690            }
2691            seq.local_get(res);
2692        }
2693        Some(Node::Str(s)) => {
2694            // Layout: [len, byte0, byte1, ...], one i64 slot each (uniform
2695            // with the rest of the memory model; not space-optimized).
2696            let bytes = s.as_bytes();
2697            seq.i64_const(((1 + bytes.len()) as i64) * 8);
2698            seq.call(c.alloc); // [ptr]
2699            seq.i64_const(0);
2700            seq.i64_const(bytes.len() as i64);
2701            seq.call(c.set); // __set(ptr,0,len)
2702            for (i, b) in bytes.iter().enumerate() {
2703                seq.i64_const(((1 + i) as i64) * 8);
2704                seq.i64_const(*b as i64);
2705                seq.call(c.set);
2706            }
2707            // [ptr] is the string value.
2708        }
2709        Some(Node::StrLen(arg)) => {
2710            lower_expr(c, scope, seq, &arg)?; // [ptr]
2711            seq.i64_const(0);
2712            seq.call(c.get); // [len]
2713        }
2714        Some(Node::StrLower(arg)) => {
2715            lower_expr(c, scope, seq, &arg)?; // [ptr]
2716            seq.call(c.str_lower); // [ptr'] (fresh ASCII-lowercased)
2717        }
2718        Some(Node::StrFromCode(arg)) => {
2719            lower_expr(c, scope, seq, &arg)?; // [code]
2720            seq.call(c.str_from_code); // [ptr] (1-byte string)
2721        }
2722        Some(Node::StrConcat(a, b)) => {
2723            lower_expr(c, scope, seq, &a)?;
2724            lower_expr(c, scope, seq, &b)?;
2725            seq.call(c.str_concat);
2726        }
2727        Some(Node::StrSlice { s, start, len }) => {
2728            lower_expr(c, scope, seq, &s)?;
2729            lower_expr(c, scope, seq, &start)?;
2730            lower_expr(c, scope, seq, &len)?;
2731            seq.call(c.str_slice);
2732        }
2733        Some(Node::StrEq(a, b)) => {
2734            lower_expr(c, scope, seq, &a)?;
2735            lower_expr(c, scope, seq, &b)?;
2736            seq.call(c.str_eq); // 0/1 (Bool)
2737        }
2738        Some(Node::StrContains { haystack, needle }) => {
2739            lower_expr(c, scope, seq, &haystack)?;
2740            lower_expr(c, scope, seq, &needle)?;
2741            seq.call(c.str_contains); // 0/1 (Bool)
2742        }
2743        Some(Node::StrStartsWith { s, prefix }) => {
2744            lower_expr(c, scope, seq, &s)?;
2745            lower_expr(c, scope, seq, &prefix)?;
2746            seq.call(c.str_starts_with); // 0/1 (Bool)
2747        }
2748        Some(Node::StrIndexOf { haystack, needle }) => {
2749            lower_expr(c, scope, seq, &haystack)?;
2750            lower_expr(c, scope, seq, &needle)?;
2751            seq.call(c.str_index_of); // first index, or -1 (Number)
2752        }
2753        Some(Node::NumberToStr(a)) => {
2754            lower_expr(c, scope, seq, &a)?;
2755            seq.call(c.num_to_str); // i64 -> string ptr
2756        }
2757        Some(Node::StrToNumberOpt(a)) => {
2758            lower_expr(c, scope, seq, &a)?;
2759            seq.call(c.str_to_num_opt); // -> Option<Number> block
2760        }
2761        Some(Node::StrToNumber(a)) => {
2762            lower_expr(c, scope, seq, &a)?;
2763            seq.call(c.str_to_num); // string ptr -> i64
2764        }
2765        Some(Node::Now) => {
2766            seq.call(c.now); // host::now() -> i64
2767        }
2768        Some(Node::List(elems)) => {
2769            // Layout: [len, e0, e1, ...], one i64 slot each.
2770            seq.i64_const(((1 + elems.len()) as i64) * 8);
2771            seq.call(c.alloc); // [ptr]
2772            seq.i64_const(0);
2773            seq.i64_const(elems.len() as i64);
2774            seq.call(c.set); // __set(ptr,0,len)
2775            for (i, e) in elems.iter().enumerate() {
2776                seq.i64_const(((1 + i) as i64) * 8);
2777                lower_expr(c, scope, seq, e)?;
2778                seq.call(c.set);
2779            }
2780            // [ptr] is the list value.
2781        }
2782        Some(Node::ListEmpty { .. }) => {
2783            // [len=0]; the element type is erased at the wasm level.
2784            seq.i64_const(8);
2785            seq.call(c.alloc); // [ptr]
2786            seq.i64_const(0);
2787            seq.i64_const(0);
2788            seq.call(c.set); // __set(ptr,0,0) -> [ptr]
2789        }
2790        Some(Node::ListCons { head, tail }) => {
2791            lower_expr(c, scope, seq, &head)?;
2792            lower_expr(c, scope, seq, &tail)?;
2793            seq.call(c.list_cons); // (head, tail) -> new list ptr
2794        }
2795        Some(Node::OptionSome(v)) => {
2796            // [tag=1, val] block.
2797            lower_expr(c, scope, seq, &v)?;
2798            let tmp = c.locals.borrow_mut().add(ValType::I64);
2799            seq.local_set(tmp);
2800            seq.i64_const(16);
2801            seq.call(c.alloc); // [p]
2802            seq.i64_const(0);
2803            seq.i64_const(1);
2804            seq.call(c.set); // tag = 1 (Some) -> [p]
2805            seq.i64_const(8);
2806            seq.local_get(tmp);
2807            seq.call(c.set); // val -> [p]
2808        }
2809        Some(Node::OptionNone { .. }) => {
2810            // [tag=0, _] block.
2811            seq.i64_const(16);
2812            seq.call(c.alloc); // [p]
2813            seq.i64_const(0);
2814            seq.i64_const(0);
2815            seq.call(c.set); // tag = 0 (None) -> [p]
2816        }
2817        Some(Node::OptionElse { opt, default }) => {
2818            lower_expr(c, scope, seq, &opt)?; // [p]
2819            let pl = c.locals.borrow_mut().add(ValType::I64);
2820            seq.local_set(pl);
2821            seq.local_get(pl);
2822            seq.i64_const(0);
2823            seq.call(c.get); // [tag]
2824            seq.i64_const(1);
2825            seq.binop(BinaryOp::I64Eq); // [i32: is Some]
2826            let mut eerr: Option<LowerError> = None;
2827            seq.if_else(
2828                InstrSeqType::Simple(Some(ValType::I64)),
2829                |t| {
2830                    t.local_get(pl).i64_const(8).call(c.get); // the value
2831                },
2832                |e| {
2833                    if let Err(er) = lower_expr(c, scope, e, &default) {
2834                        eerr = Some(er);
2835                    }
2836                },
2837            );
2838            if let Some(e) = eerr {
2839                return Err(e);
2840            }
2841        }
2842        Some(Node::OptionMatch {
2843            opt,
2844            some_bind,
2845            some_body,
2846            none_body,
2847        }) => {
2848            lower_expr(c, scope, seq, &opt)?; // [p]
2849            let pl = c.locals.borrow_mut().add(ValType::I64);
2850            seq.local_set(pl);
2851            seq.local_get(pl);
2852            seq.i64_const(0);
2853            seq.call(c.get); // [tag]
2854            seq.i64_const(1);
2855            seq.binop(BinaryOp::I64Eq); // [i32: is Some]
2856            // The payload binding loads from pl+8 (the value slot), the
2857            // same Member machinery `match` arms use.
2858            let mut s2 = scope.clone();
2859            s2.insert(some_bind.clone(), Slot::Member { base: pl, off: 8 });
2860            let mut terr: Option<LowerError> = None;
2861            let mut eerr: Option<LowerError> = None;
2862            seq.if_else(
2863                InstrSeqType::Simple(Some(ValType::I64)),
2864                |t| {
2865                    if let Err(e) = lower_expr(c, &s2, t, &some_body) {
2866                        terr = Some(e);
2867                    }
2868                },
2869                |e| {
2870                    if let Err(er) = lower_expr(c, scope, e, &none_body) {
2871                        eerr = Some(er);
2872                    }
2873                },
2874            );
2875            if let Some(e) = terr.or(eerr) {
2876                return Err(e);
2877            }
2878        }
2879        Some(Node::ListTryGet { list, index }) => {
2880            lower_expr(c, scope, seq, &list)?;
2881            lower_expr(c, scope, seq, &index)?;
2882            seq.call(c.list_try_get); // -> Option block
2883        }
2884        Some(Node::ListLen(arg)) => {
2885            lower_expr(c, scope, seq, &arg)?; // [ptr]
2886            seq.i64_const(0);
2887            seq.call(c.get); // [len]
2888        }
2889        Some(Node::ListGet { list, index }) => {
2890            lower_expr(c, scope, seq, &list)?; // [ptr]
2891            lower_expr(c, scope, seq, &index)?; // [ptr, idx]
2892            seq.call(c.list_get); // bounds-checked: [elem] or trap
2893        }
2894        Some(Node::Map(pairs)) => {
2895            // Layout: [count, k0, v0, k1, v1, ...].
2896            seq.i64_const(((1 + 2 * pairs.len()) as i64) * 8);
2897            seq.call(c.alloc); // [ptr]
2898            seq.i64_const(0);
2899            seq.i64_const(pairs.len() as i64);
2900            seq.call(c.set); // count
2901            for (i, (k, v)) in pairs.iter().enumerate() {
2902                seq.i64_const(((1 + 2 * i) as i64) * 8);
2903                lower_expr(c, scope, seq, k)?;
2904                seq.call(c.set);
2905                seq.i64_const(((2 + 2 * i) as i64) * 8);
2906                lower_expr(c, scope, seq, v)?;
2907                seq.call(c.set);
2908            }
2909            // [ptr] is the map value.
2910        }
2911        Some(Node::MapGet { map, key }) => {
2912            lower_expr(c, scope, seq, &map)?; // [ptr]
2913            lower_expr(c, scope, seq, &key)?; // [ptr, key]
2914            seq.call(c.map_get); // [val] (0 if absent)
2915        }
2916        Some(Node::MapTryGet { map, key }) => {
2917            lower_expr(c, scope, seq, &map)?; // [ptr]
2918            lower_expr(c, scope, seq, &key)?; // [ptr, key]
2919            seq.call(c.map_try_get); // [Option block]
2920        }
2921        Some(Node::MapLen(arg)) => {
2922            lower_expr(c, scope, seq, &arg)?; // [ptr]
2923            seq.i64_const(0);
2924            seq.call(c.get); // [count]
2925        }
2926        Some(Node::Log(arg)) => {
2927            lower_expr(c, scope, seq, &arg)?; // [v]
2928            seq.call(c.log); // host::log echoes v -> [v]
2929        }
2930        Some(Node::Publish(arg)) => {
2931            lower_expr(c, scope, seq, &arg)?; // [topic_ptr]
2932            seq.call(c.publish); // host::publish records topic -> [0]
2933        }
2934        Some(Node::SetHeader { name, value }) => {
2935            lower_expr(c, scope, seq, &name)?; // [name_ptr]
2936            lower_expr(c, scope, seq, &value)?; // [name_ptr, value_ptr]
2937            seq.call(c.set_header); // host::set_header buffers -> [0]
2938        }
2939        Some(Node::Rand) => {
2940            seq.call(c.rand); // host::rand() -> i64
2941        }
2942        Some(Node::DiskWrite { path, content }) => {
2943            lower_expr(c, scope, seq, &path)?;
2944            lower_expr(c, scope, seq, &content)?;
2945            seq.call(c.disk_write); // -> bytes written
2946        }
2947        Some(Node::DiskRead(path)) => {
2948            lower_expr(c, scope, seq, &path)?;
2949            seq.call(c.disk_read); // -> file byte length
2950        }
2951        Some(Node::NetGet(url)) => {
2952            lower_expr(c, scope, seq, &url)?;
2953            seq.call(c.net_get); // -> HTTP status (real ureq+TLS client; -1 on transport error)
2954        }
2955        Some(Node::DbQuery { sql, params }) => {
2956            lower_expr(c, scope, seq, &sql)?;
2957            lower_expr(c, scope, seq, &params)?;
2958            seq.call(c.db_query); // (sql_ptr, params_ptr) -> result String ptr
2959        }
2960        Some(Node::MutNew(v)) => {
2961            // A cell is a 1-slot block [value].
2962            seq.i64_const(8);
2963            seq.call(c.alloc); // [ptr]
2964            seq.i64_const(0);
2965            lower_expr(c, scope, seq, &v)?;
2966            seq.call(c.set); // __set(ptr,0,v) -> [ptr]
2967        }
2968        Some(Node::MutGet(cell)) => {
2969            lower_expr(c, scope, seq, &cell)?; // [ptr]
2970            seq.i64_const(0);
2971            seq.call(c.get); // [value]
2972        }
2973        Some(Node::MutSet { cell, value }) => {
2974            lower_expr(c, scope, seq, &cell)?; // [ptr]
2975            seq.i64_const(0);
2976            lower_expr(c, scope, seq, &value)?; // [ptr, 0, v]
2977            seq.call(c.set); // -> [ptr]
2978            // pass-through: re-read the just-written value
2979            seq.i64_const(0);
2980            seq.call(c.get); // [v]
2981        }
2982        Some(Node::RecordDef { .. }) | Some(Node::VariantDef { .. }) => {
2983            return Err(LowerError::Unsupported("type definition is not an expression"));
2984        }
2985        Some(Node::Hole { .. }) => return Err(LowerError::Hole),
2986        Some(Node::Function { .. }) | Some(Node::Module { .. }) => {
2987            return Err(LowerError::Unsupported("nested function or module"));
2988        }
2989        None => return Err(LowerError::Unsupported("missing node")),
2990    }
2991    Ok(())
2992}
2993
2994/// Why running a lowered module failed.
2995#[derive(Debug)]
2996pub enum RunError {
2997    Wasmtime(String),
2998    /// The export was absent or not an `i64(...)->i64` function.
2999    BadExport(String),
3000}
3001
3002impl std::fmt::Display for RunError {
3003    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3004        match self {
3005            RunError::Wasmtime(e) => write!(f, "wasmtime: {e}"),
3006            RunError::BadExport(n) => write!(f, "no callable i64 export `{n}`"),
3007        }
3008    }
3009}
3010
3011impl std::error::Error for RunError {}
3012
3013type WtStore = wasmtime::Store<()>;
3014
3015/// Build a Cairn string in wasm memory from the host side: call the
3016/// exported `__alloc` to reserve `[len, b0, ...]`, write it, return the
3017/// pointer (-1 on failure). This is the host→wasm allocation keystone that
3018/// lets effects return `String`/structured results.
3019fn write_wasm_string(caller: &mut wasmtime::Caller<'_, ()>, s: &str) -> i64 {
3020    let bytes = s.as_bytes();
3021    let size = ((1 + bytes.len()) as i64) * 8;
3022    let Some(alloc) = caller
3023        .get_export("__alloc")
3024        .and_then(wasmtime::Extern::into_func)
3025    else {
3026        return -1;
3027    };
3028    let mut out = [wasmtime::Val::I64(0)];
3029    if alloc
3030        .call(&mut *caller, &[wasmtime::Val::I64(size)], &mut out)
3031        .is_err()
3032    {
3033        return -1;
3034    }
3035    let wasmtime::Val::I64(ptr) = out[0] else {
3036        return -1;
3037    };
3038    let ptr = ptr as usize;
3039    let Some(mem) = caller
3040        .get_export("mem")
3041        .and_then(wasmtime::Extern::into_memory)
3042    else {
3043        return -1;
3044    };
3045    let data = mem.data_mut(&mut *caller);
3046    let put = |data: &mut [u8], off: usize, v: i64| {
3047        data[off..off + 8].copy_from_slice(&v.to_le_bytes());
3048    };
3049    put(data, ptr, bytes.len() as i64);
3050    for (i, b) in bytes.iter().enumerate() {
3051        put(data, ptr + (1 + i) * 8, *b as i64);
3052    }
3053    ptr as i64
3054}
3055
3056/// Decode a Cairn string out of wasm linear memory: `[len, b0, b1, ...]`,
3057/// one i64 slot each (the low byte of each slot is the character).
3058fn read_wasm_string(data: &[u8], ptr: usize) -> String {
3059    let rd = |off: usize| -> i64 {
3060        let mut b = [0u8; 8];
3061        b.copy_from_slice(&data[off..off + 8]);
3062        i64::from_le_bytes(b)
3063    };
3064    let len = rd(ptr).max(0) as usize;
3065    let mut bytes = Vec::with_capacity(len);
3066    for i in 0..len {
3067        bytes.push((rd(ptr + (1 + i) * 8) & 0xff) as u8);
3068    }
3069    String::from_utf8_lossy(&bytes).into_owned()
3070}
3071
3072// The observable seam for the `Live` effect: `host::publish` appends
3073// the topic here. v1 is a *thread-local* queue — a wasm instance runs
3074// on the calling thread, so the in-process live hub (design.md §10,
3075// slice 4) drains, on that same thread, exactly the topics that
3076// request published (and tests don't contaminate each other across
3077// cargo's threads). A real multi-tenant runtime replaces this with a
3078// per-server bus; the effect and primitive (the language surface) do
3079// not change.
3080thread_local! {
3081    static PUBLISHED: std::cell::RefCell<Vec<String>> =
3082        const { std::cell::RefCell::new(Vec::new()) };
3083}
3084
3085/// Take and clear the topics published on this thread since the last
3086/// drain (call it right after the request that may have published).
3087pub fn drain_published() -> Vec<String> {
3088    PUBLISHED.with(|p| std::mem::take(&mut *p.borrow_mut()))
3089}
3090
3091fn record_published(topic: String) {
3092    PUBLISHED.with(|p| p.borrow_mut().push(topic));
3093}
3094
3095// The observable seam for the `Resp` effect — the exact twin of
3096// PUBLISHED above. `host::set_header` appends `(name, value)`; the
3097// HTTP server drains it right after the handler and writes each as a
3098// real response header line. Thread-local for the same reason: the
3099// wasm instance runs on the calling thread, so the server drains
3100// exactly this request's headers, and cargo's parallel tests stay
3101// isolated. A multi-tenant runtime swaps the seam, not the language.
3102thread_local! {
3103    static RESP_HEADERS: std::cell::RefCell<Vec<(String, String)>> =
3104        const { std::cell::RefCell::new(Vec::new()) };
3105}
3106
3107/// Take and clear the response headers set on this thread since the
3108/// last drain (call it right after the request that may have set one).
3109pub fn drain_resp_headers() -> Vec<(String, String)> {
3110    RESP_HEADERS.with(|h| std::mem::take(&mut *h.borrow_mut()))
3111}
3112
3113fn record_resp_header(name: String, value: String) {
3114    RESP_HEADERS.with(|h| h.borrow_mut().push((name, value)));
3115}
3116
3117/// Decode a Cairn `List<String>` out of wasm memory: `[len, s0, s1, ...]`,
3118/// each slot a pointer to a string. Used to read `db_query` bind params.
3119fn read_wasm_str_list(data: &[u8], ptr: usize) -> Vec<String> {
3120    let rd = |off: usize| -> i64 {
3121        let mut b = [0u8; 8];
3122        b.copy_from_slice(&data[off..off + 8]);
3123        i64::from_le_bytes(b)
3124    };
3125    let len = rd(ptr).max(0) as usize;
3126    (0..len)
3127        .map(|i| read_wasm_string(data, rd(ptr + (1 + i) * 8) as usize))
3128        .collect()
3129}
3130
3131/// Instantiate `wasm` and wire the real host effects. `now`: `None` reads
3132/// the real wall clock (ms since the epoch); `Some(v)` overrides it, the
3133/// one deterministic-test affordance (not a product default). `db`:
3134/// `None` uses a fresh real in-memory SQLite, `Some(path)` a real SQLite
3135/// file. `Rand`/`Net`/`Log`/`Disk` are always real. No fakes.
3136/// Execute `sql` against the SQLite database at `db_path` (opened fresh
3137/// each call — durability lives in the file, which outlives the
3138/// per-request wasm instance). A statement with result columns is
3139/// serialized as `\n`-separated rows of `\t`-separated columns; each
3140/// cell escapes `\\`, `\t`, `\n`, so a value containing the delimiters
3141/// round-trips intact (the framework's `field` calls `unescape` to
3142/// reverse it). Otherwise the `last_insert_rowid` is returned as text.
3143/// On any error, logs to stderr and returns the empty string (the
3144/// parser then yields an empty list).
3145fn run_sql(conn: &rusqlite::Connection, sql: &str, params: &[String]) -> String {
3146    use rusqlite::types::Value;
3147    // Escape the escape char and the two delimiters so a cell value
3148    // containing a tab or newline can never break the `\t`/`\n` framing.
3149    // The framework's `field` reverses this (web::functions `unescape`).
3150    fn esc(s: &str) -> String {
3151        let mut o = String::with_capacity(s.len());
3152        for ch in s.chars() {
3153            match ch {
3154                '\\' => o.push_str("\\\\"),
3155                '\t' => o.push_str("\\t"),
3156                '\n' => o.push_str("\\n"),
3157                c => o.push(c),
3158            }
3159        }
3160        o
3161    }
3162    fn cell(v: Value) -> String {
3163        match v {
3164            Value::Null => String::new(),
3165            Value::Integer(i) => i.to_string(),
3166            Value::Real(f) => f.to_string(),
3167            Value::Text(s) => esc(&s),
3168            Value::Blob(b) => esc(&String::from_utf8_lossy(&b)),
3169        }
3170    }
3171    let go = || -> rusqlite::Result<String> {
3172        let bound = rusqlite::params_from_iter(params.iter());
3173        let mut stmt = conn.prepare(sql)?;
3174        let ncol = stmt.column_count();
3175        if ncol == 0 {
3176            drop(stmt);
3177            conn.execute(sql, bound)?;
3178            return Ok(conn.last_insert_rowid().to_string());
3179        }
3180        let mut rows = stmt.query(bound)?;
3181        let mut out: Vec<String> = Vec::new();
3182        while let Some(row) = rows.next()? {
3183            let mut cols: Vec<String> = Vec::with_capacity(ncol);
3184            for c in 0..ncol {
3185                cols.push(cell(row.get::<_, Value>(c)?));
3186            }
3187            out.push(cols.join("\t"));
3188        }
3189        Ok(out.join("\n"))
3190    };
3191    match go() {
3192        Ok(s) => s,
3193        Err(e) => {
3194            eprintln!("cairn db_query error: {e}");
3195            String::new()
3196        }
3197    }
3198}
3199
3200fn instantiate(
3201    wasm: &[u8],
3202    now: Option<i64>,
3203    db: Option<&str>,
3204) -> std::result::Result<(WtStore, wasmtime::Instance), RunError> {
3205    use wasmtime::{Engine, Linker, Module as WtModule};
3206
3207    let engine = Engine::default();
3208    let module = WtModule::new(&engine, wasm).map_err(|e| RunError::Wasmtime(e.to_string()))?;
3209    let mut store = wasmtime::Store::new(&engine, ());
3210    let mut linker = Linker::new(&engine);
3211    // host::now: the real wall clock (ms since the epoch), unless a test
3212    // overrides it for determinism.
3213    linker
3214        .func_wrap("host", "now", move || -> i64 {
3215            match now {
3216                Some(v) => v,
3217                None => std::time::SystemTime::now()
3218                    .duration_since(std::time::UNIX_EPOCH)
3219                    .map(|d| d.as_millis() as i64)
3220                    .unwrap_or(0),
3221            }
3222        })
3223        .map_err(|e| RunError::Wasmtime(e.to_string()))?;
3224    // host::log: actually observe the value (stderr), then pass it
3225    // through so `log(x)` is transparent.
3226    linker
3227        .func_wrap("host", "log", |v: i64| -> i64 {
3228            eprintln!("[cairn log] {v}");
3229            v
3230        })
3231        .map_err(|e| RunError::Wasmtime(e.to_string()))?;
3232    // host::publish (the Live effect): read the topic string and record
3233    // it on the publish queue for the live runtime; yields 0.
3234    linker
3235        .func_wrap(
3236            "host",
3237            "publish",
3238            |mut caller: wasmtime::Caller<'_, ()>, topic_ptr: i64| -> i64 {
3239                let topic = {
3240                    match caller
3241                        .get_export("mem")
3242                        .and_then(wasmtime::Extern::into_memory)
3243                    {
3244                        Some(mem) => read_wasm_string(
3245                            mem.data(&caller),
3246                            topic_ptr as usize,
3247                        ),
3248                        None => return 0,
3249                    }
3250                };
3251                record_published(topic);
3252                0
3253            },
3254        )
3255        .map_err(|e| RunError::Wasmtime(e.to_string()))?;
3256    // host::set_header (the Resp effect): read the name+value strings
3257    // and buffer the header line for the server to emit; yields 0.
3258    linker
3259        .func_wrap(
3260            "host",
3261            "set_header",
3262            |mut caller: wasmtime::Caller<'_, ()>,
3263             name_ptr: i64,
3264             value_ptr: i64|
3265             -> i64 {
3266                let (name, value) = {
3267                    match caller
3268                        .get_export("mem")
3269                        .and_then(wasmtime::Extern::into_memory)
3270                    {
3271                        Some(mem) => {
3272                            let data = mem.data(&caller);
3273                            (
3274                                read_wasm_string(data, name_ptr as usize),
3275                                read_wasm_string(
3276                                    data,
3277                                    value_ptr as usize,
3278                                ),
3279                            )
3280                        }
3281                        None => return 0,
3282                    }
3283                };
3284                record_resp_header(name, value);
3285                0
3286            },
3287        )
3288        .map_err(|e| RunError::Wasmtime(e.to_string()))?;
3289    // host::rand: real OS entropy.
3290    linker
3291        .func_wrap("host", "rand", || -> i64 {
3292            let mut b = [0u8; 8];
3293            getrandom::getrandom(&mut b).expect("OS entropy");
3294            i64::from_le_bytes(b)
3295        })
3296        .map_err(|e| RunError::Wasmtime(e.to_string()))?;
3297    // Disk: read the String arguments out of wasm memory and do real fs.
3298    linker
3299        .func_wrap(
3300            "host",
3301            "disk_write",
3302            |mut caller: wasmtime::Caller<'_, ()>, p: i64, cptr: i64| -> i64 {
3303                let Some(mem) = caller
3304                    .get_export("mem")
3305                    .and_then(wasmtime::Extern::into_memory)
3306                else {
3307                    return -1;
3308                };
3309                let data = mem.data(&caller);
3310                let path = read_wasm_string(data, p as usize);
3311                let content = read_wasm_string(data, cptr as usize);
3312                match std::fs::write(&path, content.as_bytes()) {
3313                    Ok(()) => content.len() as i64,
3314                    Err(_) => -1,
3315                }
3316            },
3317        )
3318        .map_err(|e| RunError::Wasmtime(e.to_string()))?;
3319    linker
3320        .func_wrap(
3321            "host",
3322            "disk_read",
3323            |mut caller: wasmtime::Caller<'_, ()>, p: i64| -> i64 {
3324                // Decode the path (immutable borrow), then return the file's
3325                // contents as a Cairn String via host→wasm allocation.
3326                let path = {
3327                    let Some(mem) = caller
3328                        .get_export("mem")
3329                        .and_then(wasmtime::Extern::into_memory)
3330                    else {
3331                        return -1;
3332                    };
3333                    read_wasm_string(mem.data(&caller), p as usize)
3334                };
3335                let contents = std::fs::read_to_string(&path).unwrap_or_default();
3336                write_wasm_string(&mut caller, &contents)
3337            },
3338        )
3339        .map_err(|e| RunError::Wasmtime(e.to_string()))?;
3340    // Net: a real blocking HTTP(S) GET; returns the response status code
3341    // (transport failure -> -1).
3342    linker
3343        .func_wrap(
3344            "host",
3345            "net_get",
3346            |mut caller: wasmtime::Caller<'_, ()>, u: i64| -> i64 {
3347                let url = {
3348                    let Some(mem) = caller
3349                        .get_export("mem")
3350                        .and_then(wasmtime::Extern::into_memory)
3351                    else {
3352                        return -1;
3353                    };
3354                    read_wasm_string(mem.data(&caller), u as usize)
3355                };
3356                match ureq::get(&url).call() {
3357                    Ok(resp) => resp.status() as i64,
3358                    Err(ureq::Error::Status(code, _)) => code as i64,
3359                    Err(_) => -1,
3360                }
3361            },
3362        )
3363        .map_err(|e| RunError::Wasmtime(e.to_string()))?;
3364    // Db: a real SQLite connection, created once and held for the
3365    // instance's lifetime so multiple `db_query` calls in one request
3366    // share state. `Some(path)` is a real file (persists across
3367    // requests); `None` is a real in-memory database (ephemeral, per
3368    // instance). No canned path — every query hits the engine.
3369    let conn = match db {
3370        Some(p) => rusqlite::Connection::open(p),
3371        None => rusqlite::Connection::open_in_memory(),
3372    }
3373    .map_err(|e| RunError::Wasmtime(format!("sqlite open: {e}")))?;
3374    let conn = std::sync::Mutex::new(conn);
3375    linker
3376        .func_wrap(
3377            "host",
3378            "db_query",
3379            move |mut caller: wasmtime::Caller<'_, ()>, q: i64, pp: i64| -> i64 {
3380                let (sql, params) = {
3381                    let Some(mem) = caller
3382                        .get_export("mem")
3383                        .and_then(wasmtime::Extern::into_memory)
3384                    else {
3385                        return -1;
3386                    };
3387                    let data = mem.data(&caller);
3388                    (
3389                        read_wasm_string(data, q as usize),
3390                        read_wasm_str_list(data, pp as usize),
3391                    )
3392                };
3393                let result = {
3394                    let conn = conn.lock().expect("db mutex");
3395                    run_sql(&conn, &sql, &params)
3396                };
3397                write_wasm_string(&mut caller, &result)
3398            },
3399        )
3400        .map_err(|e| RunError::Wasmtime(e.to_string()))?;
3401    let instance = linker
3402        .instantiate(&mut store, &module)
3403        .map_err(|e| RunError::Wasmtime(e.to_string()))?;
3404    Ok((store, instance))
3405}
3406
3407/// Call `func` with `args`, returning its single `i64` result.
3408pub fn run_i64(wasm: &[u8], func: &str, args: &[i64]) -> std::result::Result<i64, RunError> {
3409    run_with(wasm, func, args, None)
3410}
3411
3412/// Like [`run_i64`], but overrides the `Time` effect (`host::now`) to
3413/// return `now_value`, so effectful programs are deterministically
3414/// observable in tests. The only injection point — everything else is a
3415/// real host effect.
3416pub fn run_effectful_i64(
3417    wasm: &[u8],
3418    func: &str,
3419    args: &[i64],
3420    now_value: i64,
3421) -> std::result::Result<i64, RunError> {
3422    run_with(wasm, func, args, Some(now_value))
3423}
3424
3425fn run_with(
3426    wasm: &[u8],
3427    func: &str,
3428    args: &[i64],
3429    now: Option<i64>,
3430) -> std::result::Result<i64, RunError> {
3431    use wasmtime::Val;
3432    let (mut store, instance) = instantiate(wasm, now, None)?;
3433    let f = instance
3434        .get_func(&mut store, func)
3435        .ok_or_else(|| RunError::BadExport(func.to_string()))?;
3436    let params: Vec<Val> = args.iter().map(|a| Val::I64(*a)).collect();
3437    let mut results = [Val::I64(0)];
3438    f.call(&mut store, &params, &mut results)
3439        .map_err(|e| RunError::Wasmtime(e.to_string()))?;
3440    match results[0] {
3441        Val::I64(v) => Ok(v),
3442        _ => Err(RunError::BadExport(func.to_string())),
3443    }
3444}
3445
3446/// Run a *fallible* function: it returns a `[tag,val]` block pointer.
3447/// `Ok(val)` if `tag == 0`, else `Err(tag)` (the module-wide failure tag).
3448pub fn run_fallible(
3449    wasm: &[u8],
3450    func: &str,
3451    args: &[i64],
3452) -> std::result::Result<std::result::Result<i64, i64>, RunError> {
3453    use wasmtime::Val;
3454    let (mut store, instance) = instantiate(wasm, None, None)?;
3455    let f = instance
3456        .get_func(&mut store, func)
3457        .ok_or_else(|| RunError::BadExport(func.to_string()))?;
3458    let memory = instance
3459        .get_memory(&mut store, "mem")
3460        .ok_or_else(|| RunError::BadExport("mem".to_string()))?;
3461
3462    let params: Vec<Val> = args.iter().map(|a| Val::I64(*a)).collect();
3463    let mut results = [Val::I64(0)];
3464    f.call(&mut store, &params, &mut results)
3465        .map_err(|e| RunError::Wasmtime(e.to_string()))?;
3466
3467    let ptr = match results[0] {
3468        Val::I64(p) => p as usize,
3469        _ => return Err(RunError::BadExport(func.to_string())),
3470    };
3471    let data = memory.data(&store);
3472    let read = |off: usize| -> i64 {
3473        let mut b = [0u8; 8];
3474        b.copy_from_slice(&data[ptr + off..ptr + off + 8]);
3475        i64::from_le_bytes(b)
3476    };
3477    let tag = read(0);
3478    if tag == 0 {
3479        Ok(Ok(read(8)))
3480    } else {
3481        Ok(Err(tag))
3482    }
3483}
3484
3485/// Runner-side counterpart of `write_wasm_string`: build a Cairn string in
3486/// the instance's memory by calling the exported `__alloc`, returning the
3487/// pointer.
3488fn write_string_via_export(
3489    store: &mut WtStore,
3490    instance: &wasmtime::Instance,
3491    s: &str,
3492) -> std::result::Result<i64, RunError> {
3493    use wasmtime::Val;
3494    let bytes = s.as_bytes();
3495    let size = ((1 + bytes.len()) as i64) * 8;
3496    let alloc = instance
3497        .get_func(&mut *store, "__alloc")
3498        .ok_or_else(|| RunError::BadExport("__alloc".to_string()))?;
3499    let mut out = [Val::I64(0)];
3500    alloc
3501        .call(&mut *store, &[Val::I64(size)], &mut out)
3502        .map_err(|e| RunError::Wasmtime(e.to_string()))?;
3503    let Val::I64(ptr) = out[0] else {
3504        return Err(RunError::BadExport("__alloc".to_string()));
3505    };
3506    let mem = instance
3507        .get_memory(&mut *store, "mem")
3508        .ok_or_else(|| RunError::BadExport("mem".to_string()))?;
3509    let data = mem.data_mut(&mut *store);
3510    let p = ptr as usize;
3511    data[p..p + 8].copy_from_slice(&(bytes.len() as i64).to_le_bytes());
3512    for (i, b) in bytes.iter().enumerate() {
3513        let off = p + (1 + i) * 8;
3514        data[off..off + 8].copy_from_slice(&(*b as i64).to_le_bytes());
3515    }
3516    Ok(ptr)
3517}
3518
3519/// Drive a Cairn `handler(req: String) -> String` once: pass `request` in
3520/// (host→wasm), call the handler, read the response String back out. This is
3521/// the testable core of the standalone HTTP server — Cairn *as* a request
3522/// handler, both directions over the boundary.
3523pub fn serve_once(
3524    wasm: &[u8],
3525    handler: &str,
3526    request: &str,
3527) -> std::result::Result<String, RunError> {
3528    use wasmtime::Val;
3529    let (mut store, instance) = instantiate(wasm, None, None)?;
3530    let req_ptr = write_string_via_export(&mut store, &instance, request)?;
3531    let f = instance
3532        .get_func(&mut store, handler)
3533        .ok_or_else(|| RunError::BadExport(handler.to_string()))?;
3534    let mut out = [Val::I64(0)];
3535    f.call(&mut store, &[Val::I64(req_ptr)], &mut out)
3536        .map_err(|e| RunError::Wasmtime(e.to_string()))?;
3537    let Val::I64(resp_ptr) = out[0] else {
3538        return Err(RunError::BadExport(handler.to_string()));
3539    };
3540    let mem = instance
3541        .get_memory(&mut store, "mem")
3542        .ok_or_else(|| RunError::BadExport("mem".to_string()))?;
3543    Ok(read_wasm_string(mem.data(&store), resp_ptr as usize))
3544}
3545
3546/// Allocate an N-slot record block in the instance's memory (via the
3547/// exported `__alloc`) and write `slots` into it in declaration order,
3548/// returning the block pointer. A record is just N consecutive i64 slots —
3549/// a `String` field holds its own pointer, a `Number` field its raw value
3550/// — the same flat layout the `Record` lowering emits.
3551fn write_record_via_export(
3552    store: &mut WtStore,
3553    instance: &wasmtime::Instance,
3554    slots: &[i64],
3555) -> std::result::Result<i64, RunError> {
3556    use wasmtime::Val;
3557    let size = (slots.len() as i64) * 8;
3558    let alloc = instance
3559        .get_func(&mut *store, "__alloc")
3560        .ok_or_else(|| RunError::BadExport("__alloc".to_string()))?;
3561    let mut out = [Val::I64(0)];
3562    alloc
3563        .call(&mut *store, &[Val::I64(size)], &mut out)
3564        .map_err(|e| RunError::Wasmtime(e.to_string()))?;
3565    let Val::I64(ptr) = out[0] else {
3566        return Err(RunError::BadExport("__alloc".to_string()));
3567    };
3568    let mem = instance
3569        .get_memory(&mut *store, "mem")
3570        .ok_or_else(|| RunError::BadExport("mem".to_string()))?;
3571    let data = mem.data_mut(&mut *store);
3572    for (i, v) in slots.iter().enumerate() {
3573        let off = ptr as usize + i * 8;
3574        data[off..off + 8].copy_from_slice(&v.to_le_bytes());
3575    }
3576    Ok(ptr)
3577}
3578
3579/// A structured response read back from a Cairn `Response` record: the
3580/// `status` (a raw Number, slot 0) and the decoded `body` string (slot 1).
3581#[derive(Debug, Clone, PartialEq, Eq)]
3582pub struct HttpResponse {
3583    pub status: i64,
3584    pub body: String,
3585}
3586
3587/// Drive a Cairn `handler(req: Request) -> Response` once. The framework
3588/// ABI fixes the record field order: `Request` is
3589/// `{ method, path, body, headers }` (four String slots) and `Response`
3590/// is `{ status, body }` (a Number slot then a String slot). The host
3591/// builds the Request in wasm memory — each String via `__alloc`, then
3592/// a 4-slot block of their pointers — calls the handler, and reads the
3593/// Response back. The Cairn-side `RecordDef`s must declare their fields
3594/// in this order; that ordering is the framework contract.
3595pub fn serve_request(
3596    wasm: &[u8],
3597    handler: &str,
3598    method: &str,
3599    path: &str,
3600    body: &str,
3601) -> std::result::Result<HttpResponse, RunError> {
3602    serve_request_with(wasm, handler, method, path, body, "", None)
3603}
3604
3605/// Like [`serve_request`], but with a request header block (each
3606/// `Name: Value` line, `\n`-joined) the handler can read via the
3607/// framework `header`. The header-carrying entry the HTTP servers use.
3608pub fn serve_request_h(
3609    wasm: &[u8],
3610    handler: &str,
3611    method: &str,
3612    path: &str,
3613    body: &str,
3614    headers: &str,
3615) -> std::result::Result<HttpResponse, RunError> {
3616    serve_request_with(wasm, handler, method, path, body, headers, None)
3617}
3618
3619/// Like [`serve_request`], but `db_query` runs against the real SQLite
3620/// database at `db_path` (created if absent). State persists in that file
3621/// across requests — each request is still a fresh wasm instance, so
3622/// durability lives in SQLite, not in wasm memory. This is the persistent
3623/// standalone path: write in one request (INSERT…), read it in the next.
3624pub fn serve_request_db(
3625    wasm: &[u8],
3626    handler: &str,
3627    db_path: &str,
3628    method: &str,
3629    path: &str,
3630    body: &str,
3631) -> std::result::Result<HttpResponse, RunError> {
3632    serve_request_with(wasm, handler, method, path, body, "", Some(db_path))
3633}
3634
3635/// [`serve_request_db`] + a request header block — the path session
3636/// auth needs: persistent SQLite *and* the `Cookie` header in one
3637/// request (the framework `header`/`cookie` read it).
3638pub fn serve_request_db_h(
3639    wasm: &[u8],
3640    handler: &str,
3641    db_path: &str,
3642    method: &str,
3643    path: &str,
3644    body: &str,
3645    headers: &str,
3646) -> std::result::Result<HttpResponse, RunError> {
3647    serve_request_with(
3648        wasm,
3649        handler,
3650        method,
3651        path,
3652        body,
3653        headers,
3654        Some(db_path),
3655    )
3656}
3657
3658fn serve_request_with(
3659    wasm: &[u8],
3660    handler: &str,
3661    method: &str,
3662    path: &str,
3663    body: &str,
3664    headers: &str,
3665    db: Option<&str>,
3666) -> std::result::Result<HttpResponse, RunError> {
3667    use wasmtime::Val;
3668    let (mut store, instance) = instantiate(wasm, None, db)?;
3669    let m = write_string_via_export(&mut store, &instance, method)?;
3670    let p = write_string_via_export(&mut store, &instance, path)?;
3671    let b = write_string_via_export(&mut store, &instance, body)?;
3672    let hh = write_string_via_export(&mut store, &instance, headers)?;
3673    let req_ptr =
3674        write_record_via_export(&mut store, &instance, &[m, p, b, hh])?;
3675    let f = instance
3676        .get_func(&mut store, handler)
3677        .ok_or_else(|| RunError::BadExport(handler.to_string()))?;
3678    let mut out = [Val::I64(0)];
3679    f.call(&mut store, &[Val::I64(req_ptr)], &mut out)
3680        .map_err(|e| RunError::Wasmtime(e.to_string()))?;
3681    let Val::I64(resp_ptr) = out[0] else {
3682        return Err(RunError::BadExport(handler.to_string()));
3683    };
3684    let mem = instance
3685        .get_memory(&mut store, "mem")
3686        .ok_or_else(|| RunError::BadExport("mem".to_string()))?;
3687    let data = mem.data(&store);
3688    let slot = |i: usize| -> i64 {
3689        let off = resp_ptr as usize + i * 8;
3690        let mut x = [0u8; 8];
3691        x.copy_from_slice(&data[off..off + 8]);
3692        i64::from_le_bytes(x)
3693    };
3694    Ok(HttpResponse {
3695        status: slot(0),
3696        body: read_wasm_string(data, slot(1) as usize),
3697    })
3698}
3699
3700/// The HTTP reason phrase for the status codes the framework can currently
3701/// produce. Unknown codes fall back by class (2xx OK, 4xx/5xx generic);
3702/// deliberately small (Principle 9) — a full table is not needed yet.
3703pub(crate) fn reason_phrase(status: i64) -> &'static str {
3704    match status {
3705        200 => "OK",
3706        201 => "Created",
3707        204 => "No Content",
3708        400 => "Bad Request",
3709        404 => "Not Found",
3710        500 => "Internal Server Error",
3711        s if (200..300).contains(&s) => "OK",
3712        s if (400..500).contains(&s) => "Client Error",
3713        _ => "Error",
3714    }
3715}
3716
3717/// The standalone HTTP server: a real TCP accept loop driving the Cairn
3718/// `handler(req: Request) -> Response` per request (fresh instance per
3719/// request in v0.3 — clean state, documented). Thin glue over
3720/// [`serve_request`]: it parses the method, path, and (via
3721/// `Content-Length`) the request body, runs the handler, and emits the
3722/// typed Response's real status and body. Not unit-tested, like the MCP
3723/// server's stdio loop.
3724pub fn serve_http(
3725    wasm: &[u8],
3726    handler: &str,
3727    addr: &str,
3728) -> std::result::Result<(), RunError> {
3729    serve_http_with(wasm, handler, addr, None)
3730}
3731
3732/// Like [`serve_http`], but every request's `db_query` runs against the
3733/// real SQLite database at `db_path` — the persistent standalone server.
3734pub fn serve_http_db(
3735    wasm: &[u8],
3736    handler: &str,
3737    addr: &str,
3738    db_path: &str,
3739) -> std::result::Result<(), RunError> {
3740    serve_http_with(wasm, handler, addr, Some(db_path))
3741}
3742
3743fn serve_http_with(
3744    wasm: &[u8],
3745    handler: &str,
3746    addr: &str,
3747    db: Option<&str>,
3748) -> std::result::Result<(), RunError> {
3749    use std::io::{BufRead, BufReader, Read, Write};
3750    let listener = std::net::TcpListener::bind(addr)
3751        .map_err(|e| RunError::Wasmtime(e.to_string()))?;
3752    for stream in listener.incoming() {
3753        let Ok(mut stream) = stream else { continue };
3754        let mut reader = BufReader::new(&stream);
3755        let mut request_line = String::new();
3756        if reader.read_line(&mut request_line).is_err() {
3757            continue;
3758        }
3759        let mut parts = request_line.split_whitespace();
3760        let method = parts.next().unwrap_or("GET").to_string();
3761        let path = parts.next().unwrap_or("/").to_string();
3762        // Headers, until the blank line. Collect every line (CRLF
3763        // stripped, `\n`-joined) for the wasm Request's 4th slot; still
3764        // read Content-Length to frame the body.
3765        let mut content_length = 0usize;
3766        let mut header_lines: Vec<String> = Vec::new();
3767        loop {
3768            let mut h = String::new();
3769            if reader.read_line(&mut h).is_err() {
3770                break;
3771            }
3772            let h = h.trim_end();
3773            if h.is_empty() {
3774                break;
3775            }
3776            if let Some((name, value)) = h.split_once(':') {
3777                if name.eq_ignore_ascii_case("content-length") {
3778                    content_length = value.trim().parse().unwrap_or(0);
3779                }
3780            }
3781            header_lines.push(h.to_string());
3782        }
3783        let headers = header_lines.join("\n");
3784        let mut req_body = String::new();
3785        if content_length > 0 {
3786            let mut buf = vec![0u8; content_length];
3787            if reader.read_exact(&mut buf).is_ok() {
3788                req_body = String::from_utf8_lossy(&buf).into_owned();
3789            }
3790        }
3791        drop(reader);
3792        let (status, body) = match serve_request_with(
3793            wasm, handler, &method, &path, &req_body, &headers, db,
3794        ) {
3795                Ok(r) => (r.status, r.body),
3796                Err(e) => (500, format!("cairn handler error: {e}")),
3797            };
3798        // The Resp effect: drain the headers this request set (same
3799        // thread, right after the handler) and emit each verbatim.
3800        let extra: String = drain_resp_headers()
3801            .into_iter()
3802            .map(|(n, v)| format!("{n}: {v}\r\n"))
3803            .collect();
3804        let resp = format!(
3805            "HTTP/1.1 {status} {}\r\nContent-Length: {}\r\n\
3806             Content-Type: text/html; charset=utf-8\r\n{extra}\r\n{}",
3807            reason_phrase(status),
3808            body.len(),
3809            body
3810        );
3811        let _ = stream.write_all(resp.as_bytes());
3812    }
3813    Ok(())
3814}
3815
3816#[cfg(test)]
3817mod tests {
3818    use super::*;
3819    use crate::node::{Param, Produces};
3820    use crate::ty::{Confidence, Type};
3821    use std::collections::BTreeSet;
3822
3823    fn func(
3824        s: &Store,
3825        name: &str,
3826        params: &[&str],
3827        body: Vec<NodeHash>,
3828        result: NodeHash,
3829    ) -> NodeHash {
3830        s.put(&Node::Function {
3831            name: name.into(),
3832            type_params: vec![],
3833            params: params
3834                .iter()
3835                .map(|p| Param {
3836                    name: (*p).into(),
3837                    ty: Type::Number,
3838                    min_confidence: Confidence::External,
3839                })
3840                .collect(),
3841            produces: Produces {
3842                ty: Type::Number,
3843                confidence: Confidence::Structural,
3844            },
3845            requires: BTreeSet::new(),
3846            on_failure: vec![],
3847            body,
3848            result,
3849        })
3850        .unwrap()
3851    }
3852
3853    fn module(s: &Store, fns: Vec<NodeHash>) -> NodeHash {
3854        s.put(&Node::Module {
3855            name: "m".into(),
3856            types: vec![],
3857            functions: fns,
3858        })
3859        .unwrap()
3860    }
3861
3862    #[test]
3863    fn a_constant_function_runs_correctly() {
3864        let s = Store::open_in_memory().unwrap();
3865        let lit = s.put(&Node::Lit(42)).unwrap();
3866        let answer = func(&s, "answer", &[], vec![], lit);
3867        let m = module(&s, vec![answer]);
3868        let wasm = lower(&s, &m).unwrap();
3869        assert_eq!(run_i64(&wasm, "answer", &[]).unwrap(), 42);
3870    }
3871
3872    #[test]
3873    fn publish_performs_the_live_effect_observably() {
3874        // The Live effect end to end: publish("items") runs (returns 0)
3875        // and the host records the topic on the queue the live runtime
3876        // (slice 4) drains. Not dead surface — the seam is exercised.
3877        let s = Store::open_in_memory().unwrap();
3878        let topic = s.put(&Node::Str("items".into())).unwrap();
3879        let notify = func(
3880            &s,
3881            "notify",
3882            &[],
3883            vec![],
3884            s.put(&Node::Publish(topic)).unwrap(),
3885        );
3886        let m = module(&s, vec![notify]);
3887        let wasm = lower(&s, &m).unwrap();
3888        let _ = super::drain_published(); // clear any prior
3889        assert_eq!(run_i64(&wasm, "notify", &[]).unwrap(), 0);
3890        assert!(
3891            super::drain_published().contains(&"items".to_string()),
3892            "host::publish must record the topic"
3893        );
3894    }
3895
3896    #[test]
3897    fn num_to_str_handles_full_i64_range() {
3898        let s = Store::open_in_memory().unwrap();
3899        let rt = |name: &str, v: i64| {
3900            let lit = s.put(&Node::Lit(v)).unwrap();
3901            let n2s = s.put(&Node::NumberToStr(lit)).unwrap();
3902            func(&s, name, &[], vec![], s.put(&Node::StrToNumber(n2s)).unwrap())
3903        };
3904        let vals = [
3905            ("mn", i64::MIN),
3906            ("mx", i64::MAX),
3907            ("neg", -1),
3908            ("zero", 0),
3909            ("c", 100),
3910            ("near", i64::MIN + 7),
3911        ];
3912        let fns: Vec<_> = vals.iter().map(|(n, v)| rt(n, *v)).collect();
3913        // length of str(i64::MIN) == 20 ("-9223372036854775808")
3914        let lit = s.put(&Node::Lit(i64::MIN)).unwrap();
3915        let n2s = s.put(&Node::NumberToStr(lit)).unwrap();
3916        let mut all = fns;
3917        all.push(func(&s, "mnlen", &[], vec![], s.put(&Node::StrLen(n2s)).unwrap()));
3918        let m = module(&s, all);
3919        let wasm = lower(&s, &m).unwrap();
3920        for (n, v) in vals {
3921            assert_eq!(run_i64(&wasm, n, &[]).unwrap(), v, "round-trip {n}");
3922        }
3923        assert_eq!(run_i64(&wasm, "mnlen", &[]).unwrap(), 20);
3924    }
3925
3926    #[test]
3927    fn boolean_and_comparison_operators_run() {
3928        let s = Store::open_in_memory().unwrap();
3929        let n = |v: i64| s.put(&Node::Lit(v)).unwrap();
3930        let b = |v: bool| s.put(&Node::Bool(v)).unwrap();
3931        let bin = |op, l: NodeHash, r: NodeHash| {
3932            s.put(&Node::BinOp {
3933                op,
3934                lhs: l,
3935                rhs: r,
3936            })
3937            .unwrap()
3938        };
3939        use crate::node::BinOp::*;
3940
3941        let rmod = func(&s, "rmod", &[], vec![], bin(Mod, n(7), n(3))); // 1
3942        let le = func(&s, "le", &[], vec![], bin(Le, n(3), n(3))); // 1
3943        let gt = func(&s, "gt", &[], vec![], bin(Gt, n(5), n(2))); // 1
3944        let ge = func(&s, "ge", &[], vec![], bin(Ge, n(2), n(5))); // 0
3945        let ne = func(&s, "ne", &[], vec![], bin(Neq, n(4), n(4))); // 0
3946        let notf = func(
3947            &s,
3948            "notf",
3949            &[],
3950            vec![],
3951            s.put(&Node::Not(b(false))).unwrap(),
3952        ); // 1
3953        let andt = func(
3954            &s,
3955            "andt",
3956            &[],
3957            vec![],
3958            bin(And, b(true), bin(Lt, n(1), n(2))),
3959        ); // 1
3960
3961        // Short-circuit: `1 / 0` traps. If it were evaluated the call
3962        // would error; returning the left-driven answer proves the right
3963        // operand was skipped.
3964        let sc_or =
3965            func(&s, "sc_or", &[], vec![], bin(Or, b(true), bin(Div, n(1), n(0))));
3966        let sc_and = func(
3967            &s,
3968            "sc_and",
3969            &[],
3970            vec![],
3971            bin(And, b(false), bin(Div, n(1), n(0))),
3972        );
3973
3974        let m = module(
3975            &s,
3976            vec![rmod, le, gt, ge, ne, notf, andt, sc_or, sc_and],
3977        );
3978        let wasm = lower(&s, &m).unwrap();
3979        assert_eq!(run_i64(&wasm, "rmod", &[]).unwrap(), 1);
3980        assert_eq!(run_i64(&wasm, "le", &[]).unwrap(), 1);
3981        assert_eq!(run_i64(&wasm, "gt", &[]).unwrap(), 1);
3982        assert_eq!(run_i64(&wasm, "ge", &[]).unwrap(), 0);
3983        assert_eq!(run_i64(&wasm, "ne", &[]).unwrap(), 0);
3984        assert_eq!(run_i64(&wasm, "notf", &[]).unwrap(), 1);
3985        assert_eq!(run_i64(&wasm, "andt", &[]).unwrap(), 1);
3986        assert_eq!(
3987            run_i64(&wasm, "sc_or", &[]).unwrap(),
3988            1,
3989            "Or must short-circuit past a trapping right operand"
3990        );
3991        assert_eq!(
3992            run_i64(&wasm, "sc_and", &[]).unwrap(),
3993            0,
3994            "And must short-circuit past a trapping right operand"
3995        );
3996    }
3997
3998    #[test]
3999    fn function_values_pass_and_call_indirect() {
4000        // `double(n) = n*2`; `apply(f, x) = f(x)`; pass `&double` into
4001        // `apply` and also call a function value directly. Proves the
4002        // funcref table + call_indirect path end to end.
4003        let s = Store::open_in_memory().unwrap();
4004        let n = |v: i64| s.put(&Node::Lit(v)).unwrap();
4005        let r = |name: &str| s.put(&Node::Ref(name.into())).unwrap();
4006
4007        let double = func(
4008            &s,
4009            "double",
4010            &["n"],
4011            vec![],
4012            s.put(&Node::BinOp {
4013                op: crate::node::BinOp::Mul,
4014                lhs: r("n"),
4015                rhs: n(2),
4016            })
4017            .unwrap(),
4018        );
4019        let apply = func(
4020            &s,
4021            "apply",
4022            &["f", "x"],
4023            vec![],
4024            s.put(&Node::CallValue {
4025                callee: r("f"),
4026                args: vec![r("x")],
4027            })
4028            .unwrap(),
4029        );
4030        // apply(&double, 21) == 42
4031        let via_param = func(
4032            &s,
4033            "via_param",
4034            &[],
4035            vec![],
4036            s.put(&Node::Call {
4037                func: "apply".into(),
4038                args: vec![
4039                    s.put(&Node::FuncRef("double".into())).unwrap(),
4040                    n(21),
4041                ],
4042            })
4043            .unwrap(),
4044        );
4045        // (&double)(19) == 38 — FuncRef called directly.
4046        let direct = func(
4047            &s,
4048            "direct",
4049            &[],
4050            vec![],
4051            s.put(&Node::CallValue {
4052                callee: s.put(&Node::FuncRef("double".into())).unwrap(),
4053                args: vec![n(19)],
4054            })
4055            .unwrap(),
4056        );
4057        let m = module(&s, vec![double, apply, via_param, direct]);
4058        let wasm = lower(&s, &m).unwrap();
4059        assert_eq!(run_i64(&wasm, "via_param", &[]).unwrap(), 42);
4060        assert_eq!(run_i64(&wasm, "direct", &[]).unwrap(), 38);
4061    }
4062
4063    #[test]
4064    fn closures_capture_and_run() {
4065        // `mk(k) = |x| x + k` (captures k); apply(f,v)=f(v).
4066        // apply(mk(10), 5) == 15 ; (mk(10))(7) == 17.
4067        let s = Store::open_in_memory().unwrap();
4068        let n = |v: i64| s.put(&Node::Lit(v)).unwrap();
4069        let r = |name: &str| s.put(&Node::Ref(name.into())).unwrap();
4070        let par = |name: &str| Param {
4071            name: name.into(),
4072            ty: Type::Number,
4073            min_confidence: Confidence::External,
4074        };
4075        let add = |a: NodeHash, b: NodeHash| {
4076            s.put(&Node::BinOp {
4077                op: crate::node::BinOp::Add,
4078                lhs: a,
4079                rhs: b,
4080            })
4081            .unwrap()
4082        };
4083
4084        let lam = s
4085            .put(&Node::Lambda {
4086                params: vec![par("x")],
4087                body: add(r("x"), r("k")),
4088            })
4089            .unwrap();
4090        let mk = func(&s, "mk", &["k"], vec![], lam);
4091        let apply = func(
4092            &s,
4093            "apply",
4094            &["f", "v"],
4095            vec![],
4096            s.put(&Node::CallValue {
4097                callee: r("f"),
4098                args: vec![r("v")],
4099            })
4100            .unwrap(),
4101        );
4102        let via_apply = func(
4103            &s,
4104            "via_apply",
4105            &[],
4106            vec![],
4107            s.put(&Node::Call {
4108                func: "apply".into(),
4109                args: vec![
4110                    s.put(&Node::Call {
4111                        func: "mk".into(),
4112                        args: vec![n(10)],
4113                    })
4114                    .unwrap(),
4115                    n(5),
4116                ],
4117            })
4118            .unwrap(),
4119        );
4120        let direct = func(
4121            &s,
4122            "direct",
4123            &[],
4124            vec![],
4125            s.put(&Node::CallValue {
4126                callee: s
4127                    .put(&Node::Call {
4128                        func: "mk".into(),
4129                        args: vec![n(10)],
4130                    })
4131                    .unwrap(),
4132                args: vec![n(7)],
4133            })
4134            .unwrap(),
4135        );
4136
4137        // Transitive capture: mk2(k) = |x| (|y| (x+y)+k).
4138        let inner = s
4139            .put(&Node::Lambda {
4140                params: vec![par("y")],
4141                body: add(add(r("x"), r("y")), r("k")),
4142            })
4143            .unwrap();
4144        let outer = s
4145            .put(&Node::Lambda {
4146                params: vec![par("x")],
4147                body: inner,
4148            })
4149            .unwrap();
4150        let mk2 = func(&s, "mk2", &["k"], vec![], outer);
4151        let nested = func(
4152            &s,
4153            "nested",
4154            &[],
4155            vec![],
4156            s.put(&Node::CallValue {
4157                callee: s
4158                    .put(&Node::CallValue {
4159                        callee: s
4160                            .put(&Node::Call {
4161                                func: "mk2".into(),
4162                                args: vec![n(100)],
4163                            })
4164                            .unwrap(),
4165                        args: vec![n(20)],
4166                    })
4167                    .unwrap(),
4168                args: vec![n(3)],
4169            })
4170            .unwrap(),
4171        );
4172
4173        let m = module(
4174            &s,
4175            vec![mk, apply, via_apply, direct, mk2, nested],
4176        );
4177        let wasm = lower(&s, &m).unwrap();
4178        assert_eq!(run_i64(&wasm, "via_apply", &[]).unwrap(), 15);
4179        assert_eq!(run_i64(&wasm, "direct", &[]).unwrap(), 17);
4180        assert_eq!(
4181            run_i64(&wasm, "nested", &[]).unwrap(),
4182            123,
4183            "transitive capture: (x+y)+k = (20+3)+100"
4184        );
4185    }
4186
4187    #[test]
4188    fn option_match_drives_control_flow() {
4189        // at_or(i) = match list_try_get([10,20,30], i) {
4190        //   Some(v) -> v, None -> -1 }
4191        let s = Store::open_in_memory().unwrap();
4192        let n = |v: i64| s.put(&Node::Lit(v)).unwrap();
4193        let xs = s.put(&Node::List(vec![n(10), n(20), n(30)])).unwrap();
4194        let at = |i: i64| {
4195            s.put(&Node::OptionMatch {
4196                opt: s
4197                    .put(&Node::ListTryGet {
4198                        list: xs.clone(),
4199                        index: n(i),
4200                    })
4201                    .unwrap(),
4202                some_bind: "v".into(),
4203                some_body: s.put(&Node::Ref("v".into())).unwrap(),
4204                none_body: n(-1),
4205            })
4206            .unwrap()
4207        };
4208        let hit = func(&s, "hit", &[], vec![], at(1)); // -> 20
4209        let miss = func(&s, "miss", &[], vec![], at(9)); // -> -1
4210        let m = module(&s, vec![hit, miss]);
4211        let wasm = lower(&s, &m).unwrap();
4212        assert_eq!(run_i64(&wasm, "hit", &[]).unwrap(), 20);
4213        assert_eq!(run_i64(&wasm, "miss", &[]).unwrap(), -1);
4214    }
4215
4216    #[test]
4217    fn map_try_get_yields_an_option() {
4218        // m = {1:100, 2:200}; look up via map_try_get + OptionMatch.
4219        let s = Store::open_in_memory().unwrap();
4220        let n = |v: i64| s.put(&Node::Lit(v)).unwrap();
4221        let m = s
4222            .put(&Node::Map(vec![(n(1), n(100)), (n(2), n(200))]))
4223            .unwrap();
4224        let look = |key: i64| {
4225            s.put(&Node::OptionMatch {
4226                opt: s
4227                    .put(&Node::MapTryGet {
4228                        map: m.clone(),
4229                        key: n(key),
4230                    })
4231                    .unwrap(),
4232                some_bind: "v".into(),
4233                some_body: s.put(&Node::Ref("v".into())).unwrap(),
4234                none_body: n(-1),
4235            })
4236            .unwrap()
4237        };
4238        let hit = func(&s, "hit", &[], vec![], look(2)); // 200
4239        let miss = func(&s, "miss", &[], vec![], look(9)); // -1
4240        let md = module(&s, vec![hit, miss]);
4241        let wasm = lower(&s, &md).unwrap();
4242        assert_eq!(run_i64(&wasm, "hit", &[]).unwrap(), 200);
4243        assert_eq!(run_i64(&wasm, "miss", &[]).unwrap(), -1);
4244    }
4245
4246    #[test]
4247    fn float_arithmetic_is_real_ieee754() {
4248        use crate::node::BinOp;
4249        let s = Store::open_in_memory().unwrap();
4250        let f = |v: f64| s.put(&Node::FloatLit(v.to_bits())).unwrap();
4251        let op = |o, a: NodeHash, b: NodeHash| {
4252            s.put(&Node::FloatOp {
4253                op: o,
4254                lhs: a,
4255                rhs: b,
4256            })
4257            .unwrap()
4258        };
4259
4260        // 2.5 * 4.0 = 10.0 -> to_int = 10
4261        let prod = func(
4262            &s,
4263            "prod",
4264            &[],
4265            vec![],
4266            s.put(&Node::FloatToInt(op(BinOp::Mul, f(2.5), f(4.0)))).unwrap(),
4267        );
4268        // 0.1 < 0.2 -> true(1)
4269        let lt = func(&s, "lt", &[], vec![], op(BinOp::Lt, f(0.1), f(0.2)));
4270        // (0.1 + 0.2) == 0.3 -> false(0): real f64 inexactness
4271        let inexact = func(
4272            &s,
4273            "inexact",
4274            &[],
4275            vec![],
4276            op(BinOp::Eq, op(BinOp::Add, f(0.1), f(0.2)), f(0.3)),
4277        );
4278        // raw bits round-trip and int<->float
4279        let raw = func(&s, "raw", &[], vec![], f(3.5));
4280        let i2f2i = func(
4281            &s,
4282            "i2f2i",
4283            &[],
4284            vec![],
4285            s.put(&Node::FloatToInt(
4286                s.put(&Node::IntToFloat(s.put(&Node::Lit(7)).unwrap()))
4287                    .unwrap(),
4288            ))
4289            .unwrap(),
4290        );
4291        let m = module(&s, vec![prod, lt, inexact, raw, i2f2i]);
4292        let wasm = lower(&s, &m).unwrap();
4293        assert_eq!(run_i64(&wasm, "prod", &[]).unwrap(), 10);
4294        assert_eq!(run_i64(&wasm, "lt", &[]).unwrap(), 1);
4295        assert_eq!(
4296            run_i64(&wasm, "inexact", &[]).unwrap(),
4297            0,
4298            "0.1+0.2 != 0.3 — genuine IEEE-754"
4299        );
4300        assert_eq!(
4301            run_i64(&wasm, "raw", &[]).unwrap(),
4302            3.5f64.to_bits() as i64,
4303            "Float travels as its f64 bit pattern in the i64 slot"
4304        );
4305        assert_eq!(run_i64(&wasm, "i2f2i", &[]).unwrap(), 7);
4306    }
4307
4308    #[test]
4309    fn decimal_is_exact() {
4310        use crate::node::BinOp;
4311        let s = Store::open_in_memory().unwrap();
4312        // value pre-scaled by 10_000
4313        let d = |v: f64| {
4314            s.put(&Node::DecimalLit((v * 10_000.0).round() as i64))
4315                .unwrap()
4316        };
4317        let op = |o, a: NodeHash, b: NodeHash| {
4318            s.put(&Node::DecimalOp {
4319                op: o,
4320                lhs: a,
4321                rhs: b,
4322            })
4323            .unwrap()
4324        };
4325        let to_int = |x: NodeHash| s.put(&Node::DecimalToInt(x)).unwrap();
4326
4327        // 1.25 * 4.00 = 5.0000 exactly -> 5
4328        let prod = func(
4329            &s,
4330            "prod",
4331            &[],
4332            vec![],
4333            to_int(op(BinOp::Mul, d(1.25), d(4.0))),
4334        );
4335        // (0.10 + 0.20) == 0.30 -> TRUE(1): exact, where Float was false
4336        let exact = func(
4337            &s,
4338            "exact",
4339            &[],
4340            vec![],
4341            op(BinOp::Eq, op(BinOp::Add, d(0.10), d(0.20)), d(0.30)),
4342        );
4343        // money: 19.99 + 0.01 = 20.00 -> 20
4344        let money = func(
4345            &s,
4346            "money",
4347            &[],
4348            vec![],
4349            to_int(op(BinOp::Add, d(19.99), d(0.01))),
4350        );
4351        // int -> decimal -> int round-trips
4352        let i2d2i = func(
4353            &s,
4354            "i2d2i",
4355            &[],
4356            vec![],
4357            to_int(s.put(&Node::IntToDecimal(s.put(&Node::Lit(7)).unwrap())).unwrap()),
4358        );
4359        let m = module(&s, vec![prod, exact, money, i2d2i]);
4360        let wasm = lower(&s, &m).unwrap();
4361        assert_eq!(run_i64(&wasm, "prod", &[]).unwrap(), 5);
4362        assert_eq!(
4363            run_i64(&wasm, "exact", &[]).unwrap(),
4364            1,
4365            "0.10+0.20 == 0.30 exactly — the point of Decimal"
4366        );
4367        assert_eq!(run_i64(&wasm, "money", &[]).unwrap(), 20);
4368        assert_eq!(run_i64(&wasm, "i2d2i", &[]).unwrap(), 7);
4369    }
4370
4371    #[test]
4372    fn out_of_bounds_list_get_and_str_slice_trap() {
4373        // Bounds checks turn silent OOB memory reads into a clean trap.
4374        let s = Store::open_in_memory().unwrap();
4375        let n = |v: i64| s.put(&Node::Lit(v)).unwrap();
4376        let list = s.put(&Node::List(vec![n(10), n(20), n(30)])).unwrap();
4377
4378        // In bounds: [10,20,30][1] == 20 — unaffected.
4379        let good = func(
4380            &s,
4381            "good",
4382            &[],
4383            vec![],
4384            s.put(&Node::ListGet {
4385                list: list.clone(),
4386                index: n(1),
4387            })
4388            .unwrap(),
4389        );
4390        // Out of bounds: index 5 of a length-3 list — must trap.
4391        let oob = func(
4392            &s,
4393            "oob",
4394            &[],
4395            vec![],
4396            s.put(&Node::ListGet {
4397                list,
4398                index: n(5),
4399            })
4400            .unwrap(),
4401        );
4402        // StrSlice past the end: len("ab"[0..5]) — must trap.
4403        let bad = func(
4404            &s,
4405            "bad",
4406            &[],
4407            vec![],
4408            s.put(&Node::StrLen(
4409                s.put(&Node::StrSlice {
4410                    s: s.put(&Node::Str("ab".into())).unwrap(),
4411                    start: n(0),
4412                    len: n(5),
4413                })
4414                .unwrap(),
4415            ))
4416            .unwrap(),
4417        );
4418        let m = module(&s, vec![good, oob, bad]);
4419        let wasm = lower(&s, &m).unwrap();
4420        assert_eq!(run_i64(&wasm, "good", &[]).unwrap(), 20);
4421        assert!(run_i64(&wasm, "oob", &[]).is_err(), "OOB index must trap");
4422        assert!(
4423            run_i64(&wasm, "bad", &[]).is_err(),
4424            "OOB slice must trap"
4425        );
4426    }
4427
4428    #[test]
4429    fn str_to_number_opt_rejects_invalid_input() {
4430        // option_else(str_to_number_opt(<lit>), -999): valid -> the
4431        // number; invalid -> -999 (None), where lenient str_to_number
4432        // would have silently returned a wrong value (e.g. "12x" -> 12).
4433        let s = Store::open_in_memory().unwrap();
4434        let mk = |name: &str, input: &str| -> NodeHash {
4435            let body = s
4436                .put(&Node::OptionElse {
4437                    opt: s
4438                        .put(&Node::StrToNumberOpt(
4439                            s.put(&Node::Str(input.into())).unwrap(),
4440                        ))
4441                        .unwrap(),
4442                    default: s.put(&Node::Lit(-999)).unwrap(),
4443                })
4444                .unwrap();
4445            func(&s, name, &[], vec![], body)
4446        };
4447        let fns = vec![
4448            mk("a", "42"),
4449            mk("b", "-7"),
4450            mk("c", "0"),
4451            mk("d", "12x"),
4452            mk("e", ""),
4453            mk("f", "-"),
4454            mk("g", "3.5"),
4455        ];
4456        let m = module(&s, fns);
4457        let wasm = lower(&s, &m).unwrap();
4458        assert_eq!(run_i64(&wasm, "a", &[]).unwrap(), 42);
4459        assert_eq!(run_i64(&wasm, "b", &[]).unwrap(), -7);
4460        assert_eq!(run_i64(&wasm, "c", &[]).unwrap(), 0);
4461        assert_eq!(run_i64(&wasm, "d", &[]).unwrap(), -999); // trailing junk
4462        assert_eq!(run_i64(&wasm, "e", &[]).unwrap(), -999); // empty
4463        assert_eq!(run_i64(&wasm, "f", &[]).unwrap(), -999); // just '-'
4464        assert_eq!(run_i64(&wasm, "g", &[]).unwrap(), -999); // not an int
4465    }
4466
4467    #[test]
4468    fn option_recovers_from_out_of_bounds_without_trapping() {
4469        // at(i) = list_try_get([10,20,30], i) else -1
4470        // In bounds -> the element; OOB -> the default, no trap, and no
4471        // `on_failure` on `at` (the whole point: no signature pollution).
4472        let s = Store::open_in_memory().unwrap();
4473        let n = |v: i64| s.put(&Node::Lit(v)).unwrap();
4474        let list = s.put(&Node::List(vec![n(10), n(20), n(30)])).unwrap();
4475        let body = s
4476            .put(&Node::OptionElse {
4477                opt: s
4478                    .put(&Node::ListTryGet {
4479                        list,
4480                        index: s.put(&Node::Ref("i".into())).unwrap(),
4481                    })
4482                    .unwrap(),
4483                default: n(-1),
4484            })
4485            .unwrap();
4486        let at = func(&s, "at", &["i"], vec![], body);
4487        // some(7) else -1 == 7 ; none<Number>() else -1 == -1
4488        let some = func(
4489            &s,
4490            "som",
4491            &[],
4492            vec![],
4493            s.put(&Node::OptionElse {
4494                opt: s.put(&Node::OptionSome(n(7))).unwrap(),
4495                default: n(-1),
4496            })
4497            .unwrap(),
4498        );
4499        let none = func(
4500            &s,
4501            "non",
4502            &[],
4503            vec![],
4504            s.put(&Node::OptionElse {
4505                opt: s
4506                    .put(&Node::OptionNone {
4507                        elem: Type::Number,
4508                    })
4509                    .unwrap(),
4510                default: n(-1),
4511            })
4512            .unwrap(),
4513        );
4514        let m = module(&s, vec![at, some, none]);
4515        let wasm = lower(&s, &m).unwrap();
4516        assert_eq!(run_i64(&wasm, "at", &[0]).unwrap(), 10);
4517        assert_eq!(run_i64(&wasm, "at", &[2]).unwrap(), 30);
4518        assert_eq!(run_i64(&wasm, "at", &[5]).unwrap(), -1); // OOB, no trap
4519        assert_eq!(run_i64(&wasm, "at", &[-1]).unwrap(), -1); // OOB, no trap
4520        assert_eq!(run_i64(&wasm, "som", &[]).unwrap(), 7);
4521        assert_eq!(run_i64(&wasm, "non", &[]).unwrap(), -1);
4522    }
4523
4524    #[test]
4525    fn allocation_grows_past_the_initial_page() {
4526        // mk(n)=if n==0 then []:List<Number> else cons(n, mk(n-1));
4527        // sum over it. Each cons is a fresh array copy, so total bytes
4528        // allocated is O(n^2) — for n=1500 that is tens of MB, far past
4529        // the initial 64 KiB page. Before memory.grow this trapped.
4530        let s = Store::open_in_memory().unwrap();
4531        let r = |n: &str| s.put(&Node::Ref(n.into())).unwrap();
4532        let num = |v: i64| s.put(&Node::Lit(v)).unwrap();
4533        let binop = |op, a: NodeHash, b: NodeHash| {
4534            s.put(&Node::BinOp { op, lhs: a, rhs: b }).unwrap()
4535        };
4536        let p = |name: &str, ty: Type| Param {
4537            name: name.into(),
4538            ty,
4539            min_confidence: Confidence::External,
4540        };
4541        let ext = |ty: Type| Produces {
4542            ty,
4543            confidence: Confidence::External,
4544        };
4545        let num_list = || Type::List(Box::new(Type::Number));
4546
4547        let mk = s
4548            .put(&Node::Function {
4549                name: "mk".into(),
4550                type_params: vec![],
4551                params: vec![p("n", Type::Number)],
4552                produces: ext(num_list()),
4553                requires: BTreeSet::new(),
4554                on_failure: vec![],
4555                body: vec![],
4556                result: s
4557                    .put(&Node::If {
4558                        cond: binop(crate::node::BinOp::Eq, r("n"), num(0)),
4559                        then_branch: s
4560                            .put(&Node::ListEmpty {
4561                                elem: Type::Number,
4562                            })
4563                            .unwrap(),
4564                        else_branch: s
4565                            .put(&Node::ListCons {
4566                                head: r("n"),
4567                                tail: s
4568                                    .put(&Node::Call {
4569                                        func: "mk".into(),
4570                                        args: vec![binop(
4571                                            crate::node::BinOp::Sub,
4572                                            r("n"),
4573                                            num(1),
4574                                        )],
4575                                    })
4576                                    .unwrap(),
4577                            })
4578                            .unwrap(),
4579                    })
4580                    .unwrap(),
4581            })
4582            .unwrap();
4583        let sum = s
4584            .put(&Node::Function {
4585                name: "sum".into(),
4586                type_params: vec![],
4587                params: vec![p("xs", num_list()), p("i", Type::Number)],
4588                produces: ext(Type::Number),
4589                requires: BTreeSet::new(),
4590                on_failure: vec![],
4591                body: vec![s
4592                    .put(&Node::Step {
4593                        binding: "len".into(),
4594                        value: s.put(&Node::ListLen(r("xs"))).unwrap(),
4595                    })
4596                    .unwrap()],
4597                result: s
4598                    .put(&Node::If {
4599                        cond: binop(crate::node::BinOp::Eq, r("i"), r("len")),
4600                        then_branch: num(0),
4601                        else_branch: binop(
4602                            crate::node::BinOp::Add,
4603                            s.put(&Node::ListGet {
4604                                list: r("xs"),
4605                                index: r("i"),
4606                            })
4607                            .unwrap(),
4608                            s.put(&Node::Call {
4609                                func: "sum".into(),
4610                                args: vec![
4611                                    r("xs"),
4612                                    binop(crate::node::BinOp::Add, r("i"), num(1)),
4613                                ],
4614                            })
4615                            .unwrap(),
4616                        ),
4617                    })
4618                    .unwrap(),
4619            })
4620            .unwrap();
4621        let total = s
4622            .put(&Node::Function {
4623                name: "total".into(),
4624                type_params: vec![],
4625                params: vec![p("n", Type::Number)],
4626                produces: ext(Type::Number),
4627                requires: BTreeSet::new(),
4628                on_failure: vec![],
4629                body: vec![s
4630                    .put(&Node::Step {
4631                        binding: "xs".into(),
4632                        value: s
4633                            .put(&Node::Call {
4634                                func: "mk".into(),
4635                                args: vec![r("n")],
4636                            })
4637                            .unwrap(),
4638                    })
4639                    .unwrap()],
4640                result: s
4641                    .put(&Node::Call {
4642                        func: "sum".into(),
4643                        args: vec![r("xs"), num(0)],
4644                    })
4645                    .unwrap(),
4646            })
4647            .unwrap();
4648        let m = module(&s, vec![total, mk, sum]);
4649        let wasm = lower(&s, &m).unwrap();
4650        // 1+2+...+1500 = 1500*1501/2; allocating this never traps now.
4651        assert_eq!(run_i64(&wasm, "total", &[1500]).unwrap(), 1_125_750);
4652    }
4653
4654    #[test]
4655    fn real_net_get_returns_the_http_status() {
4656        use std::io::{Read, Write};
4657        // A loopback HTTP server (hermetic — no external network) that
4658        // answers one request with 204.
4659        let listener =
4660            std::net::TcpListener::bind("127.0.0.1:0").unwrap();
4661        let port = listener.local_addr().unwrap().port();
4662        let server = std::thread::spawn(move || {
4663            if let Ok((mut stream, _)) = listener.accept() {
4664                let mut buf = [0u8; 256];
4665                let _ = stream.read(&mut buf); // consume the request
4666                let _ = stream.write_all(
4667                    b"HTTP/1.1 204 No Content\r\nContent-Length: 0\r\n\r\n",
4668                );
4669            }
4670        });
4671
4672        // ping() requires Net -> Number { yield net_get("http://127.0.0.1:PORT/") }
4673        let s = Store::open_in_memory().unwrap();
4674        let url = s
4675            .put(&Node::Str(format!("http://127.0.0.1:{port}/")))
4676            .unwrap();
4677        let get = s.put(&Node::NetGet(url)).unwrap();
4678        let mut net = BTreeSet::new();
4679        net.insert(crate::ty::Effect::Net);
4680        let ping = s
4681            .put(&Node::Function {
4682                name: "ping".into(),
4683                type_params: vec![],
4684                params: vec![],
4685                produces: Produces {
4686                    ty: Type::Number,
4687                    confidence: Confidence::External,
4688                },
4689                requires: net,
4690                on_failure: vec![],
4691                body: vec![],
4692                result: get,
4693            })
4694            .unwrap();
4695        let m = module(&s, vec![ping]);
4696        let wasm = lower(&s, &m).unwrap();
4697        // The real client returns the HTTP status (no stub path exists).
4698        assert_eq!(run_i64(&wasm, "ping", &[]).unwrap(), 204);
4699        server.join().unwrap();
4700    }
4701
4702    #[test]
4703    fn a_function_returns_its_parameter() {
4704        let s = Store::open_in_memory().unwrap();
4705        let nref = s.put(&Node::Ref("n".into())).unwrap();
4706        let id = func(&s, "id", &["n"], vec![], nref);
4707        let m = module(&s, vec![id]);
4708        let wasm = lower(&s, &m).unwrap();
4709        assert_eq!(run_i64(&wasm, "id", &[7]).unwrap(), 7);
4710    }
4711
4712    #[test]
4713    fn a_call_through_a_binding_runs() {
4714        let s = Store::open_in_memory().unwrap();
4715        let nref = s.put(&Node::Ref("n".into())).unwrap();
4716        let id = func(&s, "id", &["n"], vec![], nref);
4717
4718        let seven = s.put(&Node::Lit(7)).unwrap();
4719        let call = s
4720            .put(&Node::Call {
4721                func: "id".into(),
4722                args: vec![seven],
4723            })
4724            .unwrap();
4725        let step = s
4726            .put(&Node::Step {
4727                binding: "x".into(),
4728                value: call,
4729            })
4730            .unwrap();
4731        let xref = s.put(&Node::Ref("x".into())).unwrap();
4732        let use_id = func(&s, "use_id", &[], vec![step], xref);
4733
4734        // callee `id` precedes caller `use_id`.
4735        let m = module(&s, vec![id, use_id]);
4736        let wasm = lower(&s, &m).unwrap();
4737        assert_eq!(run_i64(&wasm, "use_id", &[]).unwrap(), 7);
4738    }
4739
4740    #[test]
4741    fn a_hole_cannot_be_lowered() {
4742        let s = Store::open_in_memory().unwrap();
4743        let hole = s
4744            .put(&Node::Hole {
4745                expects: "Number".into(),
4746            })
4747            .unwrap();
4748        let f = func(&s, "f", &[], vec![], hole);
4749        let m = module(&s, vec![f]);
4750        assert!(matches!(lower(&s, &m), Err(LowerError::Hole)));
4751    }
4752
4753    #[test]
4754    fn an_effectful_function_runs_via_a_host_import() {
4755        let s = Store::open_in_memory().unwrap();
4756        // clock() requires Time -> Number@external { yield now() }
4757        let now = s.put(&Node::Now).unwrap();
4758        let mut time = BTreeSet::new();
4759        time.insert(crate::ty::Effect::Time);
4760        let clock = s
4761            .put(&Node::Function {
4762                name: "clock".into(),
4763                type_params: vec![],
4764                params: vec![],
4765                produces: Produces {
4766                    ty: Type::Number,
4767                    confidence: Confidence::External,
4768                },
4769                requires: time,
4770                on_failure: vec![],
4771                body: vec![],
4772                result: now,
4773            })
4774            .unwrap();
4775        // The checker must accept it (Time is covered by `requires`).
4776        let m = module(&s, vec![clock]);
4777        let report = crate::check::Checker::new(&s).check(&m).unwrap();
4778        assert!(report.ok(), "unexpected: {:?}", report.violations);
4779
4780        let wasm = lower(&s, &m).unwrap();
4781        // The Time effect (host::now) returns whatever the harness supplies.
4782        assert_eq!(run_effectful_i64(&wasm, "clock", &[], 42).unwrap(), 42);
4783        assert_eq!(run_effectful_i64(&wasm, "clock", &[], 7).unwrap(), 7);
4784    }
4785
4786    #[test]
4787    fn using_an_effect_without_declaring_it_is_a_principle_5_violation() {
4788        let s = Store::open_in_memory().unwrap();
4789        let now = s.put(&Node::Now).unwrap();
4790        let bad = s
4791            .put(&Node::Function {
4792                name: "bad".into(),
4793                type_params: vec![],
4794                params: vec![],
4795                produces: Produces {
4796                    ty: Type::Number,
4797                    confidence: Confidence::External,
4798                },
4799                requires: BTreeSet::new(), // does NOT declare Time
4800                on_failure: vec![],
4801                body: vec![],
4802                result: now,
4803            })
4804            .unwrap();
4805        let report = crate::check::Checker::new(&s).check(&bad).unwrap();
4806        assert!(report.violations.iter().any(|v| v.principle == 5));
4807    }
4808
4809    #[test]
4810    fn records_construct_in_memory_and_fields_load() {
4811        let s = Store::open_in_memory().unwrap();
4812        let pd = s
4813            .put(&Node::RecordDef {
4814                name: "Point".into(),
4815                fields: vec![
4816                    ("x".into(), Type::Number),
4817                    ("y".into(), Type::Number),
4818                ],
4819            })
4820            .unwrap();
4821
4822        // mk_x(n) { step pt = Point { x: n, y: n + 1 }; yield pt.x }  -> n
4823        // mk_y(n) { ...; yield pt.y }                                 -> n + 1
4824        let field_fn = |s: &Store, name: &str, field: &str| -> NodeHash {
4825            let nref = s.put(&Node::Ref("n".into())).unwrap();
4826            let n2 = s.put(&Node::Ref("n".into())).unwrap();
4827            let one = s.put(&Node::Lit(1)).unwrap();
4828            let yexpr = s
4829                .put(&Node::BinOp {
4830                    op: crate::node::BinOp::Add,
4831                    lhs: n2,
4832                    rhs: one,
4833                })
4834                .unwrap();
4835            let rec = s
4836                .put(&Node::Record {
4837                    type_name: "Point".into(),
4838                    fields: vec![("x".into(), nref), ("y".into(), yexpr)],
4839                })
4840                .unwrap();
4841            let step = s
4842                .put(&Node::Step {
4843                    binding: "pt".into(),
4844                    value: rec,
4845                })
4846                .unwrap();
4847            let ptref = s.put(&Node::Ref("pt".into())).unwrap();
4848            let fx = s
4849                .put(&Node::Field {
4850                    base: ptref,
4851                    type_name: "Point".into(),
4852                    field: field.into(),
4853                })
4854                .unwrap();
4855            s.put(&Node::Function {
4856                name: name.into(),
4857                type_params: vec![],
4858                params: vec![Param {
4859                    name: "n".into(),
4860                    ty: Type::Number,
4861                    min_confidence: Confidence::External,
4862                }],
4863                produces: Produces {
4864                    ty: Type::Number,
4865                    confidence: Confidence::External,
4866                },
4867                requires: BTreeSet::new(),
4868                on_failure: vec![],
4869                body: vec![step],
4870                result: fx,
4871            })
4872            .unwrap()
4873        };
4874
4875        let mk_x = field_fn(&s, "mk_x", "x");
4876        let mk_y = field_fn(&s, "mk_y", "y");
4877        let m = s
4878            .put(&Node::Module {
4879                name: "m".into(),
4880                types: vec![pd],
4881                functions: vec![mk_x, mk_y],
4882            })
4883            .unwrap();
4884        let wasm = lower(&s, &m).unwrap();
4885        assert_eq!(run_i64(&wasm, "mk_x", &[7]).unwrap(), 7);
4886        assert_eq!(run_i64(&wasm, "mk_y", &[7]).unwrap(), 8);
4887    }
4888
4889    #[test]
4890    fn variants_construct_and_match_dispatches() {
4891        let s = Store::open_in_memory().unwrap();
4892        let vd = s
4893            .put(&Node::VariantDef {
4894                name: "Status".into(),
4895                cases: vec![
4896                    ("Active".into(), vec![]),
4897                    ("Closed".into(), vec![("reason".into(), Type::Number)]),
4898                ],
4899            })
4900            .unwrap();
4901
4902        // closed(n) { step s = Status.Closed { reason: n };
4903        //             yield match s { Active -> 0, Closed(reason) -> reason } }
4904        let n = s.put(&Node::Ref("n".into())).unwrap();
4905        let ctor = s
4906            .put(&Node::Variant {
4907                type_name: "Status".into(),
4908                case: "Closed".into(),
4909                fields: vec![("reason".into(), n)],
4910            })
4911            .unwrap();
4912        let step = s
4913            .put(&Node::Step {
4914                binding: "s".into(),
4915                value: ctor,
4916            })
4917            .unwrap();
4918        let sref = s.put(&Node::Ref("s".into())).unwrap();
4919        let zero = s.put(&Node::Lit(0)).unwrap();
4920        let rref = s.put(&Node::Ref("reason".into())).unwrap();
4921        let m_expr = s
4922            .put(&Node::Match {
4923                scrutinee: sref,
4924                type_name: "Status".into(),
4925                arms: vec![
4926                    crate::node::MatchArm {
4927                        case: "Active".into(),
4928                        bindings: vec![],
4929                        body: zero,
4930                    },
4931                    crate::node::MatchArm {
4932                        case: "Closed".into(),
4933                        bindings: vec!["reason".into()],
4934                        body: rref,
4935                    },
4936                ],
4937            })
4938            .unwrap();
4939        let closed = s
4940            .put(&Node::Function {
4941                name: "closed".into(),
4942                type_params: vec![],
4943                params: vec![Param {
4944                    name: "n".into(),
4945                    ty: Type::Number,
4946                    min_confidence: Confidence::External,
4947                }],
4948                produces: Produces {
4949                    ty: Type::Number,
4950                    confidence: Confidence::External,
4951                },
4952                requires: BTreeSet::new(),
4953                on_failure: vec![],
4954                body: vec![step],
4955                result: m_expr,
4956            })
4957            .unwrap();
4958        let m = s
4959            .put(&Node::Module {
4960                name: "m".into(),
4961                types: vec![vd],
4962                functions: vec![closed],
4963            })
4964            .unwrap();
4965        let wasm = lower(&s, &m).unwrap();
4966        // Closed(7) -> the match takes the Closed arm, binds reason=7.
4967        assert_eq!(run_i64(&wasm, "closed", &[7]).unwrap(), 7);
4968        assert_eq!(run_i64(&wasm, "closed", &[42]).unwrap(), 42);
4969    }
4970
4971    #[test]
4972    fn a_fallible_function_runs_and_fails() {
4973        let s = Store::open_in_memory().unwrap();
4974        // safe_div(a,b) on_failure DivByZero =
4975        //   if b == 0 then fail DivByZero else a / b end
4976        let a = s.put(&Node::Ref("a".into())).unwrap();
4977        let b = s.put(&Node::Ref("b".into())).unwrap();
4978        let zero = s.put(&Node::Lit(0)).unwrap();
4979        let is_zero = s
4980            .put(&Node::BinOp {
4981                op: crate::node::BinOp::Eq,
4982                lhs: b.clone(),
4983                rhs: zero,
4984            })
4985            .unwrap();
4986        let boom = s.put(&Node::Fail("DivByZero".into())).unwrap();
4987        let div = s
4988            .put(&Node::BinOp {
4989                op: crate::node::BinOp::Div,
4990                lhs: a,
4991                rhs: b,
4992            })
4993            .unwrap();
4994        let iff = s
4995            .put(&Node::If {
4996                cond: is_zero,
4997                then_branch: boom,
4998                else_branch: div,
4999            })
5000            .unwrap();
5001        let f = s
5002            .put(&Node::Function {
5003                name: "safe_div".into(),
5004                type_params: vec![],
5005                params: vec![
5006                    Param {
5007                        name: "a".into(),
5008                        ty: Type::Number,
5009                        min_confidence: Confidence::External,
5010                    },
5011                    Param {
5012                        name: "b".into(),
5013                        ty: Type::Number,
5014                        min_confidence: Confidence::External,
5015                    },
5016                ],
5017                produces: Produces {
5018                    ty: Type::Number,
5019                    confidence: Confidence::External,
5020                },
5021                requires: BTreeSet::new(),
5022                on_failure: vec!["DivByZero".into()],
5023                body: vec![],
5024                result: iff,
5025            })
5026            .unwrap();
5027        let m = s
5028            .put(&Node::Module {
5029                name: "m".into(),
5030                types: vec![],
5031                functions: vec![f],
5032            })
5033            .unwrap();
5034        let wasm = lower(&s, &m).unwrap();
5035        assert_eq!(run_fallible(&wasm, "safe_div", &[6, 2]).unwrap(), Ok(3));
5036        // DivByZero is the only failure -> tag 1.
5037        assert_eq!(run_fallible(&wasm, "safe_div", &[1, 0]).unwrap(), Err(1));
5038    }
5039
5040    #[test]
5041    fn handle_recovers_a_failure_and_passes_ok_through() {
5042        let s = Store::open_in_memory().unwrap();
5043
5044        let mkfn = |s: &Store, name: &str, result: NodeHash| -> NodeHash {
5045            s.put(&Node::Function {
5046                name: name.into(),
5047                type_params: vec![],
5048                params: vec![],
5049                produces: Produces {
5050                    ty: Type::Number,
5051                    confidence: Confidence::Structural,
5052                },
5053                requires: BTreeSet::new(),
5054                on_failure: vec!["Boom".into()],
5055                body: vec![],
5056                result,
5057            })
5058            .unwrap()
5059        };
5060        let boom = s.put(&Node::Fail("Boom".into())).unwrap();
5061        let risky = mkfn(&s, "risky", boom); // always fails
5062        let seven = s.put(&Node::Lit(7)).unwrap();
5063        let okfn = mkfn(&s, "okfn", seven); // fallible but returns ok
5064
5065        // An infallible caller that handles Boom -> 0.
5066        let handled_caller = |s: &Store, name: &str, callee: &str| -> NodeHash {
5067            let call = s
5068                .put(&Node::Call {
5069                    func: callee.into(),
5070                    args: vec![],
5071                })
5072                .unwrap();
5073            let zero = s.put(&Node::Lit(0)).unwrap();
5074            let h = s
5075                .put(&Node::Handle {
5076                    body: call,
5077                    handlers: vec![("Boom".into(), zero)],
5078                })
5079                .unwrap();
5080            let step = s
5081                .put(&Node::Step {
5082                    binding: "x".into(),
5083                    value: h,
5084                })
5085                .unwrap();
5086            let xref = s.put(&Node::Ref("x".into())).unwrap();
5087            s.put(&Node::Function {
5088                name: name.into(),
5089                type_params: vec![],
5090                params: vec![],
5091                produces: Produces {
5092                    ty: Type::Number,
5093                    confidence: Confidence::Structural,
5094                },
5095                requires: BTreeSet::new(),
5096                on_failure: vec![], // Boom is handled, so this is infallible
5097                body: vec![step],
5098                result: xref,
5099            })
5100            .unwrap()
5101        };
5102        let recovered = handled_caller(&s, "recovered", "risky");
5103        let passed = handled_caller(&s, "passed", "okfn");
5104
5105        let m = s
5106            .put(&Node::Module {
5107                name: "m".into(),
5108                types: vec![],
5109                functions: vec![risky, okfn, recovered, passed],
5110            })
5111            .unwrap();
5112        let wasm = lower(&s, &m).unwrap();
5113        assert_eq!(run_i64(&wasm, "recovered", &[]).unwrap(), 0); // Boom -> 0
5114        assert_eq!(run_i64(&wasm, "passed", &[]).unwrap(), 7); // ok passes through
5115    }
5116
5117    #[test]
5118    fn a_generic_function_runs_at_a_scalar_and_a_pointer_type() {
5119        let s = Store::open_in_memory().unwrap();
5120        let pd = s
5121            .put(&Node::RecordDef {
5122                name: "Box".into(),
5123                fields: vec![("v".into(), Type::Number)],
5124            })
5125            .unwrap();
5126
5127        // identity<T>(x: T) -> T { yield x }
5128        let x = s.put(&Node::Ref("x".into())).unwrap();
5129        let identity = s
5130            .put(&Node::Function {
5131                name: "identity".into(),
5132                type_params: vec!["T".into()],
5133                params: vec![Param {
5134                    name: "x".into(),
5135                    ty: Type::Var("T".into()),
5136                    min_confidence: Confidence::Structural,
5137                }],
5138                produces: Produces {
5139                    ty: Type::Var("T".into()),
5140                    confidence: Confidence::Structural,
5141                },
5142                requires: BTreeSet::new(),
5143                on_failure: vec![],
5144                body: vec![],
5145                result: x,
5146            })
5147            .unwrap();
5148
5149        // use_num() -> identity(7)                         (T = Number)
5150        let seven = s.put(&Node::Lit(7)).unwrap();
5151        let cn = s
5152            .put(&Node::Call {
5153                func: "identity".into(),
5154                args: vec![seven],
5155            })
5156            .unwrap();
5157        let use_num = mono_fn(&s, "use_num", cn);
5158
5159        // use_box() -> step b = identity(Box{v:5}); b.v     (T = Box, a ptr)
5160        let five = s.put(&Node::Lit(5)).unwrap();
5161        let rec = s
5162            .put(&Node::Record {
5163                type_name: "Box".into(),
5164                fields: vec![("v".into(), five)],
5165            })
5166            .unwrap();
5167        let cb = s
5168            .put(&Node::Call {
5169                func: "identity".into(),
5170                args: vec![rec],
5171            })
5172            .unwrap();
5173        let step = s
5174            .put(&Node::Step {
5175                binding: "b".into(),
5176                value: cb,
5177            })
5178            .unwrap();
5179        let bref = s.put(&Node::Ref("b".into())).unwrap();
5180        let fv = s
5181            .put(&Node::Field {
5182                base: bref,
5183                type_name: "Box".into(),
5184                field: "v".into(),
5185            })
5186            .unwrap();
5187        let use_box = s
5188            .put(&Node::Function {
5189                name: "use_box".into(),
5190                type_params: vec![],
5191                params: vec![],
5192                produces: Produces {
5193                    ty: Type::Number,
5194                    confidence: Confidence::Structural,
5195                },
5196                requires: BTreeSet::new(),
5197                on_failure: vec![],
5198                body: vec![step],
5199                result: fv,
5200            })
5201            .unwrap();
5202
5203        let m = s
5204            .put(&Node::Module {
5205                name: "m".into(),
5206                types: vec![pd],
5207                functions: vec![identity, use_num, use_box],
5208            })
5209            .unwrap();
5210        let wasm = lower(&s, &m).unwrap();
5211        // The same lowered `identity` works for an i64 scalar and an i64
5212        // record pointer — type erasure is sound under uniform i64.
5213        assert_eq!(run_i64(&wasm, "use_num", &[]).unwrap(), 7);
5214        assert_eq!(run_i64(&wasm, "use_box", &[]).unwrap(), 5);
5215    }
5216
5217    /// A monomorphic, infallible, pure `() -> Number` wrapper around `result`.
5218    fn mono_fn(s: &Store, name: &str, result: NodeHash) -> NodeHash {
5219        s.put(&Node::Function {
5220            name: name.into(),
5221            type_params: vec![],
5222            params: vec![],
5223            produces: Produces {
5224                ty: Type::Number,
5225                confidence: Confidence::Structural,
5226            },
5227            requires: BTreeSet::new(),
5228            on_failure: vec![],
5229            body: vec![],
5230            result,
5231        })
5232        .unwrap()
5233    }
5234
5235    #[test]
5236    fn strings_lower_and_str_len_runs() {
5237        let s = Store::open_in_memory().unwrap();
5238        let hello = s.put(&Node::Str("hello".into())).unwrap();
5239        let sl = s.put(&Node::StrLen(hello)).unwrap();
5240        let size = mono_fn(&s, "size", sl);
5241
5242        let empty = s.put(&Node::Str(String::new())).unwrap();
5243        let sl0 = s.put(&Node::StrLen(empty)).unwrap();
5244        let zero = mono_fn(&s, "zero", sl0);
5245
5246        let m = s
5247            .put(&Node::Module {
5248                name: "m".into(),
5249                types: vec![],
5250                functions: vec![size, zero],
5251            })
5252            .unwrap();
5253        let wasm = lower(&s, &m).unwrap();
5254        assert_eq!(run_i64(&wasm, "size", &[]).unwrap(), 5);
5255        assert_eq!(run_i64(&wasm, "zero", &[]).unwrap(), 0);
5256    }
5257
5258    #[test]
5259    fn lists_construct_and_index_and_measure() {
5260        let s = Store::open_in_memory().unwrap();
5261        let list = |s: &Store| -> NodeHash {
5262            let a = s.put(&Node::Lit(10)).unwrap();
5263            let b = s.put(&Node::Lit(20)).unwrap();
5264            let cc = s.put(&Node::Lit(30)).unwrap();
5265            s.put(&Node::List(vec![a, b, cc])).unwrap()
5266        };
5267        let two = s.put(&Node::Lit(2)).unwrap();
5268        let third = s
5269            .put(&Node::ListGet {
5270                list: list(&s),
5271                index: two,
5272            })
5273            .unwrap();
5274        let get_third = mono_fn(&s, "third", third);
5275
5276        let len = s.put(&Node::ListLen(list(&s))).unwrap();
5277        let len3 = mono_fn(&s, "len3", len);
5278
5279        let m = s
5280            .put(&Node::Module {
5281                name: "m".into(),
5282                types: vec![],
5283                functions: vec![get_third, len3],
5284            })
5285            .unwrap();
5286        let wasm = lower(&s, &m).unwrap();
5287        assert_eq!(run_i64(&wasm, "third", &[]).unwrap(), 30);
5288        assert_eq!(run_i64(&wasm, "len3", &[]).unwrap(), 3);
5289    }
5290
5291    #[test]
5292    fn maps_construct_lookup_and_measure() {
5293        let s = Store::open_in_memory().unwrap();
5294        let map = |s: &Store| -> NodeHash {
5295            let k1 = s.put(&Node::Lit(1)).unwrap();
5296            let v1 = s.put(&Node::Lit(10)).unwrap();
5297            let k2 = s.put(&Node::Lit(2)).unwrap();
5298            let v2 = s.put(&Node::Lit(20)).unwrap();
5299            let k3 = s.put(&Node::Lit(3)).unwrap();
5300            let v3 = s.put(&Node::Lit(30)).unwrap();
5301            s.put(&Node::Map(vec![(k1, v1), (k2, v2), (k3, v3)]))
5302                .unwrap()
5303        };
5304        let two = s.put(&Node::Lit(2)).unwrap();
5305        let hit = s
5306            .put(&Node::MapGet {
5307                map: map(&s),
5308                key: two,
5309            })
5310            .unwrap();
5311        let lookup = mono_fn(&s, "lookup", hit);
5312
5313        let nine = s.put(&Node::Lit(9)).unwrap();
5314        let miss = s
5315            .put(&Node::MapGet {
5316                map: map(&s),
5317                key: nine,
5318            })
5319            .unwrap();
5320        let missing = mono_fn(&s, "missing", miss);
5321
5322        let sz = s.put(&Node::MapLen(map(&s))).unwrap();
5323        let size = mono_fn(&s, "size", sz);
5324
5325        let m = s
5326            .put(&Node::Module {
5327                name: "m".into(),
5328                types: vec![],
5329                functions: vec![lookup, missing, size],
5330            })
5331            .unwrap();
5332        let wasm = lower(&s, &m).unwrap();
5333        assert_eq!(run_i64(&wasm, "lookup", &[]).unwrap(), 20); // key 2 -> 20
5334        assert_eq!(run_i64(&wasm, "missing", &[]).unwrap(), 0); // absent -> 0
5335        assert_eq!(run_i64(&wasm, "size", &[]).unwrap(), 3);
5336    }
5337
5338    #[test]
5339    fn the_log_effect_runs_and_is_pass_through() {
5340        let s = Store::open_in_memory().unwrap();
5341        let n = s.put(&Node::Ref("n".into())).unwrap();
5342        let logged = s.put(&Node::Log(n)).unwrap();
5343        let mut log_eff = BTreeSet::new();
5344        log_eff.insert(crate::ty::Effect::Log);
5345        let f = s
5346            .put(&Node::Function {
5347                name: "trace".into(),
5348                type_params: vec![],
5349                params: vec![Param {
5350                    name: "n".into(),
5351                    ty: Type::Number,
5352                    min_confidence: Confidence::External,
5353                }],
5354                produces: Produces {
5355                    ty: Type::Number,
5356                    confidence: Confidence::External,
5357                },
5358                requires: log_eff,
5359                on_failure: vec![],
5360                body: vec![],
5361                result: logged,
5362            })
5363            .unwrap();
5364        let m = module(&s, vec![f]);
5365        // Checker accepts it (Log is declared).
5366        assert!(crate::check::Checker::new(&s).check(&m).unwrap().ok());
5367        let wasm = lower(&s, &m).unwrap();
5368        // log(n) passes n through.
5369        assert_eq!(run_i64(&wasm, "trace", &[99]).unwrap(), 99);
5370    }
5371
5372    #[test]
5373    fn logging_without_declaring_log_is_a_principle_5_violation() {
5374        let s = Store::open_in_memory().unwrap();
5375        let lit = s.put(&Node::Lit(1)).unwrap();
5376        let logged = s.put(&Node::Log(lit)).unwrap();
5377        let f = s
5378            .put(&Node::Function {
5379                name: "bad".into(),
5380                type_params: vec![],
5381                params: vec![],
5382                produces: Produces {
5383                    ty: Type::Number,
5384                    confidence: Confidence::Structural,
5385                },
5386                requires: BTreeSet::new(), // does NOT declare Log
5387                on_failure: vec![],
5388                body: vec![],
5389                result: logged,
5390            })
5391            .unwrap();
5392        let r = crate::check::Checker::new(&s).check(&f).unwrap();
5393        assert!(r.violations.iter().any(|v| v.principle == 5));
5394    }
5395
5396    #[test]
5397    fn the_rand_effect_runs() {
5398        let s = Store::open_in_memory().unwrap();
5399        let r = s.put(&Node::Rand).unwrap();
5400        let mut rand_eff = BTreeSet::new();
5401        rand_eff.insert(crate::ty::Effect::Rand);
5402        let f = s
5403            .put(&Node::Function {
5404                name: "pick".into(),
5405                type_params: vec![],
5406                params: vec![],
5407                produces: Produces {
5408                    ty: Type::Number,
5409                    confidence: Confidence::External,
5410                },
5411                requires: rand_eff,
5412                on_failure: vec![],
5413                body: vec![],
5414                result: r,
5415            })
5416            .unwrap();
5417        let m = module(&s, vec![f]);
5418        assert!(crate::check::Checker::new(&s).check(&m).unwrap().ok());
5419        let wasm = lower(&s, &m).unwrap();
5420        // Real OS entropy: independent draws are (overwhelmingly) not all
5421        // equal — this would fail for any constant/fake RNG.
5422        let draws: std::collections::HashSet<i64> = (0..5)
5423            .map(|_| run_i64(&wasm, "pick", &[]).unwrap())
5424            .collect();
5425        assert!(draws.len() > 1, "rand returned a constant: {draws:?}");
5426    }
5427
5428    #[test]
5429    fn a_mutable_cell_creates_sets_and_reads() {
5430        let s = Store::open_in_memory().unwrap();
5431        // counter() requires Mut -> Number {
5432        //   step c = cell(10); step _ = cell_set(c, 20); yield cell_get(c) }
5433        let ten = s.put(&Node::Lit(10)).unwrap();
5434        let cell = s.put(&Node::MutNew(ten)).unwrap();
5435        let step_c = s
5436            .put(&Node::Step {
5437                binding: "c".into(),
5438                value: cell,
5439            })
5440            .unwrap();
5441        let cref1 = s.put(&Node::Ref("c".into())).unwrap();
5442        let twenty = s.put(&Node::Lit(20)).unwrap();
5443        let set = s
5444            .put(&Node::MutSet {
5445                cell: cref1,
5446                value: twenty,
5447            })
5448            .unwrap();
5449        let step_set = s
5450            .put(&Node::Step {
5451                binding: "_".into(),
5452                value: set,
5453            })
5454            .unwrap();
5455        let cref2 = s.put(&Node::Ref("c".into())).unwrap();
5456        let getc = s.put(&Node::MutGet(cref2)).unwrap();
5457        let mut mut_eff = BTreeSet::new();
5458        mut_eff.insert(crate::ty::Effect::Mut);
5459        let f = s
5460            .put(&Node::Function {
5461                name: "counter".into(),
5462                type_params: vec![],
5463                params: vec![],
5464                produces: Produces {
5465                    ty: Type::Number,
5466                    confidence: Confidence::Structural,
5467                },
5468                requires: mut_eff,
5469                on_failure: vec![],
5470                body: vec![step_c, step_set],
5471                result: getc,
5472            })
5473            .unwrap();
5474        let m = module(&s, vec![f]);
5475        assert!(
5476            crate::check::Checker::new(&s).check(&m).unwrap().ok(),
5477            "checker rejected the cell program"
5478        );
5479        let wasm = lower(&s, &m).unwrap();
5480        // Created at 10, mutated to 20, read back 20.
5481        assert_eq!(run_i64(&wasm, "counter", &[]).unwrap(), 20);
5482    }
5483
5484    #[test]
5485    fn mutating_without_declaring_mut_is_a_principle_5_violation() {
5486        let s = Store::open_in_memory().unwrap();
5487        let one = s.put(&Node::Lit(1)).unwrap();
5488        let cell = s.put(&Node::MutNew(one)).unwrap();
5489        let f = s
5490            .put(&Node::Function {
5491                name: "bad".into(),
5492                type_params: vec![],
5493                params: vec![],
5494                produces: Produces {
5495                    ty: Type::Cell(Box::new(Type::Number)),
5496                    confidence: Confidence::Structural,
5497                },
5498                requires: BTreeSet::new(), // does NOT declare Mut
5499                on_failure: vec![],
5500                body: vec![],
5501                result: cell,
5502            })
5503            .unwrap();
5504        let r = crate::check::Checker::new(&s).check(&f).unwrap();
5505        assert!(r.violations.iter().any(|v| v.principle == 5));
5506    }
5507
5508    #[test]
5509    fn the_disk_effect_writes_and_reads_a_real_file() {
5510        let s = Store::open_in_memory().unwrap();
5511        let path = std::env::temp_dir()
5512            .join(format!("cairn_disk_{}.txt", std::process::id()))
5513            .to_string_lossy()
5514            .into_owned();
5515        let _ = std::fs::remove_file(&path);
5516
5517        // io() requires Disk -> Number {
5518        //   step _ = disk_write(PATH, "hello"); yield disk_read(PATH) }
5519        let p1 = s.put(&Node::Str(path.clone())).unwrap();
5520        let content = s.put(&Node::Str("hello".into())).unwrap();
5521        let w = s
5522            .put(&Node::DiskWrite {
5523                path: p1,
5524                content,
5525            })
5526            .unwrap();
5527        let step = s
5528            .put(&Node::Step {
5529                binding: "_".into(),
5530                value: w,
5531            })
5532            .unwrap();
5533        // disk_read returns the file's *contents* now; verify them exactly.
5534        let p2 = s.put(&Node::Str(path.clone())).unwrap();
5535        let rd = s.put(&Node::DiskRead(p2)).unwrap();
5536        let expect = s.put(&Node::Str("hello".into())).unwrap();
5537        let rd = s.put(&Node::StrEq(rd, expect)).unwrap();
5538        let mut disk = BTreeSet::new();
5539        disk.insert(crate::ty::Effect::Disk);
5540        let f = s
5541            .put(&Node::Function {
5542                name: "io".into(),
5543                type_params: vec![],
5544                params: vec![],
5545                produces: Produces {
5546                    ty: Type::Bool, // str_eq result
5547                    confidence: Confidence::External,
5548                },
5549                requires: disk,
5550                on_failure: vec![],
5551                body: vec![step],
5552                result: rd,
5553            })
5554            .unwrap();
5555        let m = module(&s, vec![f]);
5556        assert!(crate::check::Checker::new(&s).check(&m).unwrap().ok());
5557        let wasm = lower(&s, &m).unwrap();
5558        // Writes "hello", reads it back as a String via host→wasm
5559        // allocation, and str_eq confirms the exact contents (-> 1).
5560        let got = run_i64(&wasm, "io", &[]).unwrap();
5561        let _ = std::fs::remove_file(&path);
5562        assert_eq!(got, 1);
5563    }
5564
5565    // The Net host boundary is covered by the real, hermetic loopback
5566    // test `real_net_get_returns_the_http_status` (no stub form exists).
5567
5568    #[test]
5569    fn net_without_declaring_it_is_a_principle_5_violation() {
5570        let s = Store::open_in_memory().unwrap();
5571        let url = s.put(&Node::Str("u".into())).unwrap();
5572        let g = s.put(&Node::NetGet(url)).unwrap();
5573        let f = s
5574            .put(&Node::Function {
5575                name: "bad".into(),
5576                type_params: vec![],
5577                params: vec![],
5578                produces: Produces {
5579                    ty: Type::Number,
5580                    confidence: Confidence::External,
5581                },
5582                requires: BTreeSet::new(), // does NOT declare Net
5583                on_failure: vec![],
5584                body: vec![],
5585                result: g,
5586            })
5587            .unwrap();
5588        let r = crate::check::Checker::new(&s).check(&f).unwrap();
5589        assert!(r.violations.iter().any(|v| v.principle == 5));
5590    }
5591
5592    #[test]
5593    fn the_db_effect_returns_a_string_result() {
5594        let s = Store::open_in_memory().unwrap();
5595        // query() -> Bool { yield str_eq(db_query("SELECT 1"), "1") }
5596        // Real SQLite: `SELECT 1` is one row, one column, value 1.
5597        let sql = s.put(&Node::Str("SELECT 1".into())).unwrap();
5598        let q = s
5599            .put(&Node::DbQuery {
5600                sql,
5601                params: s.put(&Node::ListEmpty { elem: Type::String }).unwrap(),
5602            })
5603            .unwrap();
5604        let expect = s.put(&Node::Str("1".into())).unwrap();
5605        let eq = s.put(&Node::StrEq(q, expect)).unwrap();
5606        let mut db = BTreeSet::new();
5607        db.insert(crate::ty::Effect::Db);
5608        let f = s
5609            .put(&Node::Function {
5610                name: "query".into(),
5611                type_params: vec![],
5612                params: vec![],
5613                produces: Produces {
5614                    ty: Type::Bool,
5615                    // str_eq confidence = min(db_query=persisted,
5616                    // literal=structural) = structural.
5617                    confidence: Confidence::Structural,
5618                },
5619                requires: db,
5620                on_failure: vec![],
5621                body: vec![],
5622                result: eq,
5623            })
5624            .unwrap();
5625        let m = module(&s, vec![f]);
5626        assert!(crate::check::Checker::new(&s).check(&m).unwrap().ok());
5627        let wasm = lower(&s, &m).unwrap();
5628        // The real query result round-trips out of the Db effect via
5629        // host→wasm allocation, and str_eq confirms it exactly.
5630        assert_eq!(run_i64(&wasm, "query", &[]).unwrap(), 1);
5631    }
5632
5633    #[test]
5634    fn string_concat_slice_and_eq_run_correctly() {
5635        let s = Store::open_in_memory().unwrap();
5636        let lit = |s: &Store, t: &str| s.put(&Node::Str(t.into())).unwrap();
5637
5638        // str_eq(str_concat("ab","c"), "abc")  -> 1   (concat content)
5639        let cc = s
5640            .put(&Node::StrConcat(lit(&s, "ab"), lit(&s, "c")))
5641            .unwrap();
5642        let cc_eq = s.put(&Node::StrEq(cc, lit(&s, "abc"))).unwrap();
5643        let concat_ok = mono_fn(&s, "concat_ok", cc_eq);
5644
5645        // str_eq(str_slice("hello",1,3), "ell")  -> 1  (slice content)
5646        let one = s.put(&Node::Lit(1)).unwrap();
5647        let three = s.put(&Node::Lit(3)).unwrap();
5648        let sl = s
5649            .put(&Node::StrSlice {
5650                s: lit(&s, "hello"),
5651                start: one,
5652                len: three,
5653            })
5654            .unwrap();
5655        let sl_eq = s.put(&Node::StrEq(sl, lit(&s, "ell"))).unwrap();
5656        let slice_ok = mono_fn(&s, "slice_ok", sl_eq);
5657
5658        // str_eq("abc","abd") -> 0  (inequality)
5659        let ne = s
5660            .put(&Node::StrEq(lit(&s, "abc"), lit(&s, "abd")))
5661            .unwrap();
5662        let neq = mono_fn(&s, "neq", ne);
5663
5664        let m = s
5665            .put(&Node::Module {
5666                name: "m".into(),
5667                types: vec![],
5668                functions: vec![concat_ok, slice_ok, neq],
5669            })
5670            .unwrap();
5671        let wasm = lower(&s, &m).unwrap();
5672        assert_eq!(run_i64(&wasm, "concat_ok", &[]).unwrap(), 1);
5673        assert_eq!(run_i64(&wasm, "slice_ok", &[]).unwrap(), 1);
5674        assert_eq!(run_i64(&wasm, "neq", &[]).unwrap(), 0);
5675    }
5676
5677    #[test]
5678    fn str_contains_runs() {
5679        let s = Store::open_in_memory().unwrap();
5680        let lit = |s: &Store, t: &str| s.put(&Node::Str(t.into())).unwrap();
5681        let mk = |s: &Store, name: &str, h: &str, n: &str| -> NodeHash {
5682            let node = s
5683                .put(&Node::StrContains {
5684                    haystack: lit(s, h),
5685                    needle: lit(s, n),
5686                })
5687                .unwrap();
5688            mono_fn(s, name, node)
5689        };
5690        let hit = mk(&s, "hit", "hello world", "o w"); // 1
5691        let miss = mk(&s, "miss", "hello", "xyz"); // 0
5692        let empty = mk(&s, "empty", "abc", ""); // 1 (empty needle)
5693        let edge = mk(&s, "edge", "abc", "abcd"); // 0 (needle longer)
5694
5695        let m = s
5696            .put(&Node::Module {
5697                name: "m".into(),
5698                types: vec![],
5699                functions: vec![hit, miss, empty, edge],
5700            })
5701            .unwrap();
5702        let wasm = lower(&s, &m).unwrap();
5703        assert_eq!(run_i64(&wasm, "hit", &[]).unwrap(), 1);
5704        assert_eq!(run_i64(&wasm, "miss", &[]).unwrap(), 0);
5705        assert_eq!(run_i64(&wasm, "empty", &[]).unwrap(), 1);
5706        assert_eq!(run_i64(&wasm, "edge", &[]).unwrap(), 0);
5707    }
5708
5709    #[test]
5710    fn cairn_as_an_http_handler_round_trips_request_and_response() {
5711        let s = Store::open_in_memory().unwrap();
5712        // handle(req: String@external) -> String@external {
5713        //   yield str_concat("echo:", req) }
5714        let prefix = s.put(&Node::Str("echo:".into())).unwrap();
5715        let req = s.put(&Node::Ref("req".into())).unwrap();
5716        let body = s.put(&Node::StrConcat(prefix, req)).unwrap();
5717        let handle = s
5718            .put(&Node::Function {
5719                name: "handle".into(),
5720                type_params: vec![],
5721                params: vec![Param {
5722                    name: "req".into(),
5723                    ty: Type::String,
5724                    min_confidence: Confidence::External,
5725                }],
5726                produces: Produces {
5727                    ty: Type::String,
5728                    confidence: Confidence::External,
5729                },
5730                requires: BTreeSet::new(),
5731                on_failure: vec![],
5732                body: vec![],
5733                result: body,
5734            })
5735            .unwrap();
5736        let m = module(&s, vec![handle]);
5737        assert!(crate::check::Checker::new(&s).check(&m).unwrap().ok());
5738        let wasm = lower(&s, &m).unwrap();
5739        // Request String in (host→wasm), handler runs, response String out.
5740        assert_eq!(
5741            serve_once(&wasm, "handle", "/hello").unwrap(),
5742            "echo:/hello"
5743        );
5744        assert_eq!(serve_once(&wasm, "handle", "").unwrap(), "echo:");
5745    }
5746
5747    /// Tier 2, first slice: a tiny web app in Cairn — path routing,
5748    /// server-rendered HTML, a Db-backed page — driven through serve_once.
5749    #[test]
5750    fn a_minimal_cairn_web_app_routes_and_renders_html() {
5751        let s = Store::open_in_memory().unwrap();
5752        let str_lit = |t: &str| -> NodeHash { s.put(&Node::Str(t.into())).unwrap() };
5753        let req_param = || Param {
5754            name: "req".into(),
5755            ty: Type::String,
5756            min_confidence: Confidence::External,
5757        };
5758
5759        // home(req) -> "<h1>Cairn</h1>"
5760        let home_body = str_lit("<h1>Cairn</h1>");
5761        let home = s
5762            .put(&Node::Function {
5763                name: "home".into(),
5764                type_params: vec![],
5765                params: vec![req_param()],
5766                produces: Produces {
5767                    ty: Type::String,
5768                    confidence: Confidence::Structural,
5769                },
5770                requires: BTreeSet::new(),
5771                on_failure: vec![],
5772                body: vec![],
5773                result: home_body,
5774            })
5775            .unwrap();
5776
5777        // customers(req) requires Db ->
5778        //   "<ul>" ++ db_query("SELECT 'Acme'") ++ "</ul>"
5779        // (a self-contained real query — no schema needed).
5780        let q = s
5781            .put(&Node::DbQuery {
5782                sql: str_lit("SELECT 'Acme'"),
5783                params: s.put(&Node::ListEmpty { elem: Type::String }).unwrap(),
5784            })
5785            .unwrap();
5786        let inner = s.put(&Node::StrConcat(q, str_lit("</ul>"))).unwrap();
5787        let cust_body = s
5788            .put(&Node::StrConcat(str_lit("<ul>"), inner))
5789            .unwrap();
5790        let mut db = BTreeSet::new();
5791        db.insert(crate::ty::Effect::Db);
5792        let customers = s
5793            .put(&Node::Function {
5794                name: "customers".into(),
5795                type_params: vec![],
5796                params: vec![req_param()],
5797                produces: Produces {
5798                    ty: Type::String,
5799                    confidence: Confidence::Structural,
5800                },
5801                requires: db.clone(),
5802                on_failure: vec![],
5803                body: vec![],
5804                result: cust_body,
5805            })
5806            .unwrap();
5807
5808        // router(req) requires Db ->
5809        //   if req == "/" then home(req)
5810        //   else if req contains "/customers" then customers(req)
5811        //   else "<h1>404</h1>"
5812        let is_root = s
5813            .put(&Node::StrEq(
5814                s.put(&Node::Ref("req".into())).unwrap(),
5815                str_lit("/"),
5816            ))
5817            .unwrap();
5818        let call_home = s
5819            .put(&Node::Call {
5820                func: "home".into(),
5821                args: vec![s.put(&Node::Ref("req".into())).unwrap()],
5822            })
5823            .unwrap();
5824        let has_cust = s
5825            .put(&Node::StrContains {
5826                haystack: s.put(&Node::Ref("req".into())).unwrap(),
5827                needle: str_lit("/customers"),
5828            })
5829            .unwrap();
5830        let call_cust = s
5831            .put(&Node::Call {
5832                func: "customers".into(),
5833                args: vec![s.put(&Node::Ref("req".into())).unwrap()],
5834            })
5835            .unwrap();
5836        let inner_if = s
5837            .put(&Node::If {
5838                cond: has_cust,
5839                then_branch: call_cust,
5840                else_branch: str_lit("<h1>404</h1>"),
5841            })
5842            .unwrap();
5843        let route = s
5844            .put(&Node::If {
5845                cond: is_root,
5846                then_branch: call_home,
5847                else_branch: inner_if,
5848            })
5849            .unwrap();
5850        let router = s
5851            .put(&Node::Function {
5852                name: "router".into(),
5853                type_params: vec![],
5854                params: vec![req_param()],
5855                produces: Produces {
5856                    ty: Type::String,
5857                    confidence: Confidence::External,
5858                },
5859                requires: db,
5860                on_failure: vec![],
5861                body: vec![],
5862                result: route,
5863            })
5864            .unwrap();
5865
5866        let m = s
5867            .put(&Node::Module {
5868                name: "app".into(),
5869                types: vec![],
5870                functions: vec![home, customers, router],
5871            })
5872            .unwrap();
5873        // The whole app type-checks (effects, confidence, types).
5874        assert!(
5875            crate::check::Checker::new(&s).check(&m).unwrap().ok(),
5876            "the web app did not type-check"
5877        );
5878        let wasm = lower(&s, &m).unwrap();
5879        assert_eq!(
5880            serve_once(&wasm, "router", "/").unwrap(),
5881            "<h1>Cairn</h1>"
5882        );
5883        assert_eq!(
5884            serve_once(&wasm, "router", "/customers").unwrap(),
5885            "<ul>Acme</ul>"
5886        );
5887        assert_eq!(
5888            serve_once(&wasm, "router", "/nope").unwrap(),
5889            "<h1>404</h1>"
5890        );
5891    }
5892
5893    #[test]
5894    fn a_function_can_call_itself_recursively() {
5895        // sum(n) = if n == 0 then 0 else n + sum(n - 1).
5896        // A self-call: impossible before two-pass lowering, since the
5897        // function's own id was not registered until after its body.
5898        let s = Store::open_in_memory().unwrap();
5899        let nref = s.put(&Node::Ref("n".into())).unwrap();
5900        let zero = s.put(&Node::Lit(0)).unwrap();
5901        let cond = s
5902            .put(&Node::BinOp {
5903                op: crate::node::BinOp::Eq,
5904                lhs: nref.clone(),
5905                rhs: zero.clone(),
5906            })
5907            .unwrap();
5908        let one = s.put(&Node::Lit(1)).unwrap();
5909        let nm1 = s
5910            .put(&Node::BinOp {
5911                op: crate::node::BinOp::Sub,
5912                lhs: nref.clone(),
5913                rhs: one,
5914            })
5915            .unwrap();
5916        let rec = s
5917            .put(&Node::Call {
5918                func: "sum".into(),
5919                args: vec![nm1],
5920            })
5921            .unwrap();
5922        let n_plus_rec = s
5923            .put(&Node::BinOp {
5924                op: crate::node::BinOp::Add,
5925                lhs: nref,
5926                rhs: rec,
5927            })
5928            .unwrap();
5929        let body = s
5930            .put(&Node::If {
5931                cond,
5932                then_branch: zero,
5933                else_branch: n_plus_rec,
5934            })
5935            .unwrap();
5936        let sum = func(&s, "sum", &["n"], vec![], body);
5937        let m = module(&s, vec![sum]);
5938        let wasm = lower(&s, &m).unwrap();
5939        assert_eq!(run_i64(&wasm, "sum", &[5]).unwrap(), 15); // 5+4+3+2+1
5940        assert_eq!(run_i64(&wasm, "sum", &[0]).unwrap(), 0);
5941    }
5942
5943    #[test]
5944    fn mutually_recursive_functions_lower_regardless_of_order() {
5945        // is_even(n) = if n == 0 then 1 else is_odd(n - 1)
5946        // is_odd(n)  = if n == 0 then 0 else is_even(n - 1)
5947        let s = Store::open_in_memory().unwrap();
5948        let mk = |name: &str, other: &str, base: i64| -> NodeHash {
5949            let nref = s.put(&Node::Ref("n".into())).unwrap();
5950            let zero = s.put(&Node::Lit(0)).unwrap();
5951            let cond = s
5952                .put(&Node::BinOp {
5953                    op: crate::node::BinOp::Eq,
5954                    lhs: nref.clone(),
5955                    rhs: zero,
5956                })
5957                .unwrap();
5958            let base_lit = s.put(&Node::Lit(base)).unwrap();
5959            let one = s.put(&Node::Lit(1)).unwrap();
5960            let nm1 = s
5961                .put(&Node::BinOp {
5962                    op: crate::node::BinOp::Sub,
5963                    lhs: nref,
5964                    rhs: one,
5965                })
5966                .unwrap();
5967            let rec = s
5968                .put(&Node::Call {
5969                    func: other.into(),
5970                    args: vec![nm1],
5971                })
5972                .unwrap();
5973            let body = s
5974                .put(&Node::If {
5975                    cond,
5976                    then_branch: base_lit,
5977                    else_branch: rec,
5978                })
5979                .unwrap();
5980            func(&s, name, &["n"], vec![], body)
5981        };
5982        let is_even = mk("is_even", "is_odd", 1);
5983        let is_odd = mk("is_odd", "is_even", 0);
5984        // Caller `is_even` is listed BEFORE callee `is_odd` — a forward +
5985        // mutual reference the old single-pass lowering could not resolve.
5986        let m = module(&s, vec![is_even, is_odd]);
5987        let wasm = lower(&s, &m).unwrap();
5988        assert_eq!(run_i64(&wasm, "is_even", &[10]).unwrap(), 1);
5989        assert_eq!(run_i64(&wasm, "is_even", &[7]).unwrap(), 0);
5990        assert_eq!(run_i64(&wasm, "is_odd", &[7]).unwrap(), 1);
5991    }
5992
5993    #[test]
5994    fn a_typed_element_tree_renders_to_html() {
5995        // The Tier-2 view layer: HTML is a typed Cairn value, not string
5996        // concatenation. `Element` is a recursive variant; `render_html`
5997        // and `render_kids` are mutually recursive and listed before their
5998        // callees — only lowerable with two-pass lowering.
5999        let s = Store::open_in_memory().unwrap();
6000        let str_lit = |t: &str| -> NodeHash { s.put(&Node::Str(t.into())).unwrap() };
6001        let cat = |a: NodeHash, b: NodeHash| -> NodeHash {
6002            s.put(&Node::StrConcat(a, b)).unwrap()
6003        };
6004        let elem_ty = Type::Named("Element".into());
6005        let str_out = || Produces {
6006            ty: Type::String,
6007            confidence: Confidence::External,
6008        };
6009        let param = |name: &str, ty: Type| Param {
6010            name: name.into(),
6011            ty,
6012            min_confidence: Confidence::External,
6013        };
6014
6015        // type Element = variant {
6016        //   Text(content: String),
6017        //   El(tag: String, kids: List<Element>),
6018        // }
6019        let element_def = s
6020            .put(&Node::VariantDef {
6021                name: "Element".into(),
6022                cases: vec![
6023                    ("Text".into(), vec![("content".into(), Type::String)]),
6024                    (
6025                        "El".into(),
6026                        vec![
6027                            ("tag".into(), Type::String),
6028                            ("kids".into(), Type::List(Box::new(elem_ty.clone()))),
6029                        ],
6030                    ),
6031                ],
6032            })
6033            .unwrap();
6034
6035        // render_kids(kids: List<Element>, i: Number) -> String
6036        //   = if i == len(kids) then ""
6037        //     else render_html(kids[i]) ++ render_kids(kids, i + 1)
6038        let kids_ref = s.put(&Node::Ref("kids".into())).unwrap();
6039        let i_ref = s.put(&Node::Ref("i".into())).unwrap();
6040        let len_step = s
6041            .put(&Node::Step {
6042                binding: "n".into(),
6043                value: s.put(&Node::ListLen(kids_ref.clone())).unwrap(),
6044            })
6045            .unwrap();
6046        let at_end = s
6047            .put(&Node::BinOp {
6048                op: crate::node::BinOp::Eq,
6049                lhs: i_ref.clone(),
6050                rhs: s.put(&Node::Ref("n".into())).unwrap(),
6051            })
6052            .unwrap();
6053        let head = s
6054            .put(&Node::Call {
6055                func: "render_html".into(),
6056                args: vec![s
6057                    .put(&Node::ListGet {
6058                        list: kids_ref.clone(),
6059                        index: i_ref.clone(),
6060                    })
6061                    .unwrap()],
6062            })
6063            .unwrap();
6064        let tail = s
6065            .put(&Node::Call {
6066                func: "render_kids".into(),
6067                args: vec![
6068                    kids_ref,
6069                    s.put(&Node::BinOp {
6070                        op: crate::node::BinOp::Add,
6071                        lhs: i_ref,
6072                        rhs: s.put(&Node::Lit(1)).unwrap(),
6073                    })
6074                    .unwrap(),
6075                ],
6076            })
6077            .unwrap();
6078        let kids_body = s
6079            .put(&Node::If {
6080                cond: at_end,
6081                then_branch: str_lit(""),
6082                else_branch: cat(head, tail),
6083            })
6084            .unwrap();
6085        let render_kids = s
6086            .put(&Node::Function {
6087                name: "render_kids".into(),
6088                type_params: vec![],
6089                params: vec![
6090                    param("kids", Type::List(Box::new(elem_ty.clone()))),
6091                    param("i", Type::Number),
6092                ],
6093                produces: str_out(),
6094                requires: BTreeSet::new(),
6095                on_failure: vec![],
6096                body: vec![len_step],
6097                result: kids_body,
6098            })
6099            .unwrap();
6100
6101        // render_html(e: Element) -> String
6102        //   = match e {
6103        //       Text(content) -> content,
6104        //       El(tag, kids)  -> "<" ++ tag ++ ">"
6105        //                         ++ render_kids(kids, 0)
6106        //                         ++ "</" ++ tag ++ ">",
6107        //     }
6108        let tag_ref = s.put(&Node::Ref("tag".into())).unwrap();
6109        let open = cat(cat(str_lit("<"), tag_ref.clone()), str_lit(">"));
6110        let close = cat(cat(str_lit("</"), tag_ref), str_lit(">"));
6111        let inner = s
6112            .put(&Node::Call {
6113                func: "render_kids".into(),
6114                args: vec![
6115                    s.put(&Node::Ref("kids".into())).unwrap(),
6116                    s.put(&Node::Lit(0)).unwrap(),
6117                ],
6118            })
6119            .unwrap();
6120        let el_body = cat(open, cat(inner, close));
6121        let html_match = s
6122            .put(&Node::Match {
6123                scrutinee: s.put(&Node::Ref("e".into())).unwrap(),
6124                type_name: "Element".into(),
6125                arms: vec![
6126                    crate::node::MatchArm {
6127                        case: "Text".into(),
6128                        bindings: vec!["content".into()],
6129                        body: s.put(&Node::Ref("content".into())).unwrap(),
6130                    },
6131                    crate::node::MatchArm {
6132                        case: "El".into(),
6133                        bindings: vec!["tag".into(), "kids".into()],
6134                        body: el_body,
6135                    },
6136                ],
6137            })
6138            .unwrap();
6139        let render_html = s
6140            .put(&Node::Function {
6141                name: "render_html".into(),
6142                type_params: vec![],
6143                params: vec![param("e", elem_ty.clone())],
6144                produces: str_out(),
6145                requires: BTreeSet::new(),
6146                on_failure: vec![],
6147                body: vec![],
6148                result: html_match,
6149            })
6150            .unwrap();
6151
6152        // home(req: String) -> String builds a typed tree and renders it:
6153        //   ul[ li[ "Hello" ], li[ "World" ] ]
6154        let li = |text: &str| -> NodeHash {
6155            let t = s
6156                .put(&Node::Variant {
6157                    type_name: "Element".into(),
6158                    case: "Text".into(),
6159                    fields: vec![("content".into(), str_lit(text))],
6160                })
6161                .unwrap();
6162            s.put(&Node::Variant {
6163                type_name: "Element".into(),
6164                case: "El".into(),
6165                fields: vec![
6166                    ("tag".into(), str_lit("li")),
6167                    ("kids".into(), s.put(&Node::List(vec![t])).unwrap()),
6168                ],
6169            })
6170            .unwrap()
6171        };
6172        let ul = s
6173            .put(&Node::Variant {
6174                type_name: "Element".into(),
6175                case: "El".into(),
6176                fields: vec![
6177                    ("tag".into(), str_lit("ul")),
6178                    (
6179                        "kids".into(),
6180                        s.put(&Node::List(vec![li("Hello"), li("World")]))
6181                            .unwrap(),
6182                    ),
6183                ],
6184            })
6185            .unwrap();
6186        let home = s
6187            .put(&Node::Function {
6188                name: "home".into(),
6189                type_params: vec![],
6190                params: vec![param("req", Type::String)],
6191                produces: str_out(),
6192                requires: BTreeSet::new(),
6193                on_failure: vec![],
6194                body: vec![],
6195                result: s
6196                    .put(&Node::Call {
6197                        func: "render_html".into(),
6198                        args: vec![ul],
6199                    })
6200                    .unwrap(),
6201            })
6202            .unwrap();
6203
6204        // `home` (caller) precedes `render_html`/`render_kids`, which are
6205        // mutually recursive — exercises forward + mutual lowering.
6206        let m = s
6207            .put(&Node::Module {
6208                name: "view".into(),
6209                types: vec![element_def],
6210                functions: vec![home, render_html, render_kids],
6211            })
6212            .unwrap();
6213        let report = crate::check::Checker::new(&s).check(&m).unwrap();
6214        assert!(
6215            report.ok(),
6216            "typed view model did not type-check: {:?}",
6217            report.violations
6218        );
6219        let wasm = lower(&s, &m).unwrap();
6220        assert_eq!(
6221            serve_once(&wasm, "home", "/").unwrap(),
6222            "<ul><li>Hello</li><li>World</li></ul>"
6223        );
6224    }
6225
6226    #[test]
6227    fn a_structured_request_routes_to_a_typed_response() {
6228        // The Tier-2 entrypoint: a handler takes a typed `Request`
6229        // ({method, path, body}) and returns a typed `Response`
6230        // ({status, body}), dispatching on the structured value rather
6231        // than a bare request string.
6232        let s = Store::open_in_memory().unwrap();
6233        let str_lit = |t: &str| -> NodeHash { s.put(&Node::Str(t.into())).unwrap() };
6234        // Field order here IS the framework ABI (see `serve_request`).
6235        let request_def = s
6236            .put(&Node::RecordDef {
6237                name: "Request".into(),
6238                fields: vec![
6239                    ("method".into(), Type::String),
6240                    ("path".into(), Type::String),
6241                    ("body".into(), Type::String),
6242                ],
6243            })
6244            .unwrap();
6245        let response_def = s
6246            .put(&Node::RecordDef {
6247                name: "Response".into(),
6248                fields: vec![
6249                    ("status".into(), Type::Number),
6250                    ("body".into(), Type::String),
6251                ],
6252            })
6253            .unwrap();
6254        let resp = |status: i64, body: &str| -> NodeHash {
6255            s.put(&Node::Record {
6256                type_name: "Response".into(),
6257                fields: vec![
6258                    ("status".into(), s.put(&Node::Lit(status)).unwrap()),
6259                    ("body".into(), str_lit(body)),
6260                ],
6261            })
6262            .unwrap()
6263        };
6264        let field = |binding: &str, f: &str| -> NodeHash {
6265            s.put(&Node::Step {
6266                binding: binding.into(),
6267                value: s
6268                    .put(&Node::Field {
6269                        base: s.put(&Node::Ref("req".into())).unwrap(),
6270                        type_name: "Request".into(),
6271                        field: f.into(),
6272                    })
6273                    .unwrap(),
6274            })
6275            .unwrap()
6276        };
6277        let path_is = |p: &str| -> NodeHash {
6278            s.put(&Node::StrEq(
6279                s.put(&Node::Ref("path".into())).unwrap(),
6280                str_lit(p),
6281            ))
6282            .unwrap()
6283        };
6284        let is_post = s
6285            .put(&Node::StrEq(
6286                s.put(&Node::Ref("method".into())).unwrap(),
6287                str_lit("POST"),
6288            ))
6289            .unwrap();
6290        // /customers: POST -> 201 created, otherwise 200 list.
6291        let customers = s
6292            .put(&Node::If {
6293                cond: is_post,
6294                then_branch: resp(201, "created"),
6295                else_branch: resp(200, "list"),
6296            })
6297            .unwrap();
6298        let body = s
6299            .put(&Node::If {
6300                cond: path_is("/"),
6301                then_branch: resp(200, "home"),
6302                else_branch: s
6303                    .put(&Node::If {
6304                        cond: path_is("/customers"),
6305                        then_branch: customers,
6306                        else_branch: resp(404, "not found"),
6307                    })
6308                    .unwrap(),
6309            })
6310            .unwrap();
6311        let route = s
6312            .put(&Node::Function {
6313                name: "route".into(),
6314                type_params: vec![],
6315                params: vec![Param {
6316                    name: "req".into(),
6317                    ty: Type::Named("Request".into()),
6318                    min_confidence: Confidence::External,
6319                }],
6320                produces: Produces {
6321                    ty: Type::Named("Response".into()),
6322                    confidence: Confidence::External,
6323                },
6324                requires: BTreeSet::new(),
6325                on_failure: vec![],
6326                body: vec![field("method", "method"), field("path", "path")],
6327                result: body,
6328            })
6329            .unwrap();
6330        let m = s
6331            .put(&Node::Module {
6332                name: "app".into(),
6333                types: vec![request_def, response_def],
6334                functions: vec![route],
6335            })
6336            .unwrap();
6337        let report = crate::check::Checker::new(&s).check(&m).unwrap();
6338        assert!(
6339            report.ok(),
6340            "structured router did not type-check: {:?}",
6341            report.violations
6342        );
6343        let wasm = lower(&s, &m).unwrap();
6344        assert_eq!(
6345            serve_request(&wasm, "route", "GET", "/", "").unwrap(),
6346            HttpResponse {
6347                status: 200,
6348                body: "home".into()
6349            }
6350        );
6351        assert_eq!(
6352            serve_request(&wasm, "route", "GET", "/customers", "").unwrap(),
6353            HttpResponse {
6354                status: 200,
6355                body: "list".into()
6356            }
6357        );
6358        assert_eq!(
6359            serve_request(&wasm, "route", "POST", "/customers", "").unwrap(),
6360            HttpResponse {
6361                status: 201,
6362                body: "created".into()
6363            }
6364        );
6365        assert_eq!(
6366            serve_request(&wasm, "route", "GET", "/nope", "").unwrap(),
6367            HttpResponse {
6368                status: 404,
6369                body: "not found".into()
6370            }
6371        );
6372    }
6373
6374    #[test]
6375    fn a_deckhand_customers_page_end_to_end() {
6376        // The whole Tier-2 stack in one slice: a structured Request is
6377        // routed by path; the /customers route runs a Db query, wraps the
6378        // rows in the typed Element view model, renders to HTML through the
6379        // mutually-recursive render_html/render_kids, and returns a typed
6380        // Response. This is the first Deckhand-shaped page.
6381        let s = Store::open_in_memory().unwrap();
6382        let str_lit = |t: &str| -> NodeHash { s.put(&Node::Str(t.into())).unwrap() };
6383        let cat = |a: NodeHash, b: NodeHash| -> NodeHash {
6384            s.put(&Node::StrConcat(a, b)).unwrap()
6385        };
6386        let elem_ty = Type::Named("Element".into());
6387        let str_out = || Produces {
6388            ty: Type::String,
6389            confidence: Confidence::External,
6390        };
6391        let param = |name: &str, ty: Type| Param {
6392            name: name.into(),
6393            ty,
6394            min_confidence: Confidence::External,
6395        };
6396
6397        // --- the typed view model (Element + render_html/render_kids) ---
6398        let element_def = s
6399            .put(&Node::VariantDef {
6400                name: "Element".into(),
6401                cases: vec![
6402                    ("Text".into(), vec![("content".into(), Type::String)]),
6403                    (
6404                        "El".into(),
6405                        vec![
6406                            ("tag".into(), Type::String),
6407                            ("kids".into(), Type::List(Box::new(elem_ty.clone()))),
6408                        ],
6409                    ),
6410                ],
6411            })
6412            .unwrap();
6413        let kids_ref = s.put(&Node::Ref("kids".into())).unwrap();
6414        let i_ref = s.put(&Node::Ref("i".into())).unwrap();
6415        let len_step = s
6416            .put(&Node::Step {
6417                binding: "n".into(),
6418                value: s.put(&Node::ListLen(kids_ref.clone())).unwrap(),
6419            })
6420            .unwrap();
6421        let at_end = s
6422            .put(&Node::BinOp {
6423                op: crate::node::BinOp::Eq,
6424                lhs: i_ref.clone(),
6425                rhs: s.put(&Node::Ref("n".into())).unwrap(),
6426            })
6427            .unwrap();
6428        let head = s
6429            .put(&Node::Call {
6430                func: "render_html".into(),
6431                args: vec![s
6432                    .put(&Node::ListGet {
6433                        list: kids_ref.clone(),
6434                        index: i_ref.clone(),
6435                    })
6436                    .unwrap()],
6437            })
6438            .unwrap();
6439        let tail = s
6440            .put(&Node::Call {
6441                func: "render_kids".into(),
6442                args: vec![
6443                    kids_ref,
6444                    s.put(&Node::BinOp {
6445                        op: crate::node::BinOp::Add,
6446                        lhs: i_ref,
6447                        rhs: s.put(&Node::Lit(1)).unwrap(),
6448                    })
6449                    .unwrap(),
6450                ],
6451            })
6452            .unwrap();
6453        let render_kids = s
6454            .put(&Node::Function {
6455                name: "render_kids".into(),
6456                type_params: vec![],
6457                params: vec![
6458                    param("kids", Type::List(Box::new(elem_ty.clone()))),
6459                    param("i", Type::Number),
6460                ],
6461                produces: str_out(),
6462                requires: BTreeSet::new(),
6463                on_failure: vec![],
6464                body: vec![len_step],
6465                result: s
6466                    .put(&Node::If {
6467                        cond: at_end,
6468                        then_branch: str_lit(""),
6469                        else_branch: cat(head, tail),
6470                    })
6471                    .unwrap(),
6472            })
6473            .unwrap();
6474        let tag_ref = s.put(&Node::Ref("tag".into())).unwrap();
6475        let open = cat(cat(str_lit("<"), tag_ref.clone()), str_lit(">"));
6476        let close = cat(cat(str_lit("</"), tag_ref), str_lit(">"));
6477        let inner = s
6478            .put(&Node::Call {
6479                func: "render_kids".into(),
6480                args: vec![
6481                    s.put(&Node::Ref("kids".into())).unwrap(),
6482                    s.put(&Node::Lit(0)).unwrap(),
6483                ],
6484            })
6485            .unwrap();
6486        let render_html = s
6487            .put(&Node::Function {
6488                name: "render_html".into(),
6489                type_params: vec![],
6490                params: vec![param("e", elem_ty.clone())],
6491                produces: str_out(),
6492                requires: BTreeSet::new(),
6493                on_failure: vec![],
6494                body: vec![],
6495                result: s
6496                    .put(&Node::Match {
6497                        scrutinee: s.put(&Node::Ref("e".into())).unwrap(),
6498                        type_name: "Element".into(),
6499                        arms: vec![
6500                            crate::node::MatchArm {
6501                                case: "Text".into(),
6502                                bindings: vec!["content".into()],
6503                                body: s.put(&Node::Ref("content".into())).unwrap(),
6504                            },
6505                            crate::node::MatchArm {
6506                                case: "El".into(),
6507                                bindings: vec!["tag".into(), "kids".into()],
6508                                body: cat(open, cat(inner, close)),
6509                            },
6510                        ],
6511                    })
6512                    .unwrap(),
6513            })
6514            .unwrap();
6515
6516        // --- the structured request/response records ---
6517        let request_def = s
6518            .put(&Node::RecordDef {
6519                name: "Request".into(),
6520                fields: vec![
6521                    ("method".into(), Type::String),
6522                    ("path".into(), Type::String),
6523                    ("body".into(), Type::String),
6524                ],
6525            })
6526            .unwrap();
6527        let response_def = s
6528            .put(&Node::RecordDef {
6529                name: "Response".into(),
6530                fields: vec![
6531                    ("status".into(), Type::Number),
6532                    ("body".into(), Type::String),
6533                ],
6534            })
6535            .unwrap();
6536
6537        // --- the route: /customers queries the Db, renders the rows ---
6538        let el = |tag: &str, kids: Vec<NodeHash>| -> NodeHash {
6539            s.put(&Node::Variant {
6540                type_name: "Element".into(),
6541                case: "El".into(),
6542                fields: vec![
6543                    ("tag".into(), str_lit(tag)),
6544                    ("kids".into(), s.put(&Node::List(kids)).unwrap()),
6545                ],
6546            })
6547            .unwrap()
6548        };
6549        let rows = s
6550            .put(&Node::DbQuery {
6551                sql: str_lit("SELECT 'Acme'"),
6552                params: s.put(&Node::ListEmpty { elem: Type::String }).unwrap(),
6553            })
6554            .unwrap();
6555        let row_text = s
6556            .put(&Node::Variant {
6557                type_name: "Element".into(),
6558                case: "Text".into(),
6559                fields: vec![("content".into(), rows)],
6560            })
6561            .unwrap();
6562        let page = el("ul", vec![el("li", vec![row_text])]);
6563        let customers = s
6564            .put(&Node::Record {
6565                type_name: "Response".into(),
6566                fields: vec![
6567                    ("status".into(), s.put(&Node::Lit(200)).unwrap()),
6568                    (
6569                        "body".into(),
6570                        s.put(&Node::Call {
6571                            func: "render_html".into(),
6572                            args: vec![page],
6573                        })
6574                        .unwrap(),
6575                    ),
6576                ],
6577            })
6578            .unwrap();
6579        let not_found = s
6580            .put(&Node::Record {
6581                type_name: "Response".into(),
6582                fields: vec![
6583                    ("status".into(), s.put(&Node::Lit(404)).unwrap()),
6584                    ("body".into(), str_lit("not found")),
6585                ],
6586            })
6587            .unwrap();
6588        let route = s
6589            .put(&Node::Function {
6590                name: "route".into(),
6591                type_params: vec![],
6592                params: vec![param("req", Type::Named("Request".into()))],
6593                produces: Produces {
6594                    ty: Type::Named("Response".into()),
6595                    confidence: Confidence::External,
6596                },
6597                requires: {
6598                    let mut e = BTreeSet::new();
6599                    e.insert(crate::ty::Effect::Db);
6600                    e
6601                },
6602                on_failure: vec![],
6603                body: vec![s
6604                    .put(&Node::Step {
6605                        binding: "path".into(),
6606                        value: s
6607                            .put(&Node::Field {
6608                                base: s.put(&Node::Ref("req".into())).unwrap(),
6609                                type_name: "Request".into(),
6610                                field: "path".into(),
6611                            })
6612                            .unwrap(),
6613                    })
6614                    .unwrap()],
6615                result: s
6616                    .put(&Node::If {
6617                        cond: s
6618                            .put(&Node::StrEq(
6619                                s.put(&Node::Ref("path".into())).unwrap(),
6620                                str_lit("/customers"),
6621                            ))
6622                            .unwrap(),
6623                        then_branch: customers,
6624                        else_branch: not_found,
6625                    })
6626                    .unwrap(),
6627            })
6628            .unwrap();
6629
6630        // route (caller) precedes render_html/render_kids (mutually
6631        // recursive callees) — the whole stack lowers in two passes.
6632        let m = s
6633            .put(&Node::Module {
6634                name: "deckhand".into(),
6635                types: vec![element_def, request_def, response_def],
6636                functions: vec![route, render_html, render_kids],
6637            })
6638            .unwrap();
6639        let report = crate::check::Checker::new(&s).check(&m).unwrap();
6640        assert!(
6641            report.ok(),
6642            "the Deckhand page did not type-check: {:?}",
6643            report.violations
6644        );
6645        let wasm = lower(&s, &m).unwrap();
6646        assert_eq!(
6647            serve_request(&wasm, "route", "GET", "/customers", "").unwrap(),
6648            HttpResponse {
6649                status: 200,
6650                body: "<ul><li>Acme</li></ul>".into(),
6651            }
6652        );
6653        assert_eq!(
6654            serve_request(&wasm, "route", "GET", "/", "").unwrap(),
6655            HttpResponse {
6656                status: 404,
6657                body: "not found".into(),
6658            }
6659        );
6660    }
6661
6662    #[test]
6663    fn reason_phrase_covers_framework_codes_and_falls_back_by_class() {
6664        assert_eq!(reason_phrase(200), "OK");
6665        assert_eq!(reason_phrase(201), "Created");
6666        assert_eq!(reason_phrase(404), "Not Found");
6667        assert_eq!(reason_phrase(500), "Internal Server Error");
6668        // Unknown codes fall back by class, never panic.
6669        assert_eq!(reason_phrase(299), "OK");
6670        assert_eq!(reason_phrase(418), "Client Error");
6671        assert_eq!(reason_phrase(0), "Error");
6672    }
6673
6674    #[test]
6675    fn prefix_routing_extracts_a_path_param() {
6676        // `StrStartsWith` + `StrSlice`/`StrLen` is enough for a
6677        // parameterized route: /customers/{id} -> the id.
6678        let s = Store::open_in_memory().unwrap();
6679        let str_lit = |t: &str| -> NodeHash { s.put(&Node::Str(t.into())).unwrap() };
6680        let request_def = s
6681            .put(&Node::RecordDef {
6682                name: "Request".into(),
6683                fields: vec![
6684                    ("method".into(), Type::String),
6685                    ("path".into(), Type::String),
6686                    ("body".into(), Type::String),
6687                ],
6688            })
6689            .unwrap();
6690        let response_def = s
6691            .put(&Node::RecordDef {
6692                name: "Response".into(),
6693                fields: vec![
6694                    ("status".into(), Type::Number),
6695                    ("body".into(), Type::String),
6696                ],
6697            })
6698            .unwrap();
6699        let resp = |status: i64, body: NodeHash| -> NodeHash {
6700            s.put(&Node::Record {
6701                type_name: "Response".into(),
6702                fields: vec![
6703                    ("status".into(), s.put(&Node::Lit(status)).unwrap()),
6704                    ("body".into(), body),
6705                ],
6706            })
6707            .unwrap()
6708        };
6709        let path_ref = || s.put(&Node::Ref("path".into())).unwrap();
6710        let plen_ref = || s.put(&Node::Ref("plen".into())).unwrap();
6711        // id = path[plen ..]  (plen = len("/customers/"))
6712        let id = s
6713            .put(&Node::StrSlice {
6714                s: path_ref(),
6715                start: plen_ref(),
6716                len: s
6717                    .put(&Node::BinOp {
6718                        op: crate::node::BinOp::Sub,
6719                        lhs: s.put(&Node::StrLen(path_ref())).unwrap(),
6720                        rhs: plen_ref(),
6721                    })
6722                    .unwrap(),
6723            })
6724            .unwrap();
6725        let route = s
6726            .put(&Node::Function {
6727                name: "route".into(),
6728                type_params: vec![],
6729                params: vec![Param {
6730                    name: "req".into(),
6731                    ty: Type::Named("Request".into()),
6732                    min_confidence: Confidence::External,
6733                }],
6734                produces: Produces {
6735                    ty: Type::Named("Response".into()),
6736                    confidence: Confidence::External,
6737                },
6738                requires: BTreeSet::new(),
6739                on_failure: vec![],
6740                body: vec![
6741                    s.put(&Node::Step {
6742                        binding: "path".into(),
6743                        value: s
6744                            .put(&Node::Field {
6745                                base: s.put(&Node::Ref("req".into())).unwrap(),
6746                                type_name: "Request".into(),
6747                                field: "path".into(),
6748                            })
6749                            .unwrap(),
6750                    })
6751                    .unwrap(),
6752                    s.put(&Node::Step {
6753                        binding: "plen".into(),
6754                        value: s.put(&Node::StrLen(str_lit("/customers/"))).unwrap(),
6755                    })
6756                    .unwrap(),
6757                ],
6758                result: s
6759                    .put(&Node::If {
6760                        cond: s
6761                            .put(&Node::StrStartsWith {
6762                                s: path_ref(),
6763                                prefix: str_lit("/customers/"),
6764                            })
6765                            .unwrap(),
6766                        then_branch: resp(
6767                            200,
6768                            s.put(&Node::StrConcat(str_lit("customer "), id))
6769                                .unwrap(),
6770                        ),
6771                        else_branch: resp(404, str_lit("not found")),
6772                    })
6773                    .unwrap(),
6774            })
6775            .unwrap();
6776        // The renderer projects the new primitive.
6777        assert!(crate::render::render(&s, &route)
6778            .unwrap()
6779            .contains("str_starts_with("));
6780        let m = s
6781            .put(&Node::Module {
6782                name: "app".into(),
6783                types: vec![request_def, response_def],
6784                functions: vec![route],
6785            })
6786            .unwrap();
6787        let report = crate::check::Checker::new(&s).check(&m).unwrap();
6788        assert!(
6789            report.ok(),
6790            "prefix router did not type-check: {:?}",
6791            report.violations
6792        );
6793        let wasm = lower(&s, &m).unwrap();
6794        assert_eq!(
6795            serve_request(&wasm, "route", "GET", "/customers/42", "").unwrap(),
6796            HttpResponse {
6797                status: 200,
6798                body: "customer 42".into(),
6799            }
6800        );
6801        assert_eq!(
6802            serve_request(&wasm, "route", "GET", "/about", "").unwrap(),
6803            HttpResponse {
6804                status: 404,
6805                body: "not found".into(),
6806            }
6807        );
6808    }
6809
6810    #[test]
6811    fn number_and_string_conversions_round_trip() {
6812        // conv(req: String) -> String = number_to_str(str_to_number(req))
6813        let s = Store::open_in_memory().unwrap();
6814        let req = s.put(&Node::Ref("req".into())).unwrap();
6815        let parsed = s.put(&Node::StrToNumber(req)).unwrap();
6816        let back = s.put(&Node::NumberToStr(parsed)).unwrap();
6817        let conv = s
6818            .put(&Node::Function {
6819                name: "conv".into(),
6820                type_params: vec![],
6821                params: vec![Param {
6822                    name: "req".into(),
6823                    ty: Type::String,
6824                    min_confidence: Confidence::External,
6825                }],
6826                produces: Produces {
6827                    ty: Type::String,
6828                    confidence: Confidence::External,
6829                },
6830                requires: BTreeSet::new(),
6831                on_failure: vec![],
6832                body: vec![],
6833                result: back,
6834            })
6835            .unwrap();
6836        // Renderer projects both new primitives.
6837        let rendered = crate::render::render(&s, &conv).unwrap();
6838        assert!(rendered.contains("number_to_str("));
6839        assert!(rendered.contains("str_to_number("));
6840        let m = s
6841            .put(&Node::Module {
6842                name: "m".into(),
6843                types: vec![],
6844                functions: vec![conv],
6845            })
6846            .unwrap();
6847        let report = crate::check::Checker::new(&s).check(&m).unwrap();
6848        assert!(
6849            report.ok(),
6850            "conversions did not type-check: {:?}",
6851            report.violations
6852        );
6853        let wasm = lower(&s, &m).unwrap();
6854        for (input, expected) in [
6855            ("42", "42"),
6856            ("-7", "-7"),
6857            ("0", "0"),
6858            ("-1000000", "-1000000"),
6859            ("12ab", "12"),  // leading numeric prefix
6860            ("x", "0"),      // non-numeric -> 0
6861            ("", "0"),       // empty -> 0
6862        ] {
6863            assert_eq!(
6864                serve_once(&wasm, "conv", input).unwrap(),
6865                expected,
6866                "conv({input:?})"
6867            );
6868        }
6869    }
6870
6871    #[test]
6872    fn numeric_path_param_routes() {
6873        // /customers/{id}: parse the id to a Number, echo it back through
6874        // the view as "id=<n>" — the motivating Deckhand use case.
6875        let s = Store::open_in_memory().unwrap();
6876        let str_lit = |t: &str| -> NodeHash { s.put(&Node::Str(t.into())).unwrap() };
6877        let request_def = s
6878            .put(&Node::RecordDef {
6879                name: "Request".into(),
6880                fields: vec![
6881                    ("method".into(), Type::String),
6882                    ("path".into(), Type::String),
6883                    ("body".into(), Type::String),
6884                ],
6885            })
6886            .unwrap();
6887        let response_def = s
6888            .put(&Node::RecordDef {
6889                name: "Response".into(),
6890                fields: vec![
6891                    ("status".into(), Type::Number),
6892                    ("body".into(), Type::String),
6893                ],
6894            })
6895            .unwrap();
6896        let resp = |status: i64, body: NodeHash| -> NodeHash {
6897            s.put(&Node::Record {
6898                type_name: "Response".into(),
6899                fields: vec![
6900                    ("status".into(), s.put(&Node::Lit(status)).unwrap()),
6901                    ("body".into(), body),
6902                ],
6903            })
6904            .unwrap()
6905        };
6906        let path_ref = || s.put(&Node::Ref("path".into())).unwrap();
6907        let plen_ref = || s.put(&Node::Ref("plen".into())).unwrap();
6908        let id_str = s
6909            .put(&Node::StrSlice {
6910                s: path_ref(),
6911                start: plen_ref(),
6912                len: s
6913                    .put(&Node::BinOp {
6914                        op: crate::node::BinOp::Sub,
6915                        lhs: s.put(&Node::StrLen(path_ref())).unwrap(),
6916                        rhs: plen_ref(),
6917                    })
6918                    .unwrap(),
6919            })
6920            .unwrap();
6921        // body = "id=" ++ number_to_str(str_to_number(id_str))
6922        let echoed = s
6923            .put(&Node::StrConcat(
6924                str_lit("id="),
6925                s.put(&Node::NumberToStr(
6926                    s.put(&Node::StrToNumber(id_str)).unwrap(),
6927                ))
6928                .unwrap(),
6929            ))
6930            .unwrap();
6931        let route = s
6932            .put(&Node::Function {
6933                name: "route".into(),
6934                type_params: vec![],
6935                params: vec![Param {
6936                    name: "req".into(),
6937                    ty: Type::Named("Request".into()),
6938                    min_confidence: Confidence::External,
6939                }],
6940                produces: Produces {
6941                    ty: Type::Named("Response".into()),
6942                    confidence: Confidence::External,
6943                },
6944                requires: BTreeSet::new(),
6945                on_failure: vec![],
6946                body: vec![
6947                    s.put(&Node::Step {
6948                        binding: "path".into(),
6949                        value: s
6950                            .put(&Node::Field {
6951                                base: s.put(&Node::Ref("req".into())).unwrap(),
6952                                type_name: "Request".into(),
6953                                field: "path".into(),
6954                            })
6955                            .unwrap(),
6956                    })
6957                    .unwrap(),
6958                    s.put(&Node::Step {
6959                        binding: "plen".into(),
6960                        value: s.put(&Node::StrLen(str_lit("/customers/"))).unwrap(),
6961                    })
6962                    .unwrap(),
6963                ],
6964                result: s
6965                    .put(&Node::If {
6966                        cond: s
6967                            .put(&Node::StrStartsWith {
6968                                s: path_ref(),
6969                                prefix: str_lit("/customers/"),
6970                            })
6971                            .unwrap(),
6972                        then_branch: resp(200, echoed),
6973                        else_branch: resp(404, str_lit("not found")),
6974                    })
6975                    .unwrap(),
6976            })
6977            .unwrap();
6978        let m = s
6979            .put(&Node::Module {
6980                name: "app".into(),
6981                types: vec![request_def, response_def],
6982                functions: vec![route],
6983            })
6984            .unwrap();
6985        let report = crate::check::Checker::new(&s).check(&m).unwrap();
6986        assert!(
6987            report.ok(),
6988            "numeric router did not type-check: {:?}",
6989            report.violations
6990        );
6991        let wasm = lower(&s, &m).unwrap();
6992        assert_eq!(
6993            serve_request(&wasm, "route", "GET", "/customers/42", "").unwrap(),
6994            HttpResponse {
6995                status: 200,
6996                body: "id=42".into(),
6997            }
6998        );
6999        assert_eq!(
7000            serve_request(&wasm, "route", "GET", "/customers/abc", "").unwrap(),
7001            HttpResponse {
7002                status: 200,
7003                body: "id=0".into(),
7004            }
7005        );
7006        assert_eq!(
7007            serve_request(&wasm, "route", "GET", "/x", "").unwrap(),
7008            HttpResponse {
7009                status: 404,
7010                body: "not found".into(),
7011            }
7012        );
7013    }
7014
7015    #[test]
7016    fn a_rails_style_blog_app() {
7017        // The Tier-3 showcase: a classic Rails blog. /posts is the index,
7018        // /posts/{id} shows one post. Posts are a typed domain record;
7019        // pages are produced by recursion over a List<Post>. (DB-backed
7020        // persistence — parsing query rows into List<Post> — is a later
7021        // slice; it will want a StrIndexOf primitive. The Element view
7022        // model is proven separately; the index needs a runtime-length
7023        // child list, which List literals can't express yet, so pages are
7024        // built by recursive String concatenation here.)
7025        let s = Store::open_in_memory().unwrap();
7026        let lit = |t: &str| -> NodeHash { s.put(&Node::Str(t.into())).unwrap() };
7027        let num = |n: i64| -> NodeHash { s.put(&Node::Lit(n)).unwrap() };
7028        let r = |name: &str| -> NodeHash { s.put(&Node::Ref(name.into())).unwrap() };
7029        let cats = |parts: &[NodeHash]| -> NodeHash {
7030            let mut it = parts.iter().cloned();
7031            let first = it.next().unwrap();
7032            it.fold(first, |acc, p| s.put(&Node::StrConcat(acc, p)).unwrap())
7033        };
7034        let param = |name: &str, ty: Type| Param {
7035            name: name.into(),
7036            ty,
7037            min_confidence: Confidence::External,
7038        };
7039        let str_out = || Produces {
7040            ty: Type::String,
7041            confidence: Confidence::External,
7042        };
7043        let post_ty = Type::Named("Post".into());
7044
7045        let post_def = s
7046            .put(&Node::RecordDef {
7047                name: "Post".into(),
7048                fields: vec![
7049                    ("id".into(), Type::Number),
7050                    ("title".into(), Type::String),
7051                    ("body".into(), Type::String),
7052                ],
7053            })
7054            .unwrap();
7055        let request_def = s
7056            .put(&Node::RecordDef {
7057                name: "Request".into(),
7058                fields: vec![
7059                    ("method".into(), Type::String),
7060                    ("path".into(), Type::String),
7061                    ("body".into(), Type::String),
7062                ],
7063            })
7064            .unwrap();
7065        let response_def = s
7066            .put(&Node::RecordDef {
7067                name: "Response".into(),
7068                fields: vec![
7069                    ("status".into(), Type::Number),
7070                    ("body".into(), Type::String),
7071                ],
7072            })
7073            .unwrap();
7074
7075        // posts() -> List<Post> : the seed data.
7076        let post = |id: i64, title: &str, body: &str| -> NodeHash {
7077            s.put(&Node::Record {
7078                type_name: "Post".into(),
7079                fields: vec![
7080                    ("id".into(), num(id)),
7081                    ("title".into(), lit(title)),
7082                    ("body".into(), lit(body)),
7083                ],
7084            })
7085            .unwrap()
7086        };
7087        let posts = s
7088            .put(&Node::Function {
7089                name: "posts".into(),
7090                type_params: vec![],
7091                params: vec![],
7092                produces: Produces {
7093                    ty: Type::List(Box::new(post_ty.clone())),
7094                    confidence: Confidence::External,
7095                },
7096                requires: BTreeSet::new(),
7097                on_failure: vec![],
7098                body: vec![],
7099                result: s
7100                    .put(&Node::List(vec![
7101                        post(1, "Hello World", "The first post."),
7102                        post(2, "On Cairn", "Programs as reasoning chains."),
7103                    ]))
7104                    .unwrap(),
7105            })
7106            .unwrap();
7107
7108        // ps[i].<field>
7109        let field = |f: &str| -> NodeHash {
7110            s.put(&Node::Field {
7111                base: s
7112                    .put(&Node::ListGet {
7113                        list: r("ps"),
7114                        index: r("i"),
7115                    })
7116                    .unwrap(),
7117                type_name: "Post".into(),
7118                field: f.into(),
7119            })
7120            .unwrap()
7121        };
7122        let len_step = || {
7123            s.put(&Node::Step {
7124                binding: "n".into(),
7125                value: s.put(&Node::ListLen(r("ps"))).unwrap(),
7126            })
7127            .unwrap()
7128        };
7129        let at_end = || {
7130            s.put(&Node::BinOp {
7131                op: crate::node::BinOp::Eq,
7132                lhs: r("i"),
7133                rhs: r("n"),
7134            })
7135            .unwrap()
7136        };
7137        let next_i = || {
7138            s.put(&Node::BinOp {
7139                op: crate::node::BinOp::Add,
7140                lhs: r("i"),
7141                rhs: num(1),
7142            })
7143            .unwrap()
7144        };
7145
7146        // posts_html(ps, i) -> String : recursive <li> list.
7147        let posts_html = s
7148            .put(&Node::Function {
7149                name: "posts_html".into(),
7150                type_params: vec![],
7151                params: vec![
7152                    param("ps", Type::List(Box::new(post_ty.clone()))),
7153                    param("i", Type::Number),
7154                ],
7155                produces: str_out(),
7156                requires: BTreeSet::new(),
7157                on_failure: vec![],
7158                body: vec![len_step()],
7159                result: s
7160                    .put(&Node::If {
7161                        cond: at_end(),
7162                        then_branch: lit(""),
7163                        else_branch: cats(&[
7164                            lit("<li>"),
7165                            s.put(&Node::NumberToStr(field("id"))).unwrap(),
7166                            lit(": "),
7167                            field("title"),
7168                            lit("</li>"),
7169                            s.put(&Node::Call {
7170                                func: "posts_html".into(),
7171                                args: vec![r("ps"), next_i()],
7172                            })
7173                            .unwrap(),
7174                        ]),
7175                    })
7176                    .unwrap(),
7177            })
7178            .unwrap();
7179
7180        // show_html(ps, id, i) -> String : find the post by id, render it.
7181        let show_html = s
7182            .put(&Node::Function {
7183                name: "show_html".into(),
7184                type_params: vec![],
7185                params: vec![
7186                    param("ps", Type::List(Box::new(post_ty.clone()))),
7187                    param("id", Type::Number),
7188                    param("i", Type::Number),
7189                ],
7190                produces: str_out(),
7191                requires: BTreeSet::new(),
7192                on_failure: vec![],
7193                body: vec![len_step()],
7194                result: s
7195                    .put(&Node::If {
7196                        cond: at_end(),
7197                        then_branch: lit("<p>not found</p>"),
7198                        else_branch: s
7199                            .put(&Node::If {
7200                                cond: s
7201                                    .put(&Node::BinOp {
7202                                        op: crate::node::BinOp::Eq,
7203                                        lhs: field("id"),
7204                                        rhs: r("id"),
7205                                    })
7206                                    .unwrap(),
7207                                then_branch: cats(&[
7208                                    lit("<h1>"),
7209                                    field("title"),
7210                                    lit("</h1><p>"),
7211                                    field("body"),
7212                                    lit("</p>"),
7213                                ]),
7214                                else_branch: s
7215                                    .put(&Node::Call {
7216                                        func: "show_html".into(),
7217                                        args: vec![r("ps"), r("id"), next_i()],
7218                                    })
7219                                    .unwrap(),
7220                            })
7221                            .unwrap(),
7222                    })
7223                    .unwrap(),
7224            })
7225            .unwrap();
7226
7227        // route(req) -> Response : /posts and /posts/{id}.
7228        let resp = |status: i64, body: NodeHash| -> NodeHash {
7229            s.put(&Node::Record {
7230                type_name: "Response".into(),
7231                fields: vec![("status".into(), num(status)), ("body".into(), body)],
7232            })
7233            .unwrap()
7234        };
7235        let id_from_path = s
7236            .put(&Node::StrToNumber(
7237                s.put(&Node::StrSlice {
7238                    s: r("path"),
7239                    start: r("plen"),
7240                    len: s
7241                        .put(&Node::BinOp {
7242                            op: crate::node::BinOp::Sub,
7243                            lhs: s.put(&Node::StrLen(r("path"))).unwrap(),
7244                            rhs: r("plen"),
7245                        })
7246                        .unwrap(),
7247                })
7248                .unwrap(),
7249            ))
7250            .unwrap();
7251        let route = s
7252            .put(&Node::Function {
7253                name: "route".into(),
7254                type_params: vec![],
7255                params: vec![param("req", Type::Named("Request".into()))],
7256                produces: Produces {
7257                    ty: Type::Named("Response".into()),
7258                    confidence: Confidence::External,
7259                },
7260                requires: BTreeSet::new(),
7261                on_failure: vec![],
7262                body: vec![
7263                    s.put(&Node::Step {
7264                        binding: "path".into(),
7265                        value: s
7266                            .put(&Node::Field {
7267                                base: r("req"),
7268                                type_name: "Request".into(),
7269                                field: "path".into(),
7270                            })
7271                            .unwrap(),
7272                    })
7273                    .unwrap(),
7274                    s.put(&Node::Step {
7275                        binding: "ps".into(),
7276                        value: s
7277                            .put(&Node::Call {
7278                                func: "posts".into(),
7279                                args: vec![],
7280                            })
7281                            .unwrap(),
7282                    })
7283                    .unwrap(),
7284                    s.put(&Node::Step {
7285                        binding: "plen".into(),
7286                        value: s.put(&Node::StrLen(lit("/posts/"))).unwrap(),
7287                    })
7288                    .unwrap(),
7289                ],
7290                result: s
7291                    .put(&Node::If {
7292                        cond: s
7293                            .put(&Node::StrEq(r("path"), lit("/posts")))
7294                            .unwrap(),
7295                        then_branch: resp(
7296                            200,
7297                            cats(&[
7298                                lit("<ul>"),
7299                                s.put(&Node::Call {
7300                                    func: "posts_html".into(),
7301                                    args: vec![r("ps"), num(0)],
7302                                })
7303                                .unwrap(),
7304                                lit("</ul>"),
7305                            ]),
7306                        ),
7307                        else_branch: s
7308                            .put(&Node::If {
7309                                cond: s
7310                                    .put(&Node::StrStartsWith {
7311                                        s: r("path"),
7312                                        prefix: lit("/posts/"),
7313                                    })
7314                                    .unwrap(),
7315                                then_branch: resp(
7316                                    200,
7317                                    s.put(&Node::Call {
7318                                        func: "show_html".into(),
7319                                        args: vec![
7320                                            r("ps"),
7321                                            id_from_path,
7322                                            num(0),
7323                                        ],
7324                                    })
7325                                    .unwrap(),
7326                                ),
7327                                else_branch: resp(404, lit("not found")),
7328                            })
7329                            .unwrap(),
7330                    })
7331                    .unwrap(),
7332            })
7333            .unwrap();
7334
7335        let m = s
7336            .put(&Node::Module {
7337                name: "blog".into(),
7338                types: vec![post_def, request_def, response_def],
7339                functions: vec![route, posts_html, show_html, posts],
7340            })
7341            .unwrap();
7342        let report = crate::check::Checker::new(&s).check(&m).unwrap();
7343        assert!(
7344            report.ok(),
7345            "the blog app did not type-check: {:?}",
7346            report.violations
7347        );
7348        let wasm = lower(&s, &m).unwrap();
7349        assert_eq!(
7350            serve_request(&wasm, "route", "GET", "/posts", "").unwrap(),
7351            HttpResponse {
7352                status: 200,
7353                body: "<ul><li>1: Hello World</li><li>2: On Cairn</li></ul>"
7354                    .into(),
7355            }
7356        );
7357        assert_eq!(
7358            serve_request(&wasm, "route", "GET", "/posts/2", "").unwrap(),
7359            HttpResponse {
7360                status: 200,
7361                body: "<h1>On Cairn</h1><p>Programs as reasoning chains.</p>"
7362                    .into(),
7363            }
7364        );
7365        assert_eq!(
7366            serve_request(&wasm, "route", "GET", "/posts/9", "").unwrap(),
7367            HttpResponse {
7368                status: 200,
7369                body: "<p>not found</p>".into(),
7370            }
7371        );
7372        assert_eq!(
7373            serve_request(&wasm, "route", "GET", "/", "").unwrap(),
7374            HttpResponse {
7375                status: 404,
7376                body: "not found".into(),
7377            }
7378        );
7379    }
7380
7381    #[test]
7382    fn runtime_lists_via_cons() {
7383        // mk(n,i)  = if i==n then []:List<Number> else cons(i, mk(n,i+1))
7384        // sum(xs,i)= if i==len(xs) then 0 else xs[i] + sum(xs,i+1)
7385        // total(n) = sum(mk(n,0), 0)
7386        let s = Store::open_in_memory().unwrap();
7387        let r = |name: &str| -> NodeHash { s.put(&Node::Ref(name.into())).unwrap() };
7388        let num = |n: i64| -> NodeHash { s.put(&Node::Lit(n)).unwrap() };
7389        let bin = |op, a: NodeHash, b: NodeHash| -> NodeHash {
7390            s.put(&Node::BinOp { op, lhs: a, rhs: b }).unwrap()
7391        };
7392        let param = |name: &str, ty: Type| Param {
7393            name: name.into(),
7394            ty,
7395            min_confidence: Confidence::External,
7396        };
7397        let num_list = || Type::List(Box::new(Type::Number));
7398        let ext = |ty: Type| Produces {
7399            ty,
7400            confidence: Confidence::External,
7401        };
7402
7403        let mk = s
7404            .put(&Node::Function {
7405                name: "mk".into(),
7406                type_params: vec![],
7407                params: vec![param("n", Type::Number), param("i", Type::Number)],
7408                produces: ext(num_list()),
7409                requires: BTreeSet::new(),
7410                on_failure: vec![],
7411                body: vec![],
7412                result: s
7413                    .put(&Node::If {
7414                        cond: bin(crate::node::BinOp::Eq, r("i"), r("n")),
7415                        then_branch: s
7416                            .put(&Node::ListEmpty {
7417                                elem: Type::Number,
7418                            })
7419                            .unwrap(),
7420                        else_branch: s
7421                            .put(&Node::ListCons {
7422                                head: r("i"),
7423                                tail: s
7424                                    .put(&Node::Call {
7425                                        func: "mk".into(),
7426                                        args: vec![
7427                                            r("n"),
7428                                            bin(
7429                                                crate::node::BinOp::Add,
7430                                                r("i"),
7431                                                num(1),
7432                                            ),
7433                                        ],
7434                                    })
7435                                    .unwrap(),
7436                            })
7437                            .unwrap(),
7438                    })
7439                    .unwrap(),
7440            })
7441            .unwrap();
7442
7443        let sum = s
7444            .put(&Node::Function {
7445                name: "sum".into(),
7446                type_params: vec![],
7447                params: vec![param("xs", num_list()), param("i", Type::Number)],
7448                produces: ext(Type::Number),
7449                requires: BTreeSet::new(),
7450                on_failure: vec![],
7451                body: vec![s
7452                    .put(&Node::Step {
7453                        binding: "len".into(),
7454                        value: s.put(&Node::ListLen(r("xs"))).unwrap(),
7455                    })
7456                    .unwrap()],
7457                result: s
7458                    .put(&Node::If {
7459                        cond: bin(crate::node::BinOp::Eq, r("i"), r("len")),
7460                        then_branch: num(0),
7461                        else_branch: bin(
7462                            crate::node::BinOp::Add,
7463                            s.put(&Node::ListGet {
7464                                list: r("xs"),
7465                                index: r("i"),
7466                            })
7467                            .unwrap(),
7468                            s.put(&Node::Call {
7469                                func: "sum".into(),
7470                                args: vec![
7471                                    r("xs"),
7472                                    bin(crate::node::BinOp::Add, r("i"), num(1)),
7473                                ],
7474                            })
7475                            .unwrap(),
7476                        ),
7477                    })
7478                    .unwrap(),
7479            })
7480            .unwrap();
7481
7482        let total = s
7483            .put(&Node::Function {
7484                name: "total".into(),
7485                type_params: vec![],
7486                params: vec![param("n", Type::Number)],
7487                produces: ext(Type::Number),
7488                requires: BTreeSet::new(),
7489                on_failure: vec![],
7490                body: vec![s
7491                    .put(&Node::Step {
7492                        binding: "xs".into(),
7493                        value: s
7494                            .put(&Node::Call {
7495                                func: "mk".into(),
7496                                args: vec![r("n"), num(0)],
7497                            })
7498                            .unwrap(),
7499                    })
7500                    .unwrap()],
7501                result: s
7502                    .put(&Node::Call {
7503                        func: "sum".into(),
7504                        args: vec![r("xs"), num(0)],
7505                    })
7506                    .unwrap(),
7507            })
7508            .unwrap();
7509
7510        // Renderer projects the new primitives.
7511        let rendered = crate::render::render(&s, &mk).unwrap();
7512        assert!(rendered.contains("list_empty<Number>()"), "{rendered}");
7513        assert!(rendered.contains("cons("), "{rendered}");
7514
7515        let m = s
7516            .put(&Node::Module {
7517                name: "m".into(),
7518                types: vec![],
7519                functions: vec![total, mk, sum],
7520            })
7521            .unwrap();
7522        let report = crate::check::Checker::new(&s).check(&m).unwrap();
7523        assert!(
7524            report.ok(),
7525            "runtime lists did not type-check: {:?}",
7526            report.violations
7527        );
7528        let wasm = lower(&s, &m).unwrap();
7529        assert_eq!(run_i64(&wasm, "total", &[5]).unwrap(), 10); // 0+1+2+3+4
7530        assert_eq!(run_i64(&wasm, "total", &[0]).unwrap(), 0); // empty list
7531        assert_eq!(run_i64(&wasm, "total", &[1]).unwrap(), 0);
7532        assert_eq!(run_i64(&wasm, "total", &[4]).unwrap(), 6); // 0+1+2+3
7533    }
7534
7535    #[test]
7536    fn str_index_of_finds_substring() {
7537        let s = Store::open_in_memory().unwrap();
7538        let strf = |name: &str, needle: &str| -> NodeHash {
7539            let call = s
7540                .put(&Node::StrIndexOf {
7541                    haystack: s.put(&Node::Ref("req".into())).unwrap(),
7542                    needle: s.put(&Node::Str(needle.into())).unwrap(),
7543                })
7544                .unwrap();
7545            s.put(&Node::Function {
7546                name: name.into(),
7547                type_params: vec![],
7548                params: vec![Param {
7549                    name: "req".into(),
7550                    ty: Type::String,
7551                    min_confidence: Confidence::External,
7552                }],
7553                produces: Produces {
7554                    ty: Type::String,
7555                    confidence: Confidence::External,
7556                },
7557                requires: BTreeSet::new(),
7558                on_failure: vec![],
7559                body: vec![],
7560                result: s.put(&Node::NumberToStr(call)).unwrap(),
7561            })
7562            .unwrap()
7563        };
7564        let idx = strf("idx", "::");
7565        let idx0 = strf("idx0", "");
7566        assert!(crate::render::render(&s, &idx)
7567            .unwrap()
7568            .contains("str_index_of("));
7569        let m = s
7570            .put(&Node::Module {
7571                name: "m".into(),
7572                types: vec![],
7573                functions: vec![idx, idx0],
7574            })
7575            .unwrap();
7576        assert!(crate::check::Checker::new(&s).check(&m).unwrap().ok());
7577        let wasm = lower(&s, &m).unwrap();
7578        assert_eq!(serve_once(&wasm, "idx", "ab::cd").unwrap(), "2");
7579        assert_eq!(serve_once(&wasm, "idx", "::x").unwrap(), "0");
7580        assert_eq!(serve_once(&wasm, "idx", "abc").unwrap(), "-1");
7581        assert_eq!(serve_once(&wasm, "idx", "").unwrap(), "-1");
7582        assert_eq!(serve_once(&wasm, "idx0", "anything").unwrap(), "0");
7583    }
7584
7585    #[test]
7586    fn a_db_backed_blog_index() {
7587        // The Rails payoff: posts come from db_query, parsed into a
7588        // List<Post> by recursion over a \n/\t-delimited result string
7589        // (StrIndexOf + StrSlice + StrToNumber + ListCons).
7590        let s = Store::open_in_memory().unwrap();
7591        let lit = |t: &str| -> NodeHash { s.put(&Node::Str(t.into())).unwrap() };
7592        let num = |n: i64| -> NodeHash { s.put(&Node::Lit(n)).unwrap() };
7593        let r = |name: &str| -> NodeHash { s.put(&Node::Ref(name.into())).unwrap() };
7594        let bin = |op, a: NodeHash, b: NodeHash| -> NodeHash {
7595            s.put(&Node::BinOp { op, lhs: a, rhs: b }).unwrap()
7596        };
7597        let step = |binding: &str, value: NodeHash| -> NodeHash {
7598            s.put(&Node::Step {
7599                binding: binding.into(),
7600                value,
7601            })
7602            .unwrap()
7603        };
7604        let slice = |str_: NodeHash, start: NodeHash, len: NodeHash| -> NodeHash {
7605            s.put(&Node::StrSlice {
7606                s: str_,
7607                start,
7608                len,
7609            })
7610            .unwrap()
7611        };
7612        let call = |f: &str, args: Vec<NodeHash>| -> NodeHash {
7613            s.put(&Node::Call {
7614                func: f.into(),
7615                args,
7616            })
7617            .unwrap()
7618        };
7619        let cats = |parts: &[NodeHash]| -> NodeHash {
7620            let mut it = parts.iter().cloned();
7621            let first = it.next().unwrap();
7622            it.fold(first, |acc, p| s.put(&Node::StrConcat(acc, p)).unwrap())
7623        };
7624        let param = |name: &str, ty: Type| Param {
7625            name: name.into(),
7626            ty,
7627            min_confidence: Confidence::External,
7628        };
7629        let ext = |ty: Type| Produces {
7630            ty,
7631            confidence: Confidence::External,
7632        };
7633        let post_ty = Type::Named("Post".into());
7634        let post_list = || Type::List(Box::new(Type::Named("Post".into())));
7635        let idx = |hay: NodeHash, needle: &str| -> NodeHash {
7636            s.put(&Node::StrIndexOf {
7637                haystack: hay,
7638                needle: lit(needle),
7639            })
7640            .unwrap()
7641        };
7642
7643        let post_def = s
7644            .put(&Node::RecordDef {
7645                name: "Post".into(),
7646                fields: vec![
7647                    ("id".into(), Type::Number),
7648                    ("title".into(), Type::String),
7649                    ("body".into(), Type::String),
7650                ],
7651            })
7652            .unwrap();
7653        let request_def = s
7654            .put(&Node::RecordDef {
7655                name: "Request".into(),
7656                fields: vec![
7657                    ("method".into(), Type::String),
7658                    ("path".into(), Type::String),
7659                    ("body".into(), Type::String),
7660                ],
7661            })
7662            .unwrap();
7663        let response_def = s
7664            .put(&Node::RecordDef {
7665                name: "Response".into(),
7666                fields: vec![
7667                    ("status".into(), Type::Number),
7668                    ("body".into(), Type::String),
7669                ],
7670            })
7671            .unwrap();
7672
7673        // parse_row("id\ttitle\tbody") -> Post
7674        let t1p1 = bin(crate::node::BinOp::Add, r("t1"), num(1));
7675        let t2p1 = bin(crate::node::BinOp::Add, r("t2"), num(1));
7676        let parse_row = s
7677            .put(&Node::Function {
7678                name: "parse_row".into(),
7679                type_params: vec![],
7680                params: vec![param("row", Type::String)],
7681                produces: ext(post_ty.clone()),
7682                requires: BTreeSet::new(),
7683                on_failure: vec![],
7684                body: vec![
7685                    step("rlen", s.put(&Node::StrLen(r("row"))).unwrap()),
7686                    step("t1", idx(r("row"), "\t")),
7687                    step("ids", slice(r("row"), num(0), r("t1"))),
7688                    step(
7689                        "after1",
7690                        slice(
7691                            r("row"),
7692                            t1p1.clone(),
7693                            bin(crate::node::BinOp::Sub, r("rlen"), t1p1),
7694                        ),
7695                    ),
7696                    step("alen", s.put(&Node::StrLen(r("after1"))).unwrap()),
7697                    step("t2", idx(r("after1"), "\t")),
7698                    step("title", slice(r("after1"), num(0), r("t2"))),
7699                    step(
7700                        "pbody",
7701                        slice(
7702                            r("after1"),
7703                            t2p1.clone(),
7704                            bin(crate::node::BinOp::Sub, r("alen"), t2p1),
7705                        ),
7706                    ),
7707                ],
7708                result: s
7709                    .put(&Node::Record {
7710                        type_name: "Post".into(),
7711                        fields: vec![
7712                            (
7713                                "id".into(),
7714                                s.put(&Node::StrToNumber(r("ids"))).unwrap(),
7715                            ),
7716                            ("title".into(), r("title")),
7717                            ("body".into(), r("pbody")),
7718                        ],
7719                    })
7720                    .unwrap(),
7721            })
7722            .unwrap();
7723
7724        // parse_posts(rows) -> List<Post> : split on '\n', recurse.
7725        let nlp1 = bin(crate::node::BinOp::Add, r("nl"), num(1));
7726        let parse_posts = s
7727            .put(&Node::Function {
7728                name: "parse_posts".into(),
7729                type_params: vec![],
7730                params: vec![param("rows", Type::String)],
7731                produces: ext(post_list()),
7732                requires: BTreeSet::new(),
7733                on_failure: vec![],
7734                body: vec![
7735                    step("rl", s.put(&Node::StrLen(r("rows"))).unwrap()),
7736                    step("nl", idx(r("rows"), "\n")),
7737                ],
7738                result: s
7739                    .put(&Node::If {
7740                        cond: bin(crate::node::BinOp::Eq, r("rl"), num(0)),
7741                        then_branch: s
7742                            .put(&Node::ListEmpty {
7743                                elem: post_ty.clone(),
7744                            })
7745                            .unwrap(),
7746                        else_branch: s
7747                            .put(&Node::If {
7748                                cond: bin(
7749                                    crate::node::BinOp::Eq,
7750                                    r("nl"),
7751                                    num(-1),
7752                                ),
7753                                then_branch: s
7754                                    .put(&Node::ListCons {
7755                                        head: call(
7756                                            "parse_row",
7757                                            vec![r("rows")],
7758                                        ),
7759                                        tail: s
7760                                            .put(&Node::ListEmpty {
7761                                                elem: post_ty.clone(),
7762                                            })
7763                                            .unwrap(),
7764                                    })
7765                                    .unwrap(),
7766                                else_branch: s
7767                                    .put(&Node::ListCons {
7768                                        head: call(
7769                                            "parse_row",
7770                                            vec![slice(
7771                                                r("rows"),
7772                                                num(0),
7773                                                r("nl"),
7774                                            )],
7775                                        ),
7776                                        tail: call(
7777                                            "parse_posts",
7778                                            vec![slice(
7779                                                r("rows"),
7780                                                nlp1.clone(),
7781                                                bin(
7782                                                    crate::node::BinOp::Sub,
7783                                                    r("rl"),
7784                                                    nlp1,
7785                                                ),
7786                                            )],
7787                                        ),
7788                                    })
7789                                    .unwrap(),
7790                            })
7791                            .unwrap(),
7792                    })
7793                    .unwrap(),
7794            })
7795            .unwrap();
7796
7797        // posts_html(ps, i) -> String
7798        let pf = |f: &str| -> NodeHash {
7799            s.put(&Node::Field {
7800                base: s
7801                    .put(&Node::ListGet {
7802                        list: r("ps"),
7803                        index: r("i"),
7804                    })
7805                    .unwrap(),
7806                type_name: "Post".into(),
7807                field: f.into(),
7808            })
7809            .unwrap()
7810        };
7811        let posts_html = s
7812            .put(&Node::Function {
7813                name: "posts_html".into(),
7814                type_params: vec![],
7815                params: vec![param("ps", post_list()), param("i", Type::Number)],
7816                produces: ext(Type::String),
7817                requires: BTreeSet::new(),
7818                on_failure: vec![],
7819                body: vec![step("n", s.put(&Node::ListLen(r("ps"))).unwrap())],
7820                result: s
7821                    .put(&Node::If {
7822                        cond: bin(crate::node::BinOp::Eq, r("i"), r("n")),
7823                        then_branch: lit(""),
7824                        else_branch: cats(&[
7825                            lit("<li>"),
7826                            s.put(&Node::NumberToStr(pf("id"))).unwrap(),
7827                            lit(": "),
7828                            pf("title"),
7829                            lit("</li>"),
7830                            call(
7831                                "posts_html",
7832                                vec![
7833                                    r("ps"),
7834                                    bin(crate::node::BinOp::Add, r("i"), num(1)),
7835                                ],
7836                            ),
7837                        ]),
7838                    })
7839                    .unwrap(),
7840            })
7841            .unwrap();
7842
7843        // route(req) -> Response : db_query -> parse -> render.
7844        let mut db = BTreeSet::new();
7845        db.insert(crate::ty::Effect::Db);
7846        let route = s
7847            .put(&Node::Function {
7848                name: "route".into(),
7849                type_params: vec![],
7850                params: vec![param("req", Type::Named("Request".into()))],
7851                produces: ext(Type::Named("Response".into())),
7852                requires: db,
7853                on_failure: vec![],
7854                body: {
7855                    let none = || {
7856                        s.put(&Node::ListEmpty { elem: Type::String }).unwrap()
7857                    };
7858                    let dbq = |sql: &str| {
7859                        s.put(&Node::DbQuery {
7860                            sql: lit(sql),
7861                            params: none(),
7862                        })
7863                        .unwrap()
7864                    };
7865                    vec![
7866                        // Real in-memory SQLite: the connection is shared
7867                        // across these steps within the one request, so
7868                        // CREATE then INSERT then SELECT compose.
7869                        step(
7870                            "_c",
7871                            dbq(
7872                                "CREATE TABLE IF NOT EXISTS posts \
7873                                 (id INTEGER PRIMARY KEY, title TEXT, body TEXT)",
7874                            ),
7875                        ),
7876                        step(
7877                            "_i1",
7878                            dbq(
7879                                "INSERT INTO posts (title, body) \
7880                                 VALUES ('Hello World', 'The first post.')",
7881                            ),
7882                        ),
7883                        step(
7884                            "_i2",
7885                            dbq(
7886                                "INSERT INTO posts (title, body) VALUES \
7887                                 ('On Cairn', 'Programs as reasoning chains.')",
7888                            ),
7889                        ),
7890                        step(
7891                            "rows",
7892                            dbq("SELECT id, title, body FROM posts ORDER BY id"),
7893                        ),
7894                        step("ps", call("parse_posts", vec![r("rows")])),
7895                    ]
7896                },
7897                result: s
7898                    .put(&Node::Record {
7899                        type_name: "Response".into(),
7900                        fields: vec![
7901                            ("status".into(), num(200)),
7902                            (
7903                                "body".into(),
7904                                cats(&[
7905                                    lit("<ul>"),
7906                                    call("posts_html", vec![r("ps"), num(0)]),
7907                                    lit("</ul>"),
7908                                ]),
7909                            ),
7910                        ],
7911                    })
7912                    .unwrap(),
7913            })
7914            .unwrap();
7915
7916        let m = s
7917            .put(&Node::Module {
7918                name: "blog".into(),
7919                types: vec![post_def, request_def, response_def],
7920                functions: vec![route, parse_posts, parse_row, posts_html],
7921            })
7922            .unwrap();
7923        let report = crate::check::Checker::new(&s).check(&m).unwrap();
7924        assert!(
7925            report.ok(),
7926            "db-backed blog did not type-check: {:?}",
7927            report.violations
7928        );
7929        let wasm = lower(&s, &m).unwrap();
7930        assert_eq!(
7931            serve_request(&wasm, "route", "GET", "/posts", "").unwrap(),
7932            HttpResponse {
7933                status: 200,
7934                body: "<ul><li>1: Hello World</li><li>2: On Cairn</li></ul>"
7935                    .into(),
7936            }
7937        );
7938    }
7939
7940    #[test]
7941    fn a_persistent_sqlite_blog() {
7942        // Real persistence: request 1 (/seed) CREATEs + INSERTs via
7943        // db_query against a SQLite file; request 2 (/posts) — a separate
7944        // wasm instance that runs only SELECT — sees that data, proving it
7945        // outlived the instance because it lives in the file.
7946        let nanos = std::time::SystemTime::now()
7947            .duration_since(std::time::UNIX_EPOCH)
7948            .unwrap()
7949            .as_nanos();
7950        let db = std::env::temp_dir()
7951            .join(format!("cairn_sqlite_blog_{}_{nanos}.db", std::process::id()));
7952        let dbs = db.to_str().unwrap().to_string();
7953        let _ = std::fs::remove_file(&db);
7954
7955        let s = Store::open_in_memory().unwrap();
7956        let lit = |t: &str| -> NodeHash { s.put(&Node::Str(t.into())).unwrap() };
7957        let num = |n: i64| -> NodeHash { s.put(&Node::Lit(n)).unwrap() };
7958        let r = |name: &str| -> NodeHash { s.put(&Node::Ref(name.into())).unwrap() };
7959        let bin = |op, a: NodeHash, b: NodeHash| -> NodeHash {
7960            s.put(&Node::BinOp { op, lhs: a, rhs: b }).unwrap()
7961        };
7962        let step = |binding: &str, value: NodeHash| -> NodeHash {
7963            s.put(&Node::Step {
7964                binding: binding.into(),
7965                value,
7966            })
7967            .unwrap()
7968        };
7969        let slice = |str_: NodeHash, start: NodeHash, len: NodeHash| -> NodeHash {
7970            s.put(&Node::StrSlice {
7971                s: str_,
7972                start,
7973                len,
7974            })
7975            .unwrap()
7976        };
7977        let call = |f: &str, args: Vec<NodeHash>| -> NodeHash {
7978            s.put(&Node::Call {
7979                func: f.into(),
7980                args,
7981            })
7982            .unwrap()
7983        };
7984        let cats = |parts: &[NodeHash]| -> NodeHash {
7985            let mut it = parts.iter().cloned();
7986            let first = it.next().unwrap();
7987            it.fold(first, |acc, p| s.put(&Node::StrConcat(acc, p)).unwrap())
7988        };
7989        let param = |name: &str, ty: Type| Param {
7990            name: name.into(),
7991            ty,
7992            min_confidence: Confidence::External,
7993        };
7994        let ext = |ty: Type| Produces {
7995            ty,
7996            confidence: Confidence::External,
7997        };
7998        let post_ty = Type::Named("Post".into());
7999        let post_list = || Type::List(Box::new(Type::Named("Post".into())));
8000        let idx = |hay: NodeHash, needle: &str| -> NodeHash {
8001            s.put(&Node::StrIndexOf {
8002                haystack: hay,
8003                needle: lit(needle),
8004            })
8005            .unwrap()
8006        };
8007        let dbq = |sql: &str| -> NodeHash {
8008            s.put(&Node::DbQuery {
8009                sql: lit(sql),
8010                params: s.put(&Node::ListEmpty { elem: Type::String }).unwrap(),
8011            })
8012            .unwrap()
8013        };
8014
8015        let post_def = s
8016            .put(&Node::RecordDef {
8017                name: "Post".into(),
8018                fields: vec![
8019                    ("id".into(), Type::Number),
8020                    ("title".into(), Type::String),
8021                    ("body".into(), Type::String),
8022                ],
8023            })
8024            .unwrap();
8025        let request_def = s
8026            .put(&Node::RecordDef {
8027                name: "Request".into(),
8028                fields: vec![
8029                    ("method".into(), Type::String),
8030                    ("path".into(), Type::String),
8031                    ("body".into(), Type::String),
8032                ],
8033            })
8034            .unwrap();
8035        let response_def = s
8036            .put(&Node::RecordDef {
8037                name: "Response".into(),
8038                fields: vec![
8039                    ("status".into(), Type::Number),
8040                    ("body".into(), Type::String),
8041                ],
8042            })
8043            .unwrap();
8044
8045        // parse_row / parse_posts (same shape as the real-SQLite blog
8046        // test — every test hits the engine; no stub-backed path exists).
8047        let t1p1 = bin(crate::node::BinOp::Add, r("t1"), num(1));
8048        let t2p1 = bin(crate::node::BinOp::Add, r("t2"), num(1));
8049        let parse_row = s
8050            .put(&Node::Function {
8051                name: "parse_row".into(),
8052                type_params: vec![],
8053                params: vec![param("row", Type::String)],
8054                produces: ext(post_ty.clone()),
8055                requires: BTreeSet::new(),
8056                on_failure: vec![],
8057                body: vec![
8058                    step("rlen", s.put(&Node::StrLen(r("row"))).unwrap()),
8059                    step("t1", idx(r("row"), "\t")),
8060                    step("ids", slice(r("row"), num(0), r("t1"))),
8061                    step(
8062                        "after1",
8063                        slice(
8064                            r("row"),
8065                            t1p1.clone(),
8066                            bin(crate::node::BinOp::Sub, r("rlen"), t1p1),
8067                        ),
8068                    ),
8069                    step("alen", s.put(&Node::StrLen(r("after1"))).unwrap()),
8070                    step("t2", idx(r("after1"), "\t")),
8071                    step("title", slice(r("after1"), num(0), r("t2"))),
8072                    step(
8073                        "pbody",
8074                        slice(
8075                            r("after1"),
8076                            t2p1.clone(),
8077                            bin(crate::node::BinOp::Sub, r("alen"), t2p1),
8078                        ),
8079                    ),
8080                ],
8081                result: s
8082                    .put(&Node::Record {
8083                        type_name: "Post".into(),
8084                        fields: vec![
8085                            (
8086                                "id".into(),
8087                                s.put(&Node::StrToNumber(r("ids"))).unwrap(),
8088                            ),
8089                            ("title".into(), r("title")),
8090                            ("body".into(), r("pbody")),
8091                        ],
8092                    })
8093                    .unwrap(),
8094            })
8095            .unwrap();
8096        let nlp1 = bin(crate::node::BinOp::Add, r("nl"), num(1));
8097        let parse_posts = s
8098            .put(&Node::Function {
8099                name: "parse_posts".into(),
8100                type_params: vec![],
8101                params: vec![param("rows", Type::String)],
8102                produces: ext(post_list()),
8103                requires: BTreeSet::new(),
8104                on_failure: vec![],
8105                body: vec![
8106                    step("rl", s.put(&Node::StrLen(r("rows"))).unwrap()),
8107                    step("nl", idx(r("rows"), "\n")),
8108                ],
8109                result: s
8110                    .put(&Node::If {
8111                        cond: bin(crate::node::BinOp::Eq, r("rl"), num(0)),
8112                        then_branch: s
8113                            .put(&Node::ListEmpty {
8114                                elem: post_ty.clone(),
8115                            })
8116                            .unwrap(),
8117                        else_branch: s
8118                            .put(&Node::If {
8119                                cond: bin(
8120                                    crate::node::BinOp::Eq,
8121                                    r("nl"),
8122                                    num(-1),
8123                                ),
8124                                then_branch: s
8125                                    .put(&Node::ListCons {
8126                                        head: call(
8127                                            "parse_row",
8128                                            vec![r("rows")],
8129                                        ),
8130                                        tail: s
8131                                            .put(&Node::ListEmpty {
8132                                                elem: post_ty.clone(),
8133                                            })
8134                                            .unwrap(),
8135                                    })
8136                                    .unwrap(),
8137                                else_branch: s
8138                                    .put(&Node::ListCons {
8139                                        head: call(
8140                                            "parse_row",
8141                                            vec![slice(
8142                                                r("rows"),
8143                                                num(0),
8144                                                r("nl"),
8145                                            )],
8146                                        ),
8147                                        tail: call(
8148                                            "parse_posts",
8149                                            vec![slice(
8150                                                r("rows"),
8151                                                nlp1.clone(),
8152                                                bin(
8153                                                    crate::node::BinOp::Sub,
8154                                                    r("rl"),
8155                                                    nlp1,
8156                                                ),
8157                                            )],
8158                                        ),
8159                                    })
8160                                    .unwrap(),
8161                            })
8162                            .unwrap(),
8163                    })
8164                    .unwrap(),
8165            })
8166            .unwrap();
8167        let pf = |f: &str| -> NodeHash {
8168            s.put(&Node::Field {
8169                base: s
8170                    .put(&Node::ListGet {
8171                        list: r("ps"),
8172                        index: r("i"),
8173                    })
8174                    .unwrap(),
8175                type_name: "Post".into(),
8176                field: f.into(),
8177            })
8178            .unwrap()
8179        };
8180        let posts_html = s
8181            .put(&Node::Function {
8182                name: "posts_html".into(),
8183                type_params: vec![],
8184                params: vec![param("ps", post_list()), param("i", Type::Number)],
8185                produces: ext(Type::String),
8186                requires: BTreeSet::new(),
8187                on_failure: vec![],
8188                body: vec![step("n", s.put(&Node::ListLen(r("ps"))).unwrap())],
8189                result: s
8190                    .put(&Node::If {
8191                        cond: bin(crate::node::BinOp::Eq, r("i"), r("n")),
8192                        then_branch: lit(""),
8193                        else_branch: cats(&[
8194                            lit("<li>"),
8195                            s.put(&Node::NumberToStr(pf("id"))).unwrap(),
8196                            lit(": "),
8197                            pf("title"),
8198                            lit("</li>"),
8199                            call(
8200                                "posts_html",
8201                                vec![
8202                                    r("ps"),
8203                                    bin(crate::node::BinOp::Add, r("i"), num(1)),
8204                                ],
8205                            ),
8206                        ]),
8207                    })
8208                    .unwrap(),
8209            })
8210            .unwrap();
8211
8212        // route: /seed CREATE+INSERT (body = the three rowids "012");
8213        // /posts SELECT -> parse -> render (no writes).
8214        let resp = |status: i64, body: NodeHash| -> NodeHash {
8215            s.put(&Node::Record {
8216                type_name: "Response".into(),
8217                fields: vec![("status".into(), num(status)), ("body".into(), body)],
8218            })
8219            .unwrap()
8220        };
8221        let mut dbset = BTreeSet::new();
8222        dbset.insert(crate::ty::Effect::Db);
8223        let route = s
8224            .put(&Node::Function {
8225                name: "route".into(),
8226                type_params: vec![],
8227                params: vec![param("req", Type::Named("Request".into()))],
8228                produces: ext(Type::Named("Response".into())),
8229                requires: dbset,
8230                on_failure: vec![],
8231                body: vec![step(
8232                    "path",
8233                    s.put(&Node::Field {
8234                        base: r("req"),
8235                        type_name: "Request".into(),
8236                        field: "path".into(),
8237                    })
8238                    .unwrap(),
8239                )],
8240                result: s
8241                    .put(&Node::If {
8242                        cond: s
8243                            .put(&Node::StrEq(r("path"), lit("/seed")))
8244                            .unwrap(),
8245                        then_branch: resp(
8246                            200,
8247                            cats(&[
8248                                dbq("CREATE TABLE IF NOT EXISTS posts \
8249                                     (id INTEGER PRIMARY KEY, title TEXT, body TEXT)"),
8250                                dbq("INSERT INTO posts (title, body) \
8251                                     VALUES ('Hello World', 'The first post.')"),
8252                                dbq("INSERT INTO posts (title, body) VALUES \
8253                                     ('On Cairn', 'Programs as reasoning chains.')"),
8254                            ]),
8255                        ),
8256                        else_branch: s
8257                            .put(&Node::If {
8258                                cond: s
8259                                    .put(&Node::StrEq(r("path"), lit("/posts")))
8260                                    .unwrap(),
8261                                then_branch: resp(
8262                                    200,
8263                                    cats(&[
8264                                        lit("<ul>"),
8265                                        call(
8266                                            "posts_html",
8267                                            vec![
8268                                                call(
8269                                                    "parse_posts",
8270                                                    vec![dbq(
8271                                                        "SELECT id, title, body \
8272                                                         FROM posts ORDER BY id",
8273                                                    )],
8274                                                ),
8275                                                num(0),
8276                                            ],
8277                                        ),
8278                                        lit("</ul>"),
8279                                    ]),
8280                                ),
8281                                else_branch: resp(404, lit("not found")),
8282                            })
8283                            .unwrap(),
8284                    })
8285                    .unwrap(),
8286            })
8287            .unwrap();
8288
8289        let m = s
8290            .put(&Node::Module {
8291                name: "blog".into(),
8292                types: vec![post_def, request_def, response_def],
8293                functions: vec![route, parse_posts, parse_row, posts_html],
8294            })
8295            .unwrap();
8296        let report = crate::check::Checker::new(&s).check(&m).unwrap();
8297        assert!(
8298            report.ok(),
8299            "persistent blog did not type-check: {:?}",
8300            report.violations
8301        );
8302        let wasm = lower(&s, &m).unwrap();
8303
8304        // Request 1: seed (write). create -> rowid 0, two inserts -> 1, 2.
8305        assert_eq!(
8306            serve_request_db(&wasm, "route", &dbs, "POST", "/seed", "").unwrap(),
8307            HttpResponse {
8308                status: 200,
8309                body: "012".into(),
8310            }
8311        );
8312        // Request 2: a fresh instance that only SELECTs — the data is
8313        // there because it persisted to the SQLite file.
8314        assert_eq!(
8315            serve_request_db(&wasm, "route", &dbs, "GET", "/posts", "").unwrap(),
8316            HttpResponse {
8317                status: 200,
8318                body: "<ul><li>1: Hello World</li><li>2: On Cairn</li></ul>"
8319                    .into(),
8320            }
8321        );
8322        assert!(db.exists(), "the SQLite file should persist on disk");
8323
8324        let _ = std::fs::remove_file(&db);
8325        let _ = std::fs::remove_file(format!("{dbs}-journal"));
8326        let _ = std::fs::remove_file(format!("{dbs}-wal"));
8327        let _ = std::fs::remove_file(format!("{dbs}-shm"));
8328    }
8329}