Skip to main content

cairn_core/
stdlib.rs

1//! The standard library — written *in Cairn*, authored through the one
2//! Core API, exactly like the Tier-2 web framework.
3//!
4//! These are the generic higher-order list functions every real language
5//! has (`map`/`filter`/`fold`). They exist here, in Cairn, only because
6//! the language now has the two things that make them expressible: real
7//! generics and first-class functions/closures. Before the keystone, code
8//! that needed them hand-rolled a bespoke index recursion every time (see
9//! the framework's old `render_kids`); now it passes a closure.
10//!
11//! Each public function is a thin entry over an index-recursive `_at`
12//! helper — there is no drop-first list primitive, so iteration is by
13//! index. All are pure: the closure's `Fn` type carries no effects (an
14//! effect-polymorphic stdlib would need effect rows — out of scope, a
15//! documented v0.4 boundary). Calls among them resolve when an app
16//! assembles its module; the content-addressed store dedups them.
17
18use crate::edit::{ExprSpec, FunctionSpec, StepSpec};
19use crate::node::{BinOp, Param, Produces};
20use crate::ty::{Confidence, Type};
21use std::collections::BTreeSet;
22
23fn r(n: &str) -> ExprSpec {
24    ExprSpec::Ref(n.into())
25}
26fn num(n: i64) -> ExprSpec {
27    ExprSpec::Lit(n)
28}
29fn add1(e: ExprSpec) -> ExprSpec {
30    ExprSpec::BinOp {
31        op: BinOp::Add,
32        lhs: Box::new(e),
33        rhs: Box::new(num(1)),
34    }
35}
36fn eq(a: ExprSpec, b: ExprSpec) -> ExprSpec {
37    ExprSpec::BinOp {
38        op: BinOp::Eq,
39        lhs: Box::new(a),
40        rhs: Box::new(b),
41    }
42}
43fn call(f: &str, args: Vec<ExprSpec>) -> ExprSpec {
44    ExprSpec::Call {
45        func: f.into(),
46        args,
47    }
48}
49fn callv(callee: ExprSpec, args: Vec<ExprSpec>) -> ExprSpec {
50    ExprSpec::CallValue {
51        callee: Box::new(callee),
52        args,
53    }
54}
55fn iff(c: ExprSpec, t: ExprSpec, e: ExprSpec) -> ExprSpec {
56    ExprSpec::If {
57        cond: Box::new(c),
58        then_branch: Box::new(t),
59        else_branch: Box::new(e),
60    }
61}
62fn lget(list: ExprSpec, index: ExprSpec) -> ExprSpec {
63    ExprSpec::ListGet {
64        list: Box::new(list),
65        index: Box::new(index),
66    }
67}
68fn llen(e: ExprSpec) -> ExprSpec {
69    ExprSpec::ListLen(Box::new(e))
70}
71fn cons(head: ExprSpec, tail: ExprSpec) -> ExprSpec {
72    ExprSpec::ListCons {
73        head: Box::new(head),
74        tail: Box::new(tail),
75    }
76}
77fn empty(elem: Type) -> ExprSpec {
78    ExprSpec::ListEmpty { elem }
79}
80fn boolean(b: bool) -> ExprSpec {
81    ExprSpec::Bool(b)
82}
83fn some(v: ExprSpec) -> ExprSpec {
84    ExprSpec::OptionSome(Box::new(v))
85}
86fn none(elem: Type) -> ExprSpec {
87    ExprSpec::OptionNone { elem }
88}
89fn p(name: &str, ty: Type) -> Param {
90    Param {
91        name: name.into(),
92        ty,
93        min_confidence: Confidence::External,
94    }
95}
96fn ext(ty: Type) -> Produces {
97    Produces {
98        ty,
99        confidence: Confidence::External,
100    }
101}
102fn step(b: &str, v: ExprSpec) -> StepSpec {
103    StepSpec {
104        binding: b.into(),
105        value: v,
106    }
107}
108fn var(n: &str) -> Type {
109    Type::Var(n.into())
110}
111fn list(t: Type) -> Type {
112    Type::List(Box::new(t))
113}
114/// A pure function type `Fn(params) -> ret`.
115fn fnt(params: Vec<Type>, ret: Type) -> Type {
116    Type::Fn {
117        params,
118        ret: Box::new(ret),
119        effects: BTreeSet::new(),
120    }
121}
122fn tp(names: &[&str]) -> Vec<String> {
123    names.iter().map(|s| (*s).to_string()).collect()
124}
125
126/// `map`, `filter`, `fold` (each with its index-recursive `_at` helper).
127pub fn list_functions() -> Vec<FunctionSpec> {
128    // fold_at<T,A>(f, acc, xs, i) = if i == len(xs) then acc
129    //   else fold_at(f, f(acc, xs[i]), xs, i + 1)
130    let fold_at = FunctionSpec {
131        name: "fold_at".into(),
132        type_params: tp(&["T", "A"]),
133        params: vec![
134            p("f", fnt(vec![var("A"), var("T")], var("A"))),
135            p("acc", var("A")),
136            p("xs", list(var("T"))),
137            p("i", Type::Number),
138        ],
139        produces: ext(var("A")),
140        requires: BTreeSet::new(),
141        on_failure: vec![],
142        steps: vec![step("n", llen(r("xs")))],
143        result: iff(
144            eq(r("i"), r("n")),
145            r("acc"),
146            call(
147                "fold_at",
148                vec![
149                    r("f"),
150                    callv(r("f"), vec![r("acc"), lget(r("xs"), r("i"))]),
151                    r("xs"),
152                    add1(r("i")),
153                ],
154            ),
155        ),
156    };
157    let fold = FunctionSpec {
158        name: "fold".into(),
159        type_params: tp(&["T", "A"]),
160        params: vec![
161            p("f", fnt(vec![var("A"), var("T")], var("A"))),
162            p("acc", var("A")),
163            p("xs", list(var("T"))),
164        ],
165        produces: ext(var("A")),
166        requires: BTreeSet::new(),
167        on_failure: vec![],
168        steps: vec![],
169        result: call("fold_at", vec![r("f"), r("acc"), r("xs"), num(0)]),
170    };
171
172    // map_at<T,U>(f, xs, i) = if i == len(xs) then []
173    //   else cons(f(xs[i]), map_at(f, xs, i + 1))
174    let map_at = FunctionSpec {
175        name: "map_at".into(),
176        type_params: tp(&["T", "U"]),
177        params: vec![
178            p("f", fnt(vec![var("T")], var("U"))),
179            p("xs", list(var("T"))),
180            p("i", Type::Number),
181        ],
182        produces: ext(list(var("U"))),
183        requires: BTreeSet::new(),
184        on_failure: vec![],
185        steps: vec![step("n", llen(r("xs")))],
186        result: iff(
187            eq(r("i"), r("n")),
188            empty(var("U")),
189            cons(
190                callv(r("f"), vec![lget(r("xs"), r("i"))]),
191                call("map_at", vec![r("f"), r("xs"), add1(r("i"))]),
192            ),
193        ),
194    };
195    let map = FunctionSpec {
196        name: "map".into(),
197        type_params: tp(&["T", "U"]),
198        params: vec![
199            p("f", fnt(vec![var("T")], var("U"))),
200            p("xs", list(var("T"))),
201        ],
202        produces: ext(list(var("U"))),
203        requires: BTreeSet::new(),
204        on_failure: vec![],
205        steps: vec![],
206        result: call("map_at", vec![r("f"), r("xs"), num(0)]),
207    };
208
209    // filter_at<T>(f, xs, i) = if i == len(xs) then []
210    //   else if f(xs[i]) then cons(xs[i], rest) else rest
211    let filter_at = FunctionSpec {
212        name: "filter_at".into(),
213        type_params: tp(&["T"]),
214        params: vec![
215            p("f", fnt(vec![var("T")], Type::Bool)),
216            p("xs", list(var("T"))),
217            p("i", Type::Number),
218        ],
219        produces: ext(list(var("T"))),
220        requires: BTreeSet::new(),
221        on_failure: vec![],
222        steps: vec![step("n", llen(r("xs")))],
223        result: iff(
224            eq(r("i"), r("n")),
225            empty(var("T")),
226            iff(
227                callv(r("f"), vec![lget(r("xs"), r("i"))]),
228                cons(
229                    lget(r("xs"), r("i")),
230                    call("filter_at", vec![r("f"), r("xs"), add1(r("i"))]),
231                ),
232                call("filter_at", vec![r("f"), r("xs"), add1(r("i"))]),
233            ),
234        ),
235    };
236    let filter = FunctionSpec {
237        name: "filter".into(),
238        type_params: tp(&["T"]),
239        params: vec![
240            p("f", fnt(vec![var("T")], Type::Bool)),
241            p("xs", list(var("T"))),
242        ],
243        produces: ext(list(var("T"))),
244        requires: BTreeSet::new(),
245        on_failure: vec![],
246        steps: vec![],
247        result: call("filter_at", vec![r("f"), r("xs"), num(0)]),
248    };
249
250    // range_at(lo, hi) = if lo == hi then [] else cons(lo, range_at(lo+1, hi))
251    let range_at = FunctionSpec {
252        name: "range_at".into(),
253        type_params: vec![],
254        params: vec![p("lo", Type::Number), p("hi", Type::Number)],
255        produces: ext(list(Type::Number)),
256        requires: BTreeSet::new(),
257        on_failure: vec![],
258        steps: vec![],
259        result: iff(
260            eq(r("lo"), r("hi")),
261            empty(Type::Number),
262            cons(
263                r("lo"),
264                call("range_at", vec![add1(r("lo")), r("hi")]),
265            ),
266        ),
267    };
268    let range = FunctionSpec {
269        name: "range".into(),
270        type_params: vec![],
271        params: vec![p("lo", Type::Number), p("hi", Type::Number)],
272        produces: ext(list(Type::Number)),
273        requires: BTreeSet::new(),
274        on_failure: vec![],
275        steps: vec![],
276        result: call("range_at", vec![r("lo"), r("hi")]),
277    };
278
279    // any_at<T>(f, xs, i) = if i==len then false
280    //   else if f(xs[i]) then true else any_at(f, xs, i+1)
281    let any_at = FunctionSpec {
282        name: "any_at".into(),
283        type_params: tp(&["T"]),
284        params: vec![
285            p("f", fnt(vec![var("T")], Type::Bool)),
286            p("xs", list(var("T"))),
287            p("i", Type::Number),
288        ],
289        produces: ext(Type::Bool),
290        requires: BTreeSet::new(),
291        on_failure: vec![],
292        steps: vec![step("n", llen(r("xs")))],
293        result: iff(
294            eq(r("i"), r("n")),
295            boolean(false),
296            iff(
297                callv(r("f"), vec![lget(r("xs"), r("i"))]),
298                boolean(true),
299                call("any_at", vec![r("f"), r("xs"), add1(r("i"))]),
300            ),
301        ),
302    };
303    let any = FunctionSpec {
304        name: "any".into(),
305        type_params: tp(&["T"]),
306        params: vec![
307            p("f", fnt(vec![var("T")], Type::Bool)),
308            p("xs", list(var("T"))),
309        ],
310        produces: ext(Type::Bool),
311        requires: BTreeSet::new(),
312        on_failure: vec![],
313        steps: vec![],
314        result: call("any_at", vec![r("f"), r("xs"), num(0)]),
315    };
316
317    // all_at<T>(f, xs, i) = if i==len then true
318    //   else if f(xs[i]) then all_at(f, xs, i+1) else false
319    let all_at = FunctionSpec {
320        name: "all_at".into(),
321        type_params: tp(&["T"]),
322        params: vec![
323            p("f", fnt(vec![var("T")], Type::Bool)),
324            p("xs", list(var("T"))),
325            p("i", Type::Number),
326        ],
327        produces: ext(Type::Bool),
328        requires: BTreeSet::new(),
329        on_failure: vec![],
330        steps: vec![step("n", llen(r("xs")))],
331        result: iff(
332            eq(r("i"), r("n")),
333            boolean(true),
334            iff(
335                callv(r("f"), vec![lget(r("xs"), r("i"))]),
336                call("all_at", vec![r("f"), r("xs"), add1(r("i"))]),
337                boolean(false),
338            ),
339        ),
340    };
341    let all = FunctionSpec {
342        name: "all".into(),
343        type_params: tp(&["T"]),
344        params: vec![
345            p("f", fnt(vec![var("T")], Type::Bool)),
346            p("xs", list(var("T"))),
347        ],
348        produces: ext(Type::Bool),
349        requires: BTreeSet::new(),
350        on_failure: vec![],
351        steps: vec![],
352        result: call("all_at", vec![r("f"), r("xs"), num(0)]),
353    };
354
355    // find_at<T>(f, xs, i) = if i==len then None
356    //   else if f(xs[i]) then Some(xs[i]) else find_at(f, xs, i+1)
357    let find_at = FunctionSpec {
358        name: "find_at".into(),
359        type_params: tp(&["T"]),
360        params: vec![
361            p("f", fnt(vec![var("T")], Type::Bool)),
362            p("xs", list(var("T"))),
363            p("i", Type::Number),
364        ],
365        produces: ext(Type::Option(Box::new(var("T")))),
366        requires: BTreeSet::new(),
367        on_failure: vec![],
368        steps: vec![step("n", llen(r("xs")))],
369        result: iff(
370            eq(r("i"), r("n")),
371            none(var("T")),
372            iff(
373                callv(r("f"), vec![lget(r("xs"), r("i"))]),
374                some(lget(r("xs"), r("i"))),
375                call("find_at", vec![r("f"), r("xs"), add1(r("i"))]),
376            ),
377        ),
378    };
379    let find = FunctionSpec {
380        name: "find".into(),
381        type_params: tp(&["T"]),
382        params: vec![
383            p("f", fnt(vec![var("T")], Type::Bool)),
384            p("xs", list(var("T"))),
385        ],
386        produces: ext(Type::Option(Box::new(var("T")))),
387        requires: BTreeSet::new(),
388        on_failure: vec![],
389        steps: vec![],
390        result: call("find_at", vec![r("f"), r("xs"), num(0)]),
391    };
392
393    // concat_at<T>(a, b, i) = if i == len(a) then b
394    //   else cons(a[i], concat_at(a, b, i + 1))   — a ++ b, in order.
395    let concat_at = FunctionSpec {
396        name: "concat_at".into(),
397        type_params: tp(&["T"]),
398        params: vec![
399            p("a", list(var("T"))),
400            p("b", list(var("T"))),
401            p("i", Type::Number),
402        ],
403        produces: ext(list(var("T"))),
404        requires: BTreeSet::new(),
405        on_failure: vec![],
406        steps: vec![step("n", llen(r("a")))],
407        result: iff(
408            eq(r("i"), r("n")),
409            r("b"),
410            cons(
411                lget(r("a"), r("i")),
412                call("concat_at", vec![r("a"), r("b"), add1(r("i"))]),
413            ),
414        ),
415    };
416    let concat = FunctionSpec {
417        name: "concat".into(),
418        type_params: tp(&["T"]),
419        params: vec![p("a", list(var("T"))), p("b", list(var("T")))],
420        produces: ext(list(var("T"))),
421        requires: BTreeSet::new(),
422        on_failure: vec![],
423        steps: vec![],
424        result: call("concat_at", vec![r("a"), r("b"), num(0)]),
425    };
426
427    // lpad0(s, w) = while len(s) < w: s = "0" ++ s
428    let s_str = || ExprSpec::Str("0".into());
429    let strlen = |e: ExprSpec| ExprSpec::StrLen(Box::new(e));
430    let scat = |a: ExprSpec, b: ExprSpec| {
431        ExprSpec::StrConcat(Box::new(a), Box::new(b))
432    };
433    let n2s = |e: ExprSpec| ExprSpec::NumberToStr(Box::new(e));
434    let bop = |op: BinOp, a: ExprSpec, b: ExprSpec| ExprSpec::BinOp {
435        op,
436        lhs: Box::new(a),
437        rhs: Box::new(b),
438    };
439    let lpad0 = FunctionSpec {
440        name: "lpad0".into(),
441        type_params: vec![],
442        params: vec![p("s", Type::String), p("w", Type::Number)],
443        produces: ext(Type::String),
444        requires: BTreeSet::new(),
445        on_failure: vec![],
446        steps: vec![step("ls", strlen(r("s")))],
447        result: iff(
448            bop(BinOp::Ge, r("ls"), r("w")),
449            r("s"),
450            call("lpad0", vec![scat(s_str(), r("s")), r("w")]),
451        ),
452    };
453    // decimal_to_str(d): "[-]W.FFFF" from the scaled mantissa. Reuses
454    // the now-full-range num_to_str. (Magnitude negation overflows only
455    // at Decimal i64::MIN — a ~9e14 value, the same documented edge as
456    // every other magnitude op.)
457    let decimal_to_str = FunctionSpec {
458        name: "decimal_to_str".into(),
459        type_params: vec![],
460        params: vec![p("d", Type::Decimal)],
461        produces: ext(Type::String),
462        requires: BTreeSet::new(),
463        on_failure: vec![],
464        steps: vec![
465            step("raw", ExprSpec::DecimalRaw(Box::new(r("d")))),
466            step(
467                "m",
468                iff(
469                    bop(BinOp::Lt, r("raw"), num(0)),
470                    bop(BinOp::Sub, num(0), r("raw")),
471                    r("raw"),
472                ),
473            ),
474            step("whole", bop(BinOp::Div, r("m"), num(10_000))),
475            step("frac", bop(BinOp::Mod, r("m"), num(10_000))),
476        ],
477        result: scat(
478            scat(
479                scat(
480                    iff(
481                        bop(BinOp::Lt, r("raw"), num(0)),
482                        ExprSpec::Str("-".into()),
483                        ExprSpec::Str("".into()),
484                    ),
485                    n2s(r("whole")),
486                ),
487                ExprSpec::Str(".".into()),
488            ),
489            call("lpad0", vec![n2s(r("frac")), num(4)]),
490        ),
491    };
492
493    // take_at<T>(xs, k, i) = if i >= k or i == len(xs) then []
494    //   else cons(xs[i], take_at(xs, k, i + 1))   — first k, clamped.
495    let take_at = FunctionSpec {
496        name: "take_at".into(),
497        type_params: tp(&["T"]),
498        params: vec![
499            p("xs", list(var("T"))),
500            p("k", Type::Number),
501            p("i", Type::Number),
502        ],
503        produces: ext(list(var("T"))),
504        requires: BTreeSet::new(),
505        on_failure: vec![],
506        steps: vec![step("n", llen(r("xs")))],
507        result: iff(
508            bop(BinOp::Or, bop(BinOp::Ge, r("i"), r("k")), eq(r("i"), r("n"))),
509            empty(var("T")),
510            cons(
511                lget(r("xs"), r("i")),
512                call("take_at", vec![r("xs"), r("k"), add1(r("i"))]),
513            ),
514        ),
515    };
516    let take = FunctionSpec {
517        name: "take".into(),
518        type_params: tp(&["T"]),
519        params: vec![p("xs", list(var("T"))), p("k", Type::Number)],
520        produces: ext(list(var("T"))),
521        requires: BTreeSet::new(),
522        on_failure: vec![],
523        steps: vec![],
524        result: call("take_at", vec![r("xs"), r("k"), num(0)]),
525    };
526    // drop_at<T>(xs, k, i) = if i == len then []
527    //   else if i < k then drop_at(xs,k,i+1)
528    //   else cons(xs[i], drop_at(xs,k,i+1))       — all but first k.
529    let drop_at = FunctionSpec {
530        name: "drop_at".into(),
531        type_params: tp(&["T"]),
532        params: vec![
533            p("xs", list(var("T"))),
534            p("k", Type::Number),
535            p("i", Type::Number),
536        ],
537        produces: ext(list(var("T"))),
538        requires: BTreeSet::new(),
539        on_failure: vec![],
540        steps: vec![step("n", llen(r("xs")))],
541        result: iff(
542            eq(r("i"), r("n")),
543            empty(var("T")),
544            iff(
545                bop(BinOp::Lt, r("i"), r("k")),
546                call("drop_at", vec![r("xs"), r("k"), add1(r("i"))]),
547                cons(
548                    lget(r("xs"), r("i")),
549                    call("drop_at", vec![r("xs"), r("k"), add1(r("i"))]),
550                ),
551            ),
552        ),
553    };
554    let drop = FunctionSpec {
555        name: "drop".into(),
556        type_params: tp(&["T"]),
557        params: vec![p("xs", list(var("T"))), p("k", Type::Number)],
558        produces: ext(list(var("T"))),
559        requires: BTreeSet::new(),
560        on_failure: vec![],
561        steps: vec![],
562        result: call("drop_at", vec![r("xs"), r("k"), num(0)]),
563    };
564
565    // rev_at<T>(xs, i) = if i < 0 then [] else cons(xs[i], rev_at(xs,i-1))
566    let rev_at = FunctionSpec {
567        name: "rev_at".into(),
568        type_params: tp(&["T"]),
569        params: vec![p("xs", list(var("T"))), p("i", Type::Number)],
570        produces: ext(list(var("T"))),
571        requires: BTreeSet::new(),
572        on_failure: vec![],
573        steps: vec![],
574        result: iff(
575            bop(BinOp::Lt, r("i"), num(0)),
576            empty(var("T")),
577            cons(
578                lget(r("xs"), r("i")),
579                call(
580                    "rev_at",
581                    vec![r("xs"), bop(BinOp::Sub, r("i"), num(1))],
582                ),
583            ),
584        ),
585    };
586    let reverse = FunctionSpec {
587        name: "reverse".into(),
588        type_params: tp(&["T"]),
589        params: vec![p("xs", list(var("T")))],
590        produces: ext(list(var("T"))),
591        requires: BTreeSet::new(),
592        on_failure: vec![],
593        steps: vec![],
594        result: call(
595            "rev_at",
596            vec![r("xs"), bop(BinOp::Sub, llen(r("xs")), num(1))],
597        ),
598    };
599    // flat_map_at<T,U>(f, xs, i) = if i == len then []
600    //   else concat(f(xs[i]), flat_map_at(f, xs, i + 1))
601    let flat_map_at = FunctionSpec {
602        name: "flat_map_at".into(),
603        type_params: tp(&["T", "U"]),
604        params: vec![
605            p("f", fnt(vec![var("T")], list(var("U")))),
606            p("xs", list(var("T"))),
607            p("i", Type::Number),
608        ],
609        produces: ext(list(var("U"))),
610        requires: BTreeSet::new(),
611        on_failure: vec![],
612        steps: vec![step("n", llen(r("xs")))],
613        result: iff(
614            eq(r("i"), r("n")),
615            empty(var("U")),
616            call(
617                "concat",
618                vec![
619                    callv(r("f"), vec![lget(r("xs"), r("i"))]),
620                    call("flat_map_at", vec![r("f"), r("xs"), add1(r("i"))]),
621                ],
622            ),
623        ),
624    };
625    let flat_map = FunctionSpec {
626        name: "flat_map".into(),
627        type_params: tp(&["T", "U"]),
628        params: vec![
629            p("f", fnt(vec![var("T")], list(var("U")))),
630            p("xs", list(var("T"))),
631        ],
632        produces: ext(list(var("U"))),
633        requires: BTreeSet::new(),
634        on_failure: vec![],
635        steps: vec![],
636        result: call("flat_map_at", vec![r("f"), r("xs"), num(0)]),
637    };
638
639    // ins<T>(le, x, ys) — insert x into the already-sorted ys before
640    // the first element it is `le`-before-or-equal to. `le` is threaded
641    // explicitly (never captured) so no closure holds a function value.
642    let ins = FunctionSpec {
643        name: "ins".into(),
644        type_params: tp(&["T"]),
645        params: vec![
646            p("le", fnt(vec![var("T"), var("T")], Type::Bool)),
647            p("x", var("T")),
648            p("ys", list(var("T"))),
649        ],
650        produces: ext(list(var("T"))),
651        requires: BTreeSet::new(),
652        on_failure: vec![],
653        steps: vec![step("n", llen(r("ys")))],
654        result: iff(
655            eq(r("n"), num(0)),
656            cons(r("x"), empty(var("T"))),
657            iff(
658                callv(r("le"), vec![r("x"), lget(r("ys"), num(0))]),
659                cons(r("x"), r("ys")),
660                cons(
661                    lget(r("ys"), num(0)),
662                    call(
663                        "ins",
664                        vec![
665                            r("le"),
666                            r("x"),
667                            call("drop", vec![r("ys"), num(1)]),
668                        ],
669                    ),
670                ),
671            ),
672        ),
673    };
674    // sort_at<T>(le, xs, i, acc) = insertion sort (O(n^2) — fine for a
675    // stdlib; documented). acc stays sorted; each xs[i] is `ins`-ed.
676    let sort_at = FunctionSpec {
677        name: "sort_at".into(),
678        type_params: tp(&["T"]),
679        params: vec![
680            p("le", fnt(vec![var("T"), var("T")], Type::Bool)),
681            p("xs", list(var("T"))),
682            p("i", Type::Number),
683            p("acc", list(var("T"))),
684        ],
685        produces: ext(list(var("T"))),
686        requires: BTreeSet::new(),
687        on_failure: vec![],
688        steps: vec![step("n", llen(r("xs")))],
689        result: iff(
690            eq(r("i"), r("n")),
691            r("acc"),
692            call(
693                "sort_at",
694                vec![
695                    r("le"),
696                    r("xs"),
697                    add1(r("i")),
698                    call("ins", vec![r("le"), lget(r("xs"), r("i")), r("acc")]),
699                ],
700            ),
701        ),
702    };
703    let sort_by = FunctionSpec {
704        name: "sort_by".into(),
705        type_params: tp(&["T"]),
706        params: vec![
707            p("le", fnt(vec![var("T"), var("T")], Type::Bool)),
708            p("xs", list(var("T"))),
709        ],
710        produces: ext(list(var("T"))),
711        requires: BTreeSet::new(),
712        on_failure: vec![],
713        steps: vec![],
714        result: call(
715            "sort_at",
716            vec![r("le"), r("xs"), num(0), empty(var("T"))],
717        ),
718    };
719
720    // zip_at<A,B,C>(f, as, bs, i) = stop at the shorter length;
721    //   else cons(f(as[i], bs[i]), zip_at(f, as, bs, i + 1))
722    let zip_at = FunctionSpec {
723        name: "zip_at".into(),
724        type_params: tp(&["A", "B", "C"]),
725        params: vec![
726            p("f", fnt(vec![var("A"), var("B")], var("C"))),
727            p("as", list(var("A"))),
728            p("bs", list(var("B"))),
729            p("i", Type::Number),
730        ],
731        produces: ext(list(var("C"))),
732        requires: BTreeSet::new(),
733        on_failure: vec![],
734        steps: vec![
735            step("na", llen(r("as"))),
736            step("nb", llen(r("bs"))),
737        ],
738        result: iff(
739            bop(
740                BinOp::Or,
741                bop(BinOp::Ge, r("i"), r("na")),
742                bop(BinOp::Ge, r("i"), r("nb")),
743            ),
744            empty(var("C")),
745            cons(
746                callv(
747                    r("f"),
748                    vec![lget(r("as"), r("i")), lget(r("bs"), r("i"))],
749                ),
750                call("zip_at", vec![r("f"), r("as"), r("bs"), add1(r("i"))]),
751            ),
752        ),
753    };
754    let zip_with = FunctionSpec {
755        name: "zip_with".into(),
756        type_params: tp(&["A", "B", "C"]),
757        params: vec![
758            p("f", fnt(vec![var("A"), var("B")], var("C"))),
759            p("as", list(var("A"))),
760            p("bs", list(var("B"))),
761        ],
762        produces: ext(list(var("C"))),
763        requires: BTreeSet::new(),
764        on_failure: vec![],
765        steps: vec![],
766        result: call("zip_at", vec![r("f"), r("as"), r("bs"), num(0)]),
767    };
768    // dd_at<T>(eq, xs, i) — adjacent dedup (Unix `uniq`): drop xs[i]
769    //   when it `eq`s xs[i-1]. `eq` threaded explicitly (no capture).
770    let dd_at = FunctionSpec {
771        name: "dd_at".into(),
772        type_params: tp(&["T"]),
773        params: vec![
774            p("eq", fnt(vec![var("T"), var("T")], Type::Bool)),
775            p("xs", list(var("T"))),
776            p("i", Type::Number),
777        ],
778        produces: ext(list(var("T"))),
779        requires: BTreeSet::new(),
780        on_failure: vec![],
781        steps: vec![step("n", llen(r("xs")))],
782        result: iff(
783            eq(r("i"), r("n")),
784            empty(var("T")),
785            iff(
786                bop(BinOp::Eq, r("i"), num(0)),
787                cons(
788                    lget(r("xs"), num(0)),
789                    call("dd_at", vec![r("eq"), r("xs"), num(1)]),
790                ),
791                iff(
792                    callv(
793                        r("eq"),
794                        vec![
795                            lget(r("xs"), r("i")),
796                            lget(r("xs"), bop(BinOp::Sub, r("i"), num(1))),
797                        ],
798                    ),
799                    call("dd_at", vec![r("eq"), r("xs"), add1(r("i"))]),
800                    cons(
801                        lget(r("xs"), r("i")),
802                        call("dd_at", vec![r("eq"), r("xs"), add1(r("i"))]),
803                    ),
804                ),
805            ),
806        ),
807    };
808    let dedup_by = FunctionSpec {
809        name: "dedup_by".into(),
810        type_params: tp(&["T"]),
811        params: vec![
812            p("eq", fnt(vec![var("T"), var("T")], Type::Bool)),
813            p("xs", list(var("T"))),
814        ],
815        produces: ext(list(var("T"))),
816        requires: BTreeSet::new(),
817        on_failure: vec![],
818        steps: vec![],
819        result: call("dd_at", vec![r("eq"), r("xs"), num(0)]),
820    };
821
822    // join_at(sep, xs, i) = ""           if i == len
823    //   xs[0] ++ rest                    if i == 0
824    //   sep ++ xs[i] ++ rest             otherwise
825    let join_at = FunctionSpec {
826        name: "join_at".into(),
827        type_params: vec![],
828        params: vec![
829            p("sep", Type::String),
830            p("xs", list(Type::String)),
831            p("i", Type::Number),
832        ],
833        produces: ext(Type::String),
834        requires: BTreeSet::new(),
835        on_failure: vec![],
836        steps: vec![step("n", llen(r("xs")))],
837        result: iff(
838            eq(r("i"), r("n")),
839            ExprSpec::Str(String::new()),
840            iff(
841                eq(r("i"), num(0)),
842                scat(
843                    lget(r("xs"), num(0)),
844                    call("join_at", vec![r("sep"), r("xs"), num(1)]),
845                ),
846                scat(
847                    scat(r("sep"), lget(r("xs"), r("i"))),
848                    call("join_at", vec![r("sep"), r("xs"), add1(r("i"))]),
849                ),
850            ),
851        ),
852    };
853    let str_join = FunctionSpec {
854        name: "str_join".into(),
855        type_params: vec![],
856        params: vec![p("sep", Type::String), p("xs", list(Type::String))],
857        produces: ext(Type::String),
858        requires: BTreeSet::new(),
859        on_failure: vec![],
860        steps: vec![],
861        result: call("join_at", vec![r("sep"), r("xs"), num(0)]),
862    };
863    // rep_at(s, n, i) = if i >= n then "" else s ++ rep_at(s, n, i+1)
864    let rep_at = FunctionSpec {
865        name: "rep_at".into(),
866        type_params: vec![],
867        params: vec![
868            p("s", Type::String),
869            p("n", Type::Number),
870            p("i", Type::Number),
871        ],
872        produces: ext(Type::String),
873        requires: BTreeSet::new(),
874        on_failure: vec![],
875        steps: vec![],
876        result: iff(
877            bop(BinOp::Ge, r("i"), r("n")),
878            ExprSpec::Str(String::new()),
879            scat(
880                r("s"),
881                call("rep_at", vec![r("s"), r("n"), add1(r("i"))]),
882            ),
883        ),
884    };
885    let str_repeat = FunctionSpec {
886        name: "str_repeat".into(),
887        type_params: vec![],
888        params: vec![p("s", Type::String), p("n", Type::Number)],
889        produces: ext(Type::String),
890        requires: BTreeSet::new(),
891        on_failure: vec![],
892        steps: vec![],
893        result: call("rep_at", vec![r("s"), r("n"), num(0)]),
894    };
895
896    let sl = |s: ExprSpec, a: ExprSpec, b: ExprSpec| ExprSpec::StrSlice {
897        s: Box::new(s),
898        start: Box::new(a),
899        len: Box::new(b),
900    };
901    let slen_ = |x: ExprSpec| ExprSpec::StrLen(Box::new(x));
902    let sidx = |h: ExprSpec, n: ExprSpec| ExprSpec::StrIndexOf {
903        haystack: Box::new(h),
904        needle: Box::new(n),
905    };
906    let seq = |a: ExprSpec, b: ExprSpec| ExprSpec::StrEq(Box::new(a), Box::new(b));
907    let sub1 = |x: ExprSpec| bop(BinOp::Sub, x, num(1));
908    // is_ws(c) — c is one byte; ASCII space / tab / NL / CR.
909    let is_ws = FunctionSpec {
910        name: "is_ws".into(),
911        type_params: vec![],
912        params: vec![p("c", Type::String)],
913        produces: ext(Type::Bool),
914        requires: BTreeSet::new(),
915        on_failure: vec![],
916        steps: vec![],
917        result: bop(
918            BinOp::Or,
919            bop(
920                BinOp::Or,
921                seq(r("c"), ExprSpec::Str(" ".into())),
922                seq(r("c"), ExprSpec::Str("\t".into())),
923            ),
924            bop(
925                BinOp::Or,
926                seq(r("c"), ExprSpec::Str("\n".into())),
927                seq(r("c"), ExprSpec::Str("\r".into())),
928            ),
929        ),
930    };
931    // lead(s, i) — first non-ws index (== len if all ws / empty).
932    let lead = FunctionSpec {
933        name: "lead".into(),
934        type_params: vec![],
935        params: vec![p("s", Type::String), p("i", Type::Number)],
936        produces: ext(Type::Number),
937        requires: BTreeSet::new(),
938        on_failure: vec![],
939        steps: vec![step("n", slen_(r("s")))],
940        result: iff(
941            bop(BinOp::Ge, r("i"), r("n")),
942            r("n"),
943            iff(
944                call("is_ws", vec![sl(r("s"), r("i"), num(1))]),
945                call("lead", vec![r("s"), add1(r("i"))]),
946                r("i"),
947            ),
948        ),
949    };
950    // tend(s, i) — one past the last non-ws index (0 if all ws).
951    let tend = FunctionSpec {
952        name: "tend".into(),
953        type_params: vec![],
954        params: vec![p("s", Type::String), p("i", Type::Number)],
955        produces: ext(Type::Number),
956        requires: BTreeSet::new(),
957        on_failure: vec![],
958        steps: vec![],
959        result: iff(
960            bop(BinOp::Lt, r("i"), num(0)),
961            num(0),
962            iff(
963                call("is_ws", vec![sl(r("s"), r("i"), num(1))]),
964                call("tend", vec![r("s"), sub1(r("i"))]),
965                add1(r("i")),
966            ),
967        ),
968    };
969    let str_trim = FunctionSpec {
970        name: "str_trim".into(),
971        type_params: vec![],
972        params: vec![p("s", Type::String)],
973        produces: ext(Type::String),
974        requires: BTreeSet::new(),
975        on_failure: vec![],
976        steps: vec![
977            step("st", call("lead", vec![r("s"), num(0)])),
978            step("en", call("tend", vec![r("s"), sub1(slen_(r("s")))])),
979        ],
980        result: iff(
981            bop(BinOp::Ge, r("st"), r("en")),
982            ExprSpec::Str(String::new()),
983            sl(r("s"), r("st"), bop(BinOp::Sub, r("en"), r("st"))),
984        ),
985    };
986    // rep(s, from, to) — replace every non-overlapping `from`. Empty
987    // `from` returns `s` unchanged (no infinite loop).
988    let rep = FunctionSpec {
989        name: "rep".into(),
990        type_params: vec![],
991        params: vec![
992            p("s", Type::String),
993            p("from", Type::String),
994            p("to", Type::String),
995        ],
996        produces: ext(Type::String),
997        requires: BTreeSet::new(),
998        on_failure: vec![],
999        steps: vec![
1000            step("fl", slen_(r("from"))),
1001            step("ix", sidx(r("s"), r("from"))),
1002        ],
1003        result: iff(
1004            bop(BinOp::Eq, r("fl"), num(0)),
1005            r("s"),
1006            iff(
1007                bop(BinOp::Lt, r("ix"), num(0)),
1008                r("s"),
1009                scat(
1010                    scat(sl(r("s"), num(0), r("ix")), r("to")),
1011                    call(
1012                        "rep",
1013                        vec![
1014                            sl(
1015                                r("s"),
1016                                bop(BinOp::Add, r("ix"), r("fl")),
1017                                bop(
1018                                    BinOp::Sub,
1019                                    slen_(r("s")),
1020                                    bop(BinOp::Add, r("ix"), r("fl")),
1021                                ),
1022                            ),
1023                            r("from"),
1024                            r("to"),
1025                        ],
1026                    ),
1027                ),
1028            ),
1029        ),
1030    };
1031    let str_replace = FunctionSpec {
1032        name: "str_replace".into(),
1033        type_params: vec![],
1034        params: vec![
1035            p("s", Type::String),
1036            p("from", Type::String),
1037            p("to", Type::String),
1038        ],
1039        produces: ext(Type::String),
1040        requires: BTreeSet::new(),
1041        on_failure: vec![],
1042        steps: vec![],
1043        result: call("rep", vec![r("s"), r("from"), r("to")]),
1044    };
1045
1046    // Option ergonomics. Cairn variants/records are nominal &
1047    // monomorphic, so there is no generic `Result<T,E>` at stdlib
1048    // level (a decided boundary — design.md §9): the fallible-value
1049    // story is built-in `Option<T>` plus the language `Fail`/`Handle`.
1050    // first/last give the safe ends of a list.
1051    let first = FunctionSpec {
1052        name: "first".into(),
1053        type_params: tp(&["T"]),
1054        params: vec![p("xs", list(var("T")))],
1055        produces: ext(Type::Option(Box::new(var("T")))),
1056        requires: BTreeSet::new(),
1057        on_failure: vec![],
1058        steps: vec![step("n", llen(r("xs")))],
1059        result: iff(
1060            eq(r("n"), num(0)),
1061            none(var("T")),
1062            some(lget(r("xs"), num(0))),
1063        ),
1064    };
1065    let last = FunctionSpec {
1066        name: "last".into(),
1067        type_params: tp(&["T"]),
1068        params: vec![p("xs", list(var("T")))],
1069        produces: ext(Type::Option(Box::new(var("T")))),
1070        requires: BTreeSet::new(),
1071        on_failure: vec![],
1072        steps: vec![step("n", llen(r("xs")))],
1073        result: iff(
1074            eq(r("n"), num(0)),
1075            none(var("T")),
1076            some(lget(r("xs"), bop(BinOp::Sub, r("n"), num(1)))),
1077        ),
1078    };
1079
1080    // opt_map(f, o)  : Some(f(v)) | None
1081    // opt_then(f, o) : f(v)       | None   (monadic bind)
1082    let omatch = |o: ExprSpec, sb: ExprSpec, nb: ExprSpec| {
1083        ExprSpec::OptionMatch {
1084            opt: Box::new(o),
1085            some_bind: "v".into(),
1086            some_body: Box::new(sb),
1087            none_body: Box::new(nb),
1088        }
1089    };
1090    let opt_map = FunctionSpec {
1091        name: "opt_map".into(),
1092        type_params: tp(&["T", "U"]),
1093        params: vec![
1094            p("f", fnt(vec![var("T")], var("U"))),
1095            p("o", Type::Option(Box::new(var("T")))),
1096        ],
1097        produces: ext(Type::Option(Box::new(var("U")))),
1098        requires: BTreeSet::new(),
1099        on_failure: vec![],
1100        steps: vec![],
1101        result: omatch(
1102            r("o"),
1103            some(callv(r("f"), vec![r("v")])),
1104            none(var("U")),
1105        ),
1106    };
1107    let opt_then = FunctionSpec {
1108        name: "opt_then".into(),
1109        type_params: tp(&["T", "U"]),
1110        params: vec![
1111            p("f", fnt(vec![var("T")], Type::Option(Box::new(var("U"))))),
1112            p("o", Type::Option(Box::new(var("T")))),
1113        ],
1114        produces: ext(Type::Option(Box::new(var("U")))),
1115        requires: BTreeSet::new(),
1116        on_failure: vec![],
1117        steps: vec![],
1118        result: omatch(
1119            r("o"),
1120            callv(r("f"), vec![r("v")]),
1121            none(var("U")),
1122        ),
1123    };
1124
1125    // Integer helpers (pure Number; no language-core change — Float
1126    // transcendental math would be a Node+wasm-intrinsic pipeline-add,
1127    // out of scope here by design).
1128    let nf0 = |name: &str, params: Vec<Param>, result: ExprSpec| FunctionSpec {
1129        name: name.into(),
1130        type_params: vec![],
1131        params,
1132        produces: ext(Type::Number),
1133        requires: BTreeSet::new(),
1134        on_failure: vec![],
1135        steps: vec![],
1136        result,
1137    };
1138    let iabs = nf0(
1139        "iabs",
1140        vec![p("x", Type::Number)],
1141        iff(
1142            bop(BinOp::Lt, r("x"), num(0)),
1143            bop(BinOp::Sub, num(0), r("x")),
1144            r("x"),
1145        ),
1146    );
1147    let imin = nf0(
1148        "imin",
1149        vec![p("a", Type::Number), p("b", Type::Number)],
1150        iff(bop(BinOp::Lt, r("a"), r("b")), r("a"), r("b")),
1151    );
1152    let imax = nf0(
1153        "imax",
1154        vec![p("a", Type::Number), p("b", Type::Number)],
1155        iff(bop(BinOp::Gt, r("a"), r("b")), r("a"), r("b")),
1156    );
1157    let clamp = nf0(
1158        "clamp",
1159        vec![
1160            p("x", Type::Number),
1161            p("lo", Type::Number),
1162            p("hi", Type::Number),
1163        ],
1164        call(
1165            "imax",
1166            vec![r("lo"), call("imin", vec![r("x"), r("hi")])],
1167        ),
1168    );
1169
1170    // ipow(base, exp) — exp <= 0 yields 1 (documented). gcd via Euclid
1171    // on the signed remainder; the iabs at b==0 normalises the sign.
1172    let ipow = nf0(
1173        "ipow",
1174        vec![p("base", Type::Number), p("exp", Type::Number)],
1175        iff(
1176            bop(BinOp::Le, r("exp"), num(0)),
1177            num(1),
1178            bop(
1179                BinOp::Mul,
1180                r("base"),
1181                call(
1182                    "ipow",
1183                    vec![r("base"), bop(BinOp::Sub, r("exp"), num(1))],
1184                ),
1185            ),
1186        ),
1187    );
1188    let gcd = nf0(
1189        "gcd",
1190        vec![p("a", Type::Number), p("b", Type::Number)],
1191        iff(
1192            bop(BinOp::Eq, r("b"), num(0)),
1193            call("iabs", vec![r("a")]),
1194            call(
1195                "gcd",
1196                vec![r("b"), bop(BinOp::Mod, r("a"), r("b"))],
1197            ),
1198        ),
1199    );
1200
1201    // decimal_round_str(d, places) — round a Decimal half-up (away
1202    // from zero) to `places` (clamped 0..4) fractional digits and
1203    // format it. Returns a String, not a Decimal: reconstructing a
1204    // Decimal from a rounded raw would need a raw->Decimal core
1205    // primitive (Decimal is a scaled i64; `DecimalRaw` is the one-way
1206    // identity) — a language-core pipeline-add, out of scope for the
1207    // stdlib track by design. Display is the real need; this covers it,
1208    // composed from clamp/ipow/iabs/lpad0 (the stdlib using itself).
1209    let dr = FunctionSpec {
1210        name: "decimal_round_str".into(),
1211        type_params: vec![],
1212        params: vec![p("d", Type::Decimal), p("places", Type::Number)],
1213        produces: ext(Type::String),
1214        requires: BTreeSet::new(),
1215        on_failure: vec![],
1216        steps: vec![
1217            step("raw", ExprSpec::DecimalRaw(Box::new(r("d")))),
1218            step("m", call("iabs", vec![r("raw")])),
1219            step("pl", call("clamp", vec![r("places"), num(0), num(4)])),
1220            step(
1221                "f",
1222                call(
1223                    "ipow",
1224                    vec![num(10), bop(BinOp::Sub, num(4), r("pl"))],
1225                ),
1226            ),
1227            // round half-up: ((m + f/2) / f) * f, still scaled by 10000.
1228            step(
1229                "rnd",
1230                bop(
1231                    BinOp::Mul,
1232                    bop(
1233                        BinOp::Div,
1234                        bop(
1235                            BinOp::Add,
1236                            r("m"),
1237                            bop(BinOp::Div, r("f"), num(2)),
1238                        ),
1239                        r("f"),
1240                    ),
1241                    r("f"),
1242                ),
1243            ),
1244            step("whole", bop(BinOp::Div, r("rnd"), num(10_000))),
1245            step(
1246                "frac",
1247                bop(
1248                    BinOp::Div,
1249                    bop(BinOp::Mod, r("rnd"), num(10_000)),
1250                    r("f"),
1251                ),
1252            ),
1253            step(
1254                "sign",
1255                iff(
1256                    bop(BinOp::Lt, r("raw"), num(0)),
1257                    ExprSpec::Str("-".into()),
1258                    ExprSpec::Str(String::new()),
1259                ),
1260            ),
1261        ],
1262        result: scat(
1263            scat(r("sign"), n2s(r("whole"))),
1264            iff(
1265                bop(BinOp::Eq, r("pl"), num(0)),
1266                ExprSpec::Str(String::new()),
1267                scat(
1268                    ExprSpec::Str(".".into()),
1269                    call("lpad0", vec![n2s(r("frac")), r("pl")]),
1270                ),
1271            ),
1272        ),
1273    };
1274
1275    vec![
1276        fold_at, fold, map_at, map, filter_at, filter, range_at, range,
1277        any_at, any, all_at, all, find_at, find, concat_at, concat,
1278        lpad0, decimal_to_str, take_at, take, drop_at, drop, rev_at,
1279        reverse, flat_map_at, flat_map, ins, sort_at, sort_by, zip_at,
1280        zip_with, dd_at, dedup_by, join_at, str_join, rep_at, str_repeat,
1281        is_ws, lead, tend, str_trim, rep, str_replace, first, last,
1282        opt_map, opt_then, iabs, imin, imax, clamp, ipow, gcd, dr,
1283    ]
1284}
1285
1286#[cfg(test)]
1287mod tests {
1288    use super::*;
1289    use crate::edit::{Editor, ModuleSpec};
1290    use crate::store::Store;
1291    use crate::wasm::{lower, run_i64};
1292
1293    /// Audit B8: `decimal_to_str` renders exact fixed-point money
1294    /// (sign + whole + 4 frac digits), authored in Cairn over the
1295    /// full-range `num_to_str`. Probes compare via StrEq → Bool.
1296    #[test]
1297    fn decimal_to_str_formats() {
1298        let probe = |name: &str, v: f64, want: &str| FunctionSpec {
1299            name: name.into(),
1300            type_params: vec![],
1301            params: vec![],
1302            produces: ext(Type::Bool),
1303            requires: BTreeSet::new(),
1304            on_failure: vec![],
1305            steps: vec![],
1306            result: ExprSpec::StrEq(
1307                Box::new(call(
1308                    "decimal_to_str",
1309                    vec![ExprSpec::Decimal(v)],
1310                )),
1311                Box::new(ExprSpec::Str(want.into())),
1312            ),
1313        };
1314        let cases = [
1315            ("a", 1.25, "1.2500"),
1316            ("b", 19.99, "19.9900"),
1317            ("c", -0.5, "-0.5000"),
1318            ("d", 0.0, "0.0000"),
1319            ("e", 1000.0, "1000.0000"),
1320            ("f", -12.3456, "-12.3456"),
1321        ];
1322        let mut fns = list_functions();
1323        fns.extend(cases.iter().map(|(n, v, w)| probe(n, *v, w)));
1324        let e = Editor::new(Store::open_in_memory().unwrap());
1325        let (m, report) = e
1326            .apply_module(&ModuleSpec {
1327                name: "dec".into(),
1328                types: vec![],
1329                functions: fns,
1330            })
1331            .unwrap();
1332        assert!(report.ok(), "violations: {:?}", report.violations);
1333        let wasm = lower(e.store(), &m).unwrap();
1334        for (n, _, w) in cases {
1335            assert_eq!(
1336                run_i64(&wasm, n, &[]).unwrap(),
1337                1,
1338                "decimal_to_str should produce {w}"
1339            );
1340        }
1341    }
1342
1343    /// Author the stdlib + a caller that uses `map`/`filter`/`fold`
1344    /// through closures, then check, lower, and run it end to end.
1345    #[test]
1346    fn the_stdlib_maps_filters_and_folds() {
1347        // sum_doubled_evens(xs) =
1348        //   fold(|a,x| a + x, 0, map(|x| x*2, filter(|x| x % 2 == 0, xs)))
1349        let lam = |params: Vec<(&str, Type)>, body: ExprSpec| ExprSpec::Lambda {
1350            params: params
1351                .into_iter()
1352                .map(|(n, t)| (n.to_string(), t))
1353                .collect(),
1354            body: Box::new(body),
1355        };
1356        let bin = |op, a: ExprSpec, b: ExprSpec| ExprSpec::BinOp {
1357            op,
1358            lhs: Box::new(a),
1359            rhs: Box::new(b),
1360        };
1361
1362        let is_even = lam(
1363            vec![("x", Type::Number)],
1364            bin(BinOp::Eq, bin(BinOp::Mod, r("x"), num(2)), num(0)),
1365        );
1366        let double = lam(vec![("x", Type::Number)], bin(BinOp::Mul, r("x"), num(2)));
1367        let plus = lam(
1368            vec![("a", Type::Number), ("x", Type::Number)],
1369            bin(BinOp::Add, r("a"), r("x")),
1370        );
1371
1372        // run() = fold(|a,x| a+x, 0, map(|x| x*2, filter(even, [1..6])))
1373        //   evens [2,4,6] -> doubled [4,8,12] -> sum 24.
1374        let xs = ExprSpec::List((1..=6).map(num).collect());
1375        let caller = FunctionSpec {
1376            name: "run".into(),
1377            type_params: vec![],
1378            params: vec![],
1379            produces: ext(Type::Number),
1380            requires: BTreeSet::new(),
1381            on_failure: vec![],
1382            steps: vec![],
1383            result: call(
1384                "fold",
1385                vec![
1386                    plus,
1387                    num(0),
1388                    call("map", vec![double, call("filter", vec![is_even, xs])]),
1389                ],
1390            ),
1391        };
1392
1393        let e = Editor::new(Store::open_in_memory().unwrap());
1394        let mut fns = list_functions();
1395        fns.push(caller);
1396        let (m, report) = e
1397            .apply_module(&ModuleSpec {
1398                name: "stdtest".into(),
1399                types: vec![],
1400                functions: fns,
1401            })
1402            .unwrap();
1403        assert!(report.ok(), "violations: {:?}", report.violations);
1404
1405        let wasm = lower(e.store(), &m).unwrap();
1406        assert_eq!(run_i64(&wasm, "run", &[]).unwrap(), 24);
1407    }
1408
1409    /// range / any / all / find — find returns Option<T>, consumed with
1410    /// OptionMatch (the whole keystone chain in one path).
1411    #[test]
1412    fn the_stdlib_ranges_searches_and_quantifies() {
1413        let lam = |params: Vec<(&str, Type)>, body: ExprSpec| ExprSpec::Lambda {
1414            params: params
1415                .into_iter()
1416                .map(|(n, t)| (n.to_string(), t))
1417                .collect(),
1418            body: Box::new(body),
1419        };
1420        let bin = |op, a: ExprSpec, b: ExprSpec| ExprSpec::BinOp {
1421            op,
1422            lhs: Box::new(a),
1423            rhs: Box::new(b),
1424        };
1425        let nums = |vs: &[i64]| ExprSpec::List(vs.iter().copied().map(num).collect());
1426        let nfn = |name: &str, ret: Type, result: ExprSpec| FunctionSpec {
1427            name: name.into(),
1428            type_params: vec![],
1429            params: vec![],
1430            produces: ext(ret),
1431            requires: BTreeSet::new(),
1432            on_failure: vec![],
1433            steps: vec![],
1434            result,
1435        };
1436        let nullary = |name: &str, result: ExprSpec| nfn(name, Type::Number, result);
1437        let plus = || {
1438            lam(
1439                vec![("a", Type::Number), ("x", Type::Number)],
1440                bin(BinOp::Add, r("a"), r("x")),
1441            )
1442        };
1443        let find_or = |pred: ExprSpec, xs: ExprSpec| ExprSpec::OptionMatch {
1444            opt: Box::new(call("find", vec![pred, xs])),
1445            some_bind: "v".into(),
1446            some_body: Box::new(r("v")),
1447            none_body: Box::new(num(-1)),
1448        };
1449
1450        let mut fns = list_functions();
1451        fns.extend([
1452            // sum of range(1,5) = 1+2+3+4 = 10
1453            nullary(
1454                "r_sum",
1455                call("fold", vec![plus(), num(0), call("range", vec![num(1), num(5)])]),
1456            ),
1457            // any x == 3 in [1,2,3] -> true(1)
1458            nfn(
1459                "anyt",
1460                Type::Bool,
1461                call(
1462                    "any",
1463                    vec![
1464                        lam(vec![("x", Type::Number)], bin(BinOp::Eq, r("x"), num(3))),
1465                        nums(&[1, 2, 3]),
1466                    ],
1467                ),
1468            ),
1469            // all x > 0 -> true ; all x > 1 -> false
1470            nfn(
1471                "allt",
1472                Type::Bool,
1473                call(
1474                    "all",
1475                    vec![
1476                        lam(vec![("x", Type::Number)], bin(BinOp::Gt, r("x"), num(0))),
1477                        nums(&[1, 2, 3]),
1478                    ],
1479                ),
1480            ),
1481            nfn(
1482                "allf",
1483                Type::Bool,
1484                call(
1485                    "all",
1486                    vec![
1487                        lam(vec![("x", Type::Number)], bin(BinOp::Gt, r("x"), num(1))),
1488                        nums(&[1, 2, 3]),
1489                    ],
1490                ),
1491            ),
1492            // find x > 2 in [1,2,3,4] -> Some(3) ; find x > 9 -> None
1493            nullary(
1494                "find_hit",
1495                find_or(
1496                    lam(vec![("x", Type::Number)], bin(BinOp::Gt, r("x"), num(2))),
1497                    nums(&[1, 2, 3, 4]),
1498                ),
1499            ),
1500            nullary(
1501                "find_miss",
1502                find_or(
1503                    lam(vec![("x", Type::Number)], bin(BinOp::Gt, r("x"), num(9))),
1504                    nums(&[1, 2, 3, 4]),
1505                ),
1506            ),
1507        ]);
1508
1509        let e = Editor::new(Store::open_in_memory().unwrap());
1510        let (m, report) = e
1511            .apply_module(&ModuleSpec {
1512                name: "stdtest2".into(),
1513                types: vec![],
1514                functions: fns,
1515            })
1516            .unwrap();
1517        assert!(report.ok(), "violations: {:?}", report.violations);
1518        let wasm = lower(e.store(), &m).unwrap();
1519        assert_eq!(run_i64(&wasm, "r_sum", &[]).unwrap(), 10);
1520        assert_eq!(run_i64(&wasm, "anyt", &[]).unwrap(), 1);
1521        assert_eq!(run_i64(&wasm, "allt", &[]).unwrap(), 1);
1522        assert_eq!(run_i64(&wasm, "allf", &[]).unwrap(), 0);
1523        assert_eq!(run_i64(&wasm, "find_hit", &[]).unwrap(), 3);
1524        assert_eq!(run_i64(&wasm, "find_miss", &[]).unwrap(), -1);
1525    }
1526
1527    /// take / drop: first-k and all-but-first-k, both clamped (k=0,
1528    /// k>len never trap — the guards check `len` before any index).
1529    #[test]
1530    fn the_stdlib_takes_and_drops() {
1531        let lam = |params: Vec<(&str, Type)>, body: ExprSpec| ExprSpec::Lambda {
1532            params: params
1533                .into_iter()
1534                .map(|(n, t)| (n.to_string(), t))
1535                .collect(),
1536            body: Box::new(body),
1537        };
1538        let bin = |op, a: ExprSpec, b: ExprSpec| ExprSpec::BinOp {
1539            op,
1540            lhs: Box::new(a),
1541            rhs: Box::new(b),
1542        };
1543        let xs = || ExprSpec::List([10, 20, 30, 40, 50].iter().copied().map(num).collect());
1544        let plus = || {
1545            lam(
1546                vec![("a", Type::Number), ("x", Type::Number)],
1547                bin(BinOp::Add, r("a"), r("x")),
1548            )
1549        };
1550        let sum = |l: ExprSpec| call("fold", vec![plus(), num(0), l]);
1551        let nf = |name: &str, result: ExprSpec| FunctionSpec {
1552            name: name.into(),
1553            type_params: vec![],
1554            params: vec![],
1555            produces: ext(Type::Number),
1556            requires: BTreeSet::new(),
1557            on_failure: vec![],
1558            steps: vec![],
1559            result,
1560        };
1561        let mut fns = list_functions();
1562        fns.extend([
1563            nf("t_take2", sum(call("take", vec![xs(), num(2)]))), // 10+20
1564            nf("t_drop2", sum(call("drop", vec![xs(), num(2)]))), // 30+40+50
1565            nf("t_take_over", llen(call("take", vec![xs(), num(99)]))), // 5
1566            nf("t_take_zero", llen(call("take", vec![xs(), num(0)]))), // 0
1567            nf("t_drop_zero", llen(call("drop", vec![xs(), num(0)]))), // 5
1568            nf("t_drop_over", llen(call("drop", vec![xs(), num(99)]))), // 0
1569        ]);
1570        let e = Editor::new(Store::open_in_memory().unwrap());
1571        let (m, report) = e
1572            .apply_module(&ModuleSpec {
1573                name: "stdtake".into(),
1574                types: vec![],
1575                functions: fns,
1576            })
1577            .unwrap();
1578        assert!(report.ok(), "violations: {:?}", report.violations);
1579        let wasm = lower(e.store(), &m).unwrap();
1580        assert_eq!(run_i64(&wasm, "t_take2", &[]).unwrap(), 30);
1581        assert_eq!(run_i64(&wasm, "t_drop2", &[]).unwrap(), 120);
1582        assert_eq!(run_i64(&wasm, "t_take_over", &[]).unwrap(), 5);
1583        assert_eq!(run_i64(&wasm, "t_take_zero", &[]).unwrap(), 0);
1584        assert_eq!(run_i64(&wasm, "t_drop_zero", &[]).unwrap(), 5);
1585        assert_eq!(run_i64(&wasm, "t_drop_over", &[]).unwrap(), 0);
1586    }
1587
1588    /// reverse (empty-safe via the i<0 guard) and flat_map (concat of a
1589    /// list-returning closure over the input).
1590    #[test]
1591    fn the_stdlib_reverses_and_flat_maps() {
1592        let lam = |params: Vec<(&str, Type)>, body: ExprSpec| ExprSpec::Lambda {
1593            params: params
1594                .into_iter()
1595                .map(|(n, t)| (n.to_string(), t))
1596                .collect(),
1597            body: Box::new(body),
1598        };
1599        let bin = |op, a: ExprSpec, b: ExprSpec| ExprSpec::BinOp {
1600            op,
1601            lhs: Box::new(a),
1602            rhs: Box::new(b),
1603        };
1604        let nums = |vs: &[i64]| ExprSpec::List(vs.iter().copied().map(num).collect());
1605        let nf = |name: &str, result: ExprSpec| FunctionSpec {
1606            name: name.into(),
1607            type_params: vec![],
1608            params: vec![],
1609            produces: ext(Type::Number),
1610            requires: BTreeSet::new(),
1611            on_failure: vec![],
1612            steps: vec![],
1613            result,
1614        };
1615        // fold(|a,x| a*10+x, 0, ·) reads a digit list as a number, so
1616        // reverse([1,2,3]) -> 321 is an exact, order-sensitive probe.
1617        let horner = || {
1618            lam(
1619                vec![("a", Type::Number), ("x", Type::Number)],
1620                bin(BinOp::Add, bin(BinOp::Mul, r("a"), num(10)), r("x")),
1621            )
1622        };
1623        let dig = |l: ExprSpec| call("fold", vec![horner(), num(0), l]);
1624        let dup = || {
1625            lam(
1626                vec![("x", Type::Number)],
1627                ExprSpec::List(vec![r("x"), r("x")]),
1628            )
1629        };
1630        let mut fns = list_functions();
1631        fns.extend([
1632            nf("rev3", dig(call("reverse", vec![nums(&[1, 2, 3])]))), // 321
1633            nf(
1634                "rev_empty",
1635                llen(call("reverse", vec![empty(Type::Number)])),
1636            ), // 0
1637            nf(
1638                "fm_dup",
1639                dig(call("flat_map", vec![dup(), nums(&[1, 2, 3])])),
1640            ), // [1,1,2,2,3,3] -> 112233
1641            nf(
1642                "fm_len",
1643                llen(call("flat_map", vec![dup(), nums(&[4, 5])])),
1644            ), // 4
1645        ]);
1646        let e = Editor::new(Store::open_in_memory().unwrap());
1647        let (m, report) = e
1648            .apply_module(&ModuleSpec {
1649                name: "stdrev".into(),
1650                types: vec![],
1651                functions: fns,
1652            })
1653            .unwrap();
1654        assert!(report.ok(), "violations: {:?}", report.violations);
1655        let wasm = lower(e.store(), &m).unwrap();
1656        assert_eq!(run_i64(&wasm, "rev3", &[]).unwrap(), 321);
1657        assert_eq!(run_i64(&wasm, "rev_empty", &[]).unwrap(), 0);
1658        assert_eq!(run_i64(&wasm, "fm_dup", &[]).unwrap(), 112233);
1659        assert_eq!(run_i64(&wasm, "fm_len", &[]).unwrap(), 4);
1660    }
1661
1662    /// sort_by with a comparator closure: ascending (`<=`), descending
1663    /// (`>=`), already-sorted, and empty. `le` is threaded explicitly,
1664    /// so no closure ever captures a function value (§9 boundary).
1665    #[test]
1666    fn the_stdlib_sorts_by_a_comparator() {
1667        let lam = |params: Vec<(&str, Type)>, body: ExprSpec| ExprSpec::Lambda {
1668            params: params
1669                .into_iter()
1670                .map(|(n, t)| (n.to_string(), t))
1671                .collect(),
1672            body: Box::new(body),
1673        };
1674        let bin = |op, a: ExprSpec, b: ExprSpec| ExprSpec::BinOp {
1675            op,
1676            lhs: Box::new(a),
1677            rhs: Box::new(b),
1678        };
1679        let nums = |vs: &[i64]| ExprSpec::List(vs.iter().copied().map(num).collect());
1680        let nf = |name: &str, result: ExprSpec| FunctionSpec {
1681            name: name.into(),
1682            type_params: vec![],
1683            params: vec![],
1684            produces: ext(Type::Number),
1685            requires: BTreeSet::new(),
1686            on_failure: vec![],
1687            steps: vec![],
1688            result,
1689        };
1690        let le = || {
1691            lam(
1692                vec![("a", Type::Number), ("b", Type::Number)],
1693                bin(BinOp::Le, r("a"), r("b")),
1694            )
1695        };
1696        let ge = || {
1697            lam(
1698                vec![("a", Type::Number), ("b", Type::Number)],
1699                bin(BinOp::Ge, r("a"), r("b")),
1700            )
1701        };
1702        let horner = || {
1703            lam(
1704                vec![("a", Type::Number), ("x", Type::Number)],
1705                bin(BinOp::Add, bin(BinOp::Mul, r("a"), num(10)), r("x")),
1706            )
1707        };
1708        let dig = |l: ExprSpec| call("fold", vec![horner(), num(0), l]);
1709        let mut fns = list_functions();
1710        fns.extend([
1711            nf("asc", dig(call("sort_by", vec![le(), nums(&[3, 1, 2])]))), // 123
1712            nf("desc", dig(call("sort_by", vec![ge(), nums(&[3, 1, 2])]))), // 321
1713            nf(
1714                "already",
1715                dig(call("sort_by", vec![le(), nums(&[1, 2, 3])])),
1716            ), // 123
1717            nf(
1718                "sort_empty",
1719                llen(call("sort_by", vec![le(), empty(Type::Number)])),
1720            ), // 0
1721            nf(
1722                "asc5",
1723                dig(call("sort_by", vec![le(), nums(&[5, 4, 3, 2, 1])])),
1724            ), // 12345
1725        ]);
1726        let e = Editor::new(Store::open_in_memory().unwrap());
1727        let (m, report) = e
1728            .apply_module(&ModuleSpec {
1729                name: "stdsort".into(),
1730                types: vec![],
1731                functions: fns,
1732            })
1733            .unwrap();
1734        assert!(report.ok(), "violations: {:?}", report.violations);
1735        let wasm = lower(e.store(), &m).unwrap();
1736        assert_eq!(run_i64(&wasm, "asc", &[]).unwrap(), 123);
1737        assert_eq!(run_i64(&wasm, "desc", &[]).unwrap(), 321);
1738        assert_eq!(run_i64(&wasm, "already", &[]).unwrap(), 123);
1739        assert_eq!(run_i64(&wasm, "sort_empty", &[]).unwrap(), 0);
1740        assert_eq!(run_i64(&wasm, "asc5", &[]).unwrap(), 12345);
1741    }
1742
1743    /// zip_with (stops at the shorter input) and dedup_by (adjacent
1744    /// `uniq`), both threading the closure explicitly.
1745    #[test]
1746    fn the_stdlib_zips_and_dedups() {
1747        let lam = |params: Vec<(&str, Type)>, body: ExprSpec| ExprSpec::Lambda {
1748            params: params
1749                .into_iter()
1750                .map(|(n, t)| (n.to_string(), t))
1751                .collect(),
1752            body: Box::new(body),
1753        };
1754        let bin = |op, a: ExprSpec, b: ExprSpec| ExprSpec::BinOp {
1755            op,
1756            lhs: Box::new(a),
1757            rhs: Box::new(b),
1758        };
1759        let nums = |vs: &[i64]| ExprSpec::List(vs.iter().copied().map(num).collect());
1760        let nf = |name: &str, result: ExprSpec| FunctionSpec {
1761            name: name.into(),
1762            type_params: vec![],
1763            params: vec![],
1764            produces: ext(Type::Number),
1765            requires: BTreeSet::new(),
1766            on_failure: vec![],
1767            steps: vec![],
1768            result,
1769        };
1770        let plus = || {
1771            lam(
1772                vec![("a", Type::Number), ("x", Type::Number)],
1773                bin(BinOp::Add, r("a"), r("x")),
1774            )
1775        };
1776        let sum = |l: ExprSpec| call("fold", vec![plus(), num(0), l]);
1777        let addf = || {
1778            lam(
1779                vec![("a", Type::Number), ("b", Type::Number)],
1780                bin(BinOp::Add, r("a"), r("b")),
1781            )
1782        };
1783        let eqf = || {
1784            lam(
1785                vec![("a", Type::Number), ("b", Type::Number)],
1786                bin(BinOp::Eq, r("a"), r("b")),
1787            )
1788        };
1789        let horner = || {
1790            lam(
1791                vec![("a", Type::Number), ("x", Type::Number)],
1792                bin(BinOp::Add, bin(BinOp::Mul, r("a"), num(10)), r("x")),
1793            )
1794        };
1795        let dig = |l: ExprSpec| call("fold", vec![horner(), num(0), l]);
1796        let mut fns = list_functions();
1797        fns.extend([
1798            // [1,2,3]+[10,20,30] -> [11,22,33], sum 66
1799            nf(
1800                "zip_sum",
1801                sum(call(
1802                    "zip_with",
1803                    vec![addf(), nums(&[1, 2, 3]), nums(&[10, 20, 30])],
1804                )),
1805            ),
1806            // uneven: [1,2]+[10,20,30] -> [11,22], len 2
1807            nf(
1808                "zip_uneven_len",
1809                llen(call(
1810                    "zip_with",
1811                    vec![addf(), nums(&[1, 2]), nums(&[10, 20, 30])],
1812                )),
1813            ),
1814            // adjacent dedup: [1,1,2,2,2,3,1] -> [1,2,3,1] -> 1231
1815            nf(
1816                "dd",
1817                dig(call(
1818                    "dedup_by",
1819                    vec![eqf(), nums(&[1, 1, 2, 2, 2, 3, 1])],
1820                )),
1821            ),
1822            nf(
1823                "dd_empty",
1824                llen(call("dedup_by", vec![eqf(), empty(Type::Number)])),
1825            ),
1826        ]);
1827        let e = Editor::new(Store::open_in_memory().unwrap());
1828        let (m, report) = e
1829            .apply_module(&ModuleSpec {
1830                name: "stdzip".into(),
1831                types: vec![],
1832                functions: fns,
1833            })
1834            .unwrap();
1835        assert!(report.ok(), "violations: {:?}", report.violations);
1836        let wasm = lower(e.store(), &m).unwrap();
1837        assert_eq!(run_i64(&wasm, "zip_sum", &[]).unwrap(), 66);
1838        assert_eq!(run_i64(&wasm, "zip_uneven_len", &[]).unwrap(), 2);
1839        assert_eq!(run_i64(&wasm, "dd", &[]).unwrap(), 1231);
1840        assert_eq!(run_i64(&wasm, "dd_empty", &[]).unwrap(), 0);
1841    }
1842
1843    /// str_join (separator only between elements; empty/single safe) and
1844    /// str_repeat (n<=0 → ""). Probes compare via StrEq → Bool.
1845    #[test]
1846    fn the_stdlib_joins_and_repeats() {
1847        let strs = |vs: &[&str]| {
1848            ExprSpec::List(vs.iter().map(|s| ExprSpec::Str((*s).into())).collect())
1849        };
1850        let probe = |name: &str, got: ExprSpec, want: &str| FunctionSpec {
1851            name: name.into(),
1852            type_params: vec![],
1853            params: vec![],
1854            produces: ext(Type::Bool),
1855            requires: BTreeSet::new(),
1856            on_failure: vec![],
1857            steps: vec![],
1858            result: ExprSpec::StrEq(
1859                Box::new(got),
1860                Box::new(ExprSpec::Str(want.into())),
1861            ),
1862        };
1863        let mut fns = list_functions();
1864        fns.extend([
1865            probe(
1866                "j_abc",
1867                call(
1868                    "str_join",
1869                    vec![ExprSpec::Str(", ".into()), strs(&["a", "b", "c"])],
1870                ),
1871                "a, b, c",
1872            ),
1873            probe(
1874                "j_empty",
1875                call(
1876                    "str_join",
1877                    vec![
1878                        ExprSpec::Str("-".into()),
1879                        ExprSpec::ListEmpty { elem: Type::String },
1880                    ],
1881                ),
1882                "",
1883            ),
1884            probe(
1885                "j_one",
1886                call(
1887                    "str_join",
1888                    vec![ExprSpec::Str("-".into()), strs(&["x"])],
1889                ),
1890                "x",
1891            ),
1892            probe(
1893                "r_ab3",
1894                call(
1895                    "str_repeat",
1896                    vec![ExprSpec::Str("ab".into()), num(3)],
1897                ),
1898                "ababab",
1899            ),
1900            probe(
1901                "r_zero",
1902                call(
1903                    "str_repeat",
1904                    vec![ExprSpec::Str("z".into()), num(0)],
1905                ),
1906                "",
1907            ),
1908        ]);
1909        let e = Editor::new(Store::open_in_memory().unwrap());
1910        let (m, report) = e
1911            .apply_module(&ModuleSpec {
1912                name: "stdjoin".into(),
1913                types: vec![],
1914                functions: fns,
1915            })
1916            .unwrap();
1917        assert!(report.ok(), "violations: {:?}", report.violations);
1918        let wasm = lower(e.store(), &m).unwrap();
1919        for n in ["j_abc", "j_empty", "j_one", "r_ab3", "r_zero"] {
1920            assert_eq!(run_i64(&wasm, n, &[]).unwrap(), 1, "{n}");
1921        }
1922    }
1923
1924    /// str_trim (leading/trailing ASCII ws, all-ws → "", empty-safe) and
1925    /// str_replace (all non-overlapping; empty `from` is a no-op).
1926    #[test]
1927    fn the_stdlib_trims_and_replaces() {
1928        let probe = |name: &str, got: ExprSpec, want: &str| FunctionSpec {
1929            name: name.into(),
1930            type_params: vec![],
1931            params: vec![],
1932            produces: ext(Type::Bool),
1933            requires: BTreeSet::new(),
1934            on_failure: vec![],
1935            steps: vec![],
1936            result: ExprSpec::StrEq(
1937                Box::new(got),
1938                Box::new(ExprSpec::Str(want.into())),
1939            ),
1940        };
1941        let s = |x: &str| ExprSpec::Str(x.into());
1942        let tr = |x: &str| call("str_trim", vec![s(x)]);
1943        let rp = |a: &str, b: &str, c: &str| {
1944            call("str_replace", vec![s(a), s(b), s(c)])
1945        };
1946        let mut fns = list_functions();
1947        fns.extend([
1948            probe("t_pad", tr("  hi  "), "hi"),
1949            probe("t_id", tr("x"), "x"),
1950            probe("t_allws", tr("   "), ""),
1951            probe("t_mixed", tr("\t a \n"), "a"),
1952            probe("t_empty", tr(""), ""),
1953            probe("rp_dots", rp("a.b.c", ".", "-"), "a-b-c"),
1954            probe("rp_grow", rp("aaa", "a", "bb"), "bbbbbb"),
1955            probe("rp_none", rp("none", "x", "y"), "none"),
1956            probe("rp_empty_from", rp("abc", "", "-"), "abc"),
1957        ]);
1958        let e = Editor::new(Store::open_in_memory().unwrap());
1959        let (m, report) = e
1960            .apply_module(&ModuleSpec {
1961                name: "stdtrim".into(),
1962                types: vec![],
1963                functions: fns,
1964            })
1965            .unwrap();
1966        assert!(report.ok(), "violations: {:?}", report.violations);
1967        let wasm = lower(e.store(), &m).unwrap();
1968        for n in [
1969            "t_pad",
1970            "t_id",
1971            "t_allws",
1972            "t_mixed",
1973            "t_empty",
1974            "rp_dots",
1975            "rp_grow",
1976            "rp_none",
1977            "rp_empty_from",
1978        ] {
1979            assert_eq!(run_i64(&wasm, n, &[]).unwrap(), 1, "{n}");
1980        }
1981    }
1982
1983    /// first / last return Option<T> (None on empty), consumed with
1984    /// OptionMatch — the safe list ends without a trapping ListGet.
1985    #[test]
1986    fn the_stdlib_first_and_last() {
1987        let nums = |vs: &[i64]| ExprSpec::List(vs.iter().copied().map(num).collect());
1988        let opt_or = |call_e: ExprSpec| ExprSpec::OptionMatch {
1989            opt: Box::new(call_e),
1990            some_bind: "v".into(),
1991            some_body: Box::new(r("v")),
1992            none_body: Box::new(num(-1)),
1993        };
1994        let nf = |name: &str, result: ExprSpec| FunctionSpec {
1995            name: name.into(),
1996            type_params: vec![],
1997            params: vec![],
1998            produces: ext(Type::Number),
1999            requires: BTreeSet::new(),
2000            on_failure: vec![],
2001            steps: vec![],
2002            result,
2003        };
2004        let mut fns = list_functions();
2005        fns.extend([
2006            nf("f_hit", opt_or(call("first", vec![nums(&[7, 8, 9])]))), // 7
2007            nf("f_miss", opt_or(call("first", vec![empty(Type::Number)]))), // -1
2008            nf("l_hit", opt_or(call("last", vec![nums(&[7, 8, 9])]))), // 9
2009            nf("l_one", opt_or(call("last", vec![nums(&[5])]))),       // 5
2010            nf("l_miss", opt_or(call("last", vec![empty(Type::Number)]))), // -1
2011        ]);
2012        let e = Editor::new(Store::open_in_memory().unwrap());
2013        let (m, report) = e
2014            .apply_module(&ModuleSpec {
2015                name: "stdopt".into(),
2016                types: vec![],
2017                functions: fns,
2018            })
2019            .unwrap();
2020        assert!(report.ok(), "violations: {:?}", report.violations);
2021        let wasm = lower(e.store(), &m).unwrap();
2022        assert_eq!(run_i64(&wasm, "f_hit", &[]).unwrap(), 7);
2023        assert_eq!(run_i64(&wasm, "f_miss", &[]).unwrap(), -1);
2024        assert_eq!(run_i64(&wasm, "l_hit", &[]).unwrap(), 9);
2025        assert_eq!(run_i64(&wasm, "l_one", &[]).unwrap(), 5);
2026        assert_eq!(run_i64(&wasm, "l_miss", &[]).unwrap(), -1);
2027    }
2028
2029    /// opt_map (Some(f v)|None) and opt_then (monadic bind: f v|None).
2030    #[test]
2031    fn the_stdlib_opt_map_and_then() {
2032        let lam = |params: Vec<(&str, Type)>, body: ExprSpec| ExprSpec::Lambda {
2033            params: params
2034                .into_iter()
2035                .map(|(n, t)| (n.to_string(), t))
2036                .collect(),
2037            body: Box::new(body),
2038        };
2039        let bin = |op, a: ExprSpec, b: ExprSpec| ExprSpec::BinOp {
2040            op,
2041            lhs: Box::new(a),
2042            rhs: Box::new(b),
2043        };
2044        let som = |v: ExprSpec| ExprSpec::OptionSome(Box::new(v));
2045        let non = || ExprSpec::OptionNone { elem: Type::Number };
2046        let unwrap = |o: ExprSpec| ExprSpec::OptionMatch {
2047            opt: Box::new(o),
2048            some_bind: "v".into(),
2049            some_body: Box::new(r("v")),
2050            none_body: Box::new(num(-1)),
2051        };
2052        let dbl = || lam(vec![("x", Type::Number)], bin(BinOp::Mul, r("x"), num(2)));
2053        // pos10: x -> if x > 0 then Some(x*10) else None
2054        let pos10 = || {
2055            lam(
2056                vec![("x", Type::Number)],
2057                ExprSpec::If {
2058                    cond: Box::new(bin(BinOp::Gt, r("x"), num(0))),
2059                    then_branch: Box::new(som(bin(BinOp::Mul, r("x"), num(10)))),
2060                    else_branch: Box::new(non()),
2061                },
2062            )
2063        };
2064        let nf = |name: &str, result: ExprSpec| FunctionSpec {
2065            name: name.into(),
2066            type_params: vec![],
2067            params: vec![],
2068            produces: ext(Type::Number),
2069            requires: BTreeSet::new(),
2070            on_failure: vec![],
2071            steps: vec![],
2072            result,
2073        };
2074        let mut fns = list_functions();
2075        fns.extend([
2076            nf("m_some", unwrap(call("opt_map", vec![dbl(), som(num(5))]))), // 10
2077            nf("m_none", unwrap(call("opt_map", vec![dbl(), non()]))),       // -1
2078            nf("t_some", unwrap(call("opt_then", vec![pos10(), som(num(3))]))), // 30
2079            nf("t_zero", unwrap(call("opt_then", vec![pos10(), som(num(-1))]))), // -1
2080            nf("t_none", unwrap(call("opt_then", vec![pos10(), non()]))),    // -1
2081        ]);
2082        let e = Editor::new(Store::open_in_memory().unwrap());
2083        let (m, report) = e
2084            .apply_module(&ModuleSpec {
2085                name: "stdoptm".into(),
2086                types: vec![],
2087                functions: fns,
2088            })
2089            .unwrap();
2090        assert!(report.ok(), "violations: {:?}", report.violations);
2091        let wasm = lower(e.store(), &m).unwrap();
2092        assert_eq!(run_i64(&wasm, "m_some", &[]).unwrap(), 10);
2093        assert_eq!(run_i64(&wasm, "m_none", &[]).unwrap(), -1);
2094        assert_eq!(run_i64(&wasm, "t_some", &[]).unwrap(), 30);
2095        assert_eq!(run_i64(&wasm, "t_zero", &[]).unwrap(), -1);
2096        assert_eq!(run_i64(&wasm, "t_none", &[]).unwrap(), -1);
2097    }
2098
2099    /// iabs / imin / imax / clamp (clamp composed from imin∘imax).
2100    #[test]
2101    fn the_stdlib_integer_helpers() {
2102        let nf = |name: &str, result: ExprSpec| FunctionSpec {
2103            name: name.into(),
2104            type_params: vec![],
2105            params: vec![],
2106            produces: ext(Type::Number),
2107            requires: BTreeSet::new(),
2108            on_failure: vec![],
2109            steps: vec![],
2110            result,
2111        };
2112        let mut fns = list_functions();
2113        fns.extend([
2114            nf("ab_neg", call("iabs", vec![num(-7)])),       // 7
2115            nf("ab_pos", call("iabs", vec![num(3)])),        // 3
2116            nf("mn", call("imin", vec![num(2), num(5)])),    // 2
2117            nf("mx", call("imax", vec![num(2), num(5)])),    // 5
2118            nf("c_hi", call("clamp", vec![num(10), num(0), num(5)])), // 5
2119            nf("c_lo", call("clamp", vec![num(-3), num(0), num(5)])), // 0
2120            nf("c_in", call("clamp", vec![num(3), num(0), num(5)])),  // 3
2121        ]);
2122        let e = Editor::new(Store::open_in_memory().unwrap());
2123        let (m, report) = e
2124            .apply_module(&ModuleSpec {
2125                name: "stdnum".into(),
2126                types: vec![],
2127                functions: fns,
2128            })
2129            .unwrap();
2130        assert!(report.ok(), "violations: {:?}", report.violations);
2131        let wasm = lower(e.store(), &m).unwrap();
2132        for (n, want) in [
2133            ("ab_neg", 7),
2134            ("ab_pos", 3),
2135            ("mn", 2),
2136            ("mx", 5),
2137            ("c_hi", 5),
2138            ("c_lo", 0),
2139            ("c_in", 3),
2140        ] {
2141            assert_eq!(run_i64(&wasm, n, &[]).unwrap(), want, "{n}");
2142        }
2143    }
2144
2145    /// ipow (exp<=0 → 1) and gcd (Euclid, sign-normalised).
2146    #[test]
2147    fn the_stdlib_ipow_and_gcd() {
2148        let nf = |name: &str, result: ExprSpec| FunctionSpec {
2149            name: name.into(),
2150            type_params: vec![],
2151            params: vec![],
2152            produces: ext(Type::Number),
2153            requires: BTreeSet::new(),
2154            on_failure: vec![],
2155            steps: vec![],
2156            result,
2157        };
2158        let mut fns = list_functions();
2159        fns.extend([
2160            nf("p_2_10", call("ipow", vec![num(2), num(10)])), // 1024
2161            nf("p_5_0", call("ipow", vec![num(5), num(0)])),   // 1
2162            nf("p_3_3", call("ipow", vec![num(3), num(3)])),   // 27
2163            nf("p_neg", call("ipow", vec![num(7), num(-1)])),  // 1
2164            nf("g_12_8", call("gcd", vec![num(12), num(8)])),  // 4
2165            nf("g_54_24", call("gcd", vec![num(54), num(24)])), // 6
2166            nf("g_7_0", call("gcd", vec![num(7), num(0)])),    // 7
2167            nf("g_0_0", call("gcd", vec![num(0), num(0)])),    // 0
2168            nf("g_neg", call("gcd", vec![num(-12), num(8)])),  // 4
2169        ]);
2170        let e = Editor::new(Store::open_in_memory().unwrap());
2171        let (m, report) = e
2172            .apply_module(&ModuleSpec {
2173                name: "stdpow".into(),
2174                types: vec![],
2175                functions: fns,
2176            })
2177            .unwrap();
2178        assert!(report.ok(), "violations: {:?}", report.violations);
2179        let wasm = lower(e.store(), &m).unwrap();
2180        for (n, want) in [
2181            ("p_2_10", 1024),
2182            ("p_5_0", 1),
2183            ("p_3_3", 27),
2184            ("p_neg", 1),
2185            ("g_12_8", 4),
2186            ("g_54_24", 6),
2187            ("g_7_0", 7),
2188            ("g_0_0", 0),
2189            ("g_neg", 4),
2190        ] {
2191            assert_eq!(run_i64(&wasm, n, &[]).unwrap(), want, "{n}");
2192        }
2193    }
2194
2195    /// decimal_round_str: half-up (away from zero) to N (0..4) places,
2196    /// formatted; composed from clamp/ipow/iabs/lpad0.
2197    #[test]
2198    fn decimal_round_str_formats() {
2199        let probe = |name: &str, v: f64, places: i64, want: &str| FunctionSpec {
2200            name: name.into(),
2201            type_params: vec![],
2202            params: vec![],
2203            produces: ext(Type::Bool),
2204            requires: BTreeSet::new(),
2205            on_failure: vec![],
2206            steps: vec![],
2207            result: ExprSpec::StrEq(
2208                Box::new(call(
2209                    "decimal_round_str",
2210                    vec![ExprSpec::Decimal(v), num(places)],
2211                )),
2212                Box::new(ExprSpec::Str(want.into())),
2213            ),
2214        };
2215        let cases = [
2216            ("a", 1.25, 1, "1.3"),
2217            ("b", 1.24, 1, "1.2"),
2218            ("c", 19.99, 1, "20.0"),
2219            ("d", 1.255, 2, "1.26"),
2220            ("e", -1.255, 2, "-1.26"),
2221            ("f", 2.5, 0, "3"),
2222            ("g", 1.4, 0, "1"),
2223            ("h", 1.2345, 4, "1.2345"),
2224            ("i", 0.0, 2, "0.00"),
2225        ];
2226        let mut fns = list_functions();
2227        fns.extend(cases.iter().map(|(n, v, pl, w)| probe(n, *v, *pl, w)));
2228        let e = Editor::new(Store::open_in_memory().unwrap());
2229        let (m, report) = e
2230            .apply_module(&ModuleSpec {
2231                name: "stddr".into(),
2232                types: vec![],
2233                functions: fns,
2234            })
2235            .unwrap();
2236        assert!(report.ok(), "violations: {:?}", report.violations);
2237        let wasm = lower(e.store(), &m).unwrap();
2238        for (n, _, _, w) in cases {
2239            assert_eq!(
2240                run_i64(&wasm, n, &[]).unwrap(),
2241                1,
2242                "decimal_round_str should produce {w}"
2243            );
2244        }
2245    }
2246}