Skip to main content

cairn_core/
check.rs

1//! The single Core checker.
2//!
3//! There is exactly one checker, here, and every front end goes through it
4//! (`docs/design.md` Section 7). Every mutating operation gets back a
5//! [`Report`] — the verification verdict (Section 6). An incomplete program
6//! (one with holes) is not an error; it is a normal state.
7//!
8//! What is checked, and which Section 2 principle a failure maps to:
9//!
10//! - structural integrity — every referenced child hash is present (P4)
11//! - locality — every name (binding or callee) resolves (P1)
12//! - types — argument and result types honor declared signatures (P2)
13//! - confidence — arguments meet each param's `min_confidence`, and a
14//!   function's result is no weaker than its declared `produces` (P7)
15//! - effects — a function's `requires` covers every effect it performs,
16//!   effects propagating by union (P5)
17//! - failures — every failure a callee declares is covered by the caller's
18//!   `on_failure` (P6)
19//!
20//! ## Why this is incremental
21//!
22//! The store is content-addressed, so a [`NodeHash`] denotes one immutable
23//! subtree forever. Facts that depend only on a subtree's content
24//! ([`SubtreeFacts`]: holes and structural integrity) are a pure function of
25//! its hash and are memoized by hash; re-checking after an edit recomputes
26//! only changed hashes. That is the Section 7 incrementality claim, falling
27//! out of content-addressing rather than being engineered. The typed walk
28//! (name/type/confidence/effect/failure) is context-dependent and is
29//! recomputed; memoizing it on `(hash, scope)` is a later refinement.
30
31use crate::node::{MatchArm, Node, NodeHash, Produces};
32use crate::store::{Result, Store};
33use crate::ty::{Confidence, Effect, Type};
34use serde::Serialize;
35use std::cell::Cell;
36use std::collections::{BTreeSet, HashMap};
37
38/// Whether a subtree has unfilled holes.
39#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize)]
40pub enum Status {
41    /// No holes anywhere in the subtree.
42    Complete,
43    /// At least one hole remains. A normal state, not an error.
44    Incomplete,
45}
46
47/// Whether every callee-declared failure is covered by the caller.
48#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
49pub enum Failures {
50    Exhaustive,
51    Uncovered(Vec<String>),
52}
53
54/// A broken design principle, with the offending node and a reason. The
55/// `principle` is the Section 2 principle number.
56#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
57pub struct Violation {
58    pub principle: u8,
59    pub node: NodeHash,
60    pub detail: String,
61}
62
63/// The verification verdict for a checked root (`docs/design.md` Section 6).
64#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
65pub struct Report {
66    pub node: NodeHash,
67    pub status: Status,
68    pub holes: Vec<NodeHash>,
69    /// Effects actually performed (union of callee `requires`).
70    pub effects: BTreeSet<Effect>,
71    /// Confidence at the produces site, when inferable.
72    pub confidence: Option<Confidence>,
73    pub failures: Failures,
74    pub violations: Vec<Violation>,
75}
76
77impl Report {
78    /// True when nothing invalid was found. Holes are not violations, so an
79    /// `Incomplete` report can still be `ok`.
80    pub fn ok(&self) -> bool {
81        self.violations.is_empty()
82    }
83}
84
85/// A function's checkable contract, extracted from its node.
86#[derive(Clone)]
87struct Signature {
88    type_params: Vec<String>,
89    params: Vec<crate::node::Param>,
90    produces: Produces,
91    requires: BTreeSet<Effect>,
92    on_failure: Vec<String>,
93}
94
95/// Content-only facts about a subtree: holes it contains and referenced child
96/// hashes absent from the store. A pure function of the subtree's hash.
97#[derive(Clone, Default)]
98struct SubtreeFacts {
99    holes: Vec<NodeHash>,
100    missing: Vec<NodeHash>,
101}
102
103/// What checking one function produced.
104struct FnResult {
105    result_confidence: Option<Confidence>,
106    effects: BTreeSet<Effect>,
107    uncovered_failures: Vec<String>,
108}
109
110/// An ordered name → (type, confidence) scope. Later entries shadow earlier
111/// ones; an entry is `None` when its value's type could not be inferred (so
112/// dependent checks are skipped rather than cascading).
113type Scope = Vec<(String, Option<(Type, Confidence)>)>;
114
115/// Record type name → its fields.
116type RecordTable = HashMap<String, Vec<(String, Type)>>;
117/// Variant type name → its cases (case name → payload fields).
118type VariantTable = HashMap<String, Vec<(String, Vec<(String, Type)>)>>;
119
120/// The checker. Borrows a [`Store`]; owns a per-instance memo of
121/// [`SubtreeFacts`] keyed by content hash.
122pub struct Checker<'a> {
123    store: &'a Store,
124    facts: HashMap<NodeHash, SubtreeFacts>,
125    /// Populated from the root module at the start of `check`, read while
126    /// walking.
127    records: RecordTable,
128    variants: VariantTable,
129    computed: Cell<u64>,
130}
131
132impl<'a> Checker<'a> {
133    pub fn new(store: &'a Store) -> Self {
134        Self {
135            store,
136            facts: HashMap::new(),
137            records: HashMap::new(),
138            variants: HashMap::new(),
139            computed: Cell::new(0),
140        }
141    }
142
143    /// Nodes whose content-facts were computed rather than memoized. Used by
144    /// tests to demonstrate incrementality.
145    pub fn computed_count(&self) -> u64 {
146        self.computed.get()
147    }
148
149    /// Check the subtree rooted at `root` and produce its [`Report`].
150    pub fn check(&mut self, root: &NodeHash) -> Result<Report> {
151        let Some(root_node) = self.store.get(root)? else {
152            return Ok(Report {
153                node: root.clone(),
154                status: Status::Complete,
155                holes: Vec::new(),
156                effects: BTreeSet::new(),
157                confidence: None,
158                failures: Failures::Exhaustive,
159                violations: vec![Violation {
160                    principle: 4,
161                    node: root.clone(),
162                    detail: "root node is not present in the store".into(),
163                }],
164            });
165        };
166
167        let facts = self.subtree_facts(root)?;
168        let mut violations: Vec<Violation> = facts
169            .missing
170            .iter()
171            .map(|h| Violation {
172                principle: 4,
173                node: h.clone(),
174                detail: "referenced child node is not present in the store".into(),
175            })
176            .collect();
177
178        let sigs = self.signatures(root, &root_node)?;
179        self.records = self.record_defs(&root_node)?;
180        self.variants = self.variant_defs(&root_node)?;
181        let mut effects = BTreeSet::new();
182        let mut confidence = None;
183        let mut uncovered: Vec<String> = Vec::new();
184
185        match &root_node {
186            Node::Function { .. } => {
187                let fr = self.check_function(root, &root_node, &sigs, &mut violations)?;
188                effects = fr.effects;
189                confidence = fr.result_confidence;
190                uncovered = fr.uncovered_failures;
191            }
192            Node::Module { functions, .. } => {
193                for fh in functions {
194                    if let Some(fnode) = self.store.get(fh)? {
195                        let fr = self.check_function(fh, &fnode, &sigs, &mut violations)?;
196                        effects.extend(fr.effects);
197                        for u in fr.uncovered_failures {
198                            if !uncovered.contains(&u) {
199                                uncovered.push(u);
200                            }
201                        }
202                    }
203                }
204            }
205            _ => {
206                // A bare expression root: still report unresolved names.
207                let mut fl = BTreeSet::new();
208                confidence = self
209                    .walk_expr(root, &[], &sigs, &mut violations, &mut effects, &mut fl)?
210                    .map(|(_, c)| c);
211            }
212        }
213
214        Ok(Report {
215            node: root.clone(),
216            status: if facts.holes.is_empty() {
217                Status::Complete
218            } else {
219                Status::Incomplete
220            },
221            holes: facts.holes,
222            effects,
223            confidence,
224            failures: if uncovered.is_empty() {
225                Failures::Exhaustive
226            } else {
227                Failures::Uncovered(uncovered)
228            },
229            violations,
230        })
231    }
232
233    /// Content-only facts for a subtree, memoized by hash. The caller
234    /// guarantees `hash` is present in the store.
235    fn subtree_facts(&mut self, hash: &NodeHash) -> Result<SubtreeFacts> {
236        if let Some(cached) = self.facts.get(hash) {
237            return Ok(cached.clone());
238        }
239        self.computed.set(self.computed.get() + 1);
240
241        let node = self
242            .store
243            .get(hash)?
244            .expect("subtree_facts caller guarantees presence");
245
246        let mut facts = SubtreeFacts::default();
247        if let Node::Hole { .. } = node {
248            facts.holes.push(hash.clone());
249        }
250        for child in child_hashes(&node) {
251            match self.store.get(child)? {
252                None => facts.missing.push(child.clone()),
253                Some(_) => {
254                    let sub = self.subtree_facts(child)?;
255                    facts.holes.extend(sub.holes);
256                    facts.missing.extend(sub.missing);
257                }
258            }
259        }
260
261        self.facts.insert(hash.clone(), facts.clone());
262        Ok(facts)
263    }
264
265    /// Build the name → [`Signature`] table the root resolves calls against:
266    /// every function in a module, or a lone function (so self-calls resolve).
267    fn signatures(&self, _root: &NodeHash, root_node: &Node) -> Result<HashMap<String, Signature>> {
268        let mut sigs = HashMap::new();
269        let mut add = |node: &Node| {
270            if let Node::Function {
271                name,
272                type_params,
273                params,
274                produces,
275                requires,
276                on_failure,
277                ..
278            } = node
279            {
280                sigs.insert(
281                    name.clone(),
282                    Signature {
283                        type_params: type_params.clone(),
284                        params: params.clone(),
285                        produces: produces.clone(),
286                        requires: requires.clone(),
287                        on_failure: on_failure.clone(),
288                    },
289                );
290            }
291        };
292        match root_node {
293            Node::Function { .. } => add(root_node),
294            Node::Module { functions, .. } => {
295                for fh in functions {
296                    if let Some(fnode) = self.store.get(fh)? {
297                        add(&fnode);
298                    }
299                }
300            }
301            _ => {}
302        }
303        Ok(sigs)
304    }
305
306    /// Build the record-type table the root resolves type names against:
307    /// every `RecordDef` in a module, or a lone `RecordDef`.
308    fn record_defs(&self, root_node: &Node) -> Result<RecordTable> {
309        let mut recs = HashMap::new();
310        let mut add = |node: &Node| {
311            if let Node::RecordDef { name, fields } = node {
312                recs.insert(name.clone(), fields.clone());
313            }
314        };
315        match root_node {
316            Node::RecordDef { .. } => add(root_node),
317            Node::Module { types, .. } => {
318                for th in types {
319                    if let Some(tnode) = self.store.get(th)? {
320                        add(&tnode);
321                    }
322                }
323            }
324            _ => {}
325        }
326        Ok(recs)
327    }
328
329    /// Build the variant-type table: every `VariantDef` in a module, or a
330    /// lone `VariantDef`.
331    fn variant_defs(&self, root_node: &Node) -> Result<VariantTable> {
332        let mut vars = HashMap::new();
333        let mut add = |node: &Node| {
334            if let Node::VariantDef { name, cases } = node {
335                vars.insert(name.clone(), cases.clone());
336            }
337        };
338        match root_node {
339            Node::VariantDef { .. } => add(root_node),
340            Node::Module { types, .. } => {
341                for th in types {
342                    if let Some(tnode) = self.store.get(th)? {
343                        add(&tnode);
344                    }
345                }
346            }
347            _ => {}
348        }
349        Ok(vars)
350    }
351
352    /// Check one function: scope, types, confidence, effects, failures.
353    fn check_function(
354        &self,
355        fn_hash: &NodeHash,
356        fn_node: &Node,
357        sigs: &HashMap<String, Signature>,
358        out: &mut Vec<Violation>,
359    ) -> Result<FnResult> {
360        let Node::Function {
361            params,
362            produces,
363            requires,
364            on_failure,
365            body,
366            result,
367            ..
368        } = fn_node
369        else {
370            return Ok(FnResult {
371                result_confidence: None,
372                effects: BTreeSet::new(),
373                uncovered_failures: Vec::new(),
374            });
375        };
376
377        let mut scope: Scope = params
378            .iter()
379            .map(|p| (p.name.clone(), Some((p.ty.clone(), p.min_confidence))))
380            .collect();
381        let mut effects = BTreeSet::new();
382        let mut failures = BTreeSet::new();
383
384        for step_hash in body {
385            let Some(step) = self.store.get(step_hash)? else {
386                continue;
387            };
388            if let Node::Step { binding, value } = &step {
389                let v = self
390                    .walk_expr(value, &scope, sigs, out, &mut effects, &mut failures)?;
391                scope.push((binding.clone(), v));
392            }
393        }
394
395        let result_confidence = match self
396            .walk_expr(result, &scope, sigs, out, &mut effects, &mut failures)?
397        {
398            Some((rt, rc)) => {
399                if !compatible(&rt, &produces.ty) {
400                    out.push(Violation {
401                        principle: 2,
402                        node: result.clone(),
403                        detail: format!(
404                            "result is {:?} but `produces` declares {:?}",
405                            rt, produces.ty
406                        ),
407                    });
408                }
409                if rc < produces.confidence {
410                    out.push(Violation {
411                        principle: 7,
412                        node: result.clone(),
413                        detail: format!(
414                            "result confidence {:?} is weaker than declared {:?}",
415                            rc, produces.confidence
416                        ),
417                    });
418                }
419                Some(rc)
420            }
421            None => None,
422        };
423
424        // Effects propagate by union; `requires` must cover them (P5).
425        for e in &effects {
426            if !requires.contains(e) {
427                out.push(Violation {
428                    principle: 5,
429                    node: fn_hash.clone(),
430                    detail: format!("performs effect {e:?} not declared in `requires`"),
431                });
432            }
433        }
434
435        // Every callee-declared failure must be in this function's
436        // `on_failure` (P6).
437        let mut uncovered_failures = Vec::new();
438        for f in &failures {
439            if !on_failure.contains(f) {
440                uncovered_failures.push(f.clone());
441                out.push(Violation {
442                    principle: 6,
443                    node: fn_hash.clone(),
444                    detail: format!("failure `{f}` is not covered by `on_failure`"),
445                });
446            }
447        }
448
449        Ok(FnResult {
450            result_confidence,
451            effects,
452            uncovered_failures,
453        })
454    }
455
456    /// Infer an expression's `(type, confidence)`, reporting name, type, and
457    /// confidence violations and accumulating effects and failures. `None`
458    /// means "not inferable" (an unresolved name or a hole) — dependent checks
459    /// are then skipped rather than cascading.
460    #[allow(clippy::only_used_in_recursion)]
461    fn walk_expr(
462        &self,
463        hash: &NodeHash,
464        scope: &[(String, Option<(Type, Confidence)>)],
465        sigs: &HashMap<String, Signature>,
466        out: &mut Vec<Violation>,
467        effects: &mut BTreeSet<Effect>,
468        failures: &mut BTreeSet<String>,
469    ) -> Result<Option<(Type, Confidence)>> {
470        let Some(node) = self.store.get(hash)? else {
471            // Absence is a structural violation, already reported via facts.
472            return Ok(None);
473        };
474        Ok(match node {
475            Node::Lit(_) => Some((Type::Number, Confidence::Structural)),
476            Node::FloatLit(_) => Some((Type::Float, Confidence::Structural)),
477            Node::FloatOp { op, lhs, rhs } => {
478                let l = self.walk_expr(&lhs, scope, sigs, out, effects, failures)?;
479                let r = self.walk_expr(&rhs, scope, sigs, out, effects, failures)?;
480                for (h, t) in [(&lhs, &l), (&rhs, &r)] {
481                    if let Some((ty, _)) = t {
482                        if *ty != Type::Float && *ty != Type::Never {
483                            out.push(Violation {
484                                principle: 2,
485                                node: h.clone(),
486                                detail: format!(
487                                    "float operand is {ty:?}, expected Float"
488                                ),
489                            });
490                        }
491                    }
492                }
493                let conf = match (&l, &r) {
494                    (Some((_, a)), Some((_, b))) => (*a).min(*b),
495                    _ => Confidence::Structural,
496                };
497                use crate::node::BinOp as B;
498                if op.is_logical() || op == B::Mod || op == B::Neq {
499                    out.push(Violation {
500                        principle: 2,
501                        node: hash.clone(),
502                        detail: format!(
503                            "operator `{}` is not defined on Float",
504                            op.symbol()
505                        ),
506                    });
507                    None
508                } else if op.is_comparison() {
509                    Some((Type::Bool, conf))
510                } else {
511                    Some((Type::Float, conf))
512                }
513            }
514            Node::IntToFloat(a) => {
515                let t = self.walk_expr(&a, scope, sigs, out, effects, failures)?;
516                if let Some((ty, c)) = t {
517                    if ty != Type::Number && ty != Type::Never {
518                        out.push(Violation {
519                            principle: 2,
520                            node: a.clone(),
521                            detail: format!("to_float expects Number, got {ty:?}"),
522                        });
523                    }
524                    Some((Type::Float, c))
525                } else {
526                    None
527                }
528            }
529            Node::FloatToInt(a) => {
530                let t = self.walk_expr(&a, scope, sigs, out, effects, failures)?;
531                if let Some((ty, c)) = t {
532                    if ty != Type::Float && ty != Type::Never {
533                        out.push(Violation {
534                            principle: 2,
535                            node: a.clone(),
536                            detail: format!("to_int expects Float, got {ty:?}"),
537                        });
538                    }
539                    Some((Type::Number, c))
540                } else {
541                    None
542                }
543            }
544            Node::DecimalLit(_) => {
545                Some((Type::Decimal, Confidence::Structural))
546            }
547            Node::DecimalOp { op, lhs, rhs } => {
548                let l = self.walk_expr(&lhs, scope, sigs, out, effects, failures)?;
549                let r = self.walk_expr(&rhs, scope, sigs, out, effects, failures)?;
550                for (h, t) in [(&lhs, &l), (&rhs, &r)] {
551                    if let Some((ty, _)) = t {
552                        if *ty != Type::Decimal && *ty != Type::Never {
553                            out.push(Violation {
554                                principle: 2,
555                                node: h.clone(),
556                                detail: format!(
557                                    "decimal operand is {ty:?}, expected Decimal"
558                                ),
559                            });
560                        }
561                    }
562                }
563                let conf = match (&l, &r) {
564                    (Some((_, a)), Some((_, b))) => (*a).min(*b),
565                    _ => Confidence::Structural,
566                };
567                use crate::node::BinOp as B;
568                if op.is_logical() || op == B::Mod {
569                    out.push(Violation {
570                        principle: 2,
571                        node: hash.clone(),
572                        detail: format!(
573                            "operator `{}` is not defined on Decimal",
574                            op.symbol()
575                        ),
576                    });
577                    None
578                } else if op.is_comparison() {
579                    Some((Type::Bool, conf))
580                } else {
581                    Some((Type::Decimal, conf))
582                }
583            }
584            Node::IntToDecimal(a) => {
585                let t = self.walk_expr(&a, scope, sigs, out, effects, failures)?;
586                if let Some((ty, c)) = t {
587                    if ty != Type::Number && ty != Type::Never {
588                        out.push(Violation {
589                            principle: 2,
590                            node: a.clone(),
591                            detail: format!(
592                                "to_decimal expects Number, got {ty:?}"
593                            ),
594                        });
595                    }
596                    Some((Type::Decimal, c))
597                } else {
598                    None
599                }
600            }
601            Node::DecimalToInt(a) | Node::DecimalRaw(a) => {
602                let t = self.walk_expr(&a, scope, sigs, out, effects, failures)?;
603                if let Some((ty, c)) = t {
604                    if ty != Type::Decimal && ty != Type::Never {
605                        out.push(Violation {
606                            principle: 2,
607                            node: a.clone(),
608                            detail: format!(
609                                "expects Decimal, got {ty:?}"
610                            ),
611                        });
612                    }
613                    Some((Type::Number, c))
614                } else {
615                    None
616                }
617            }
618            Node::Bool(_) => Some((Type::Bool, Confidence::Structural)),
619            Node::Not(arg) => {
620                let a = self.walk_expr(&arg, scope, sigs, out, effects, failures)?;
621                if let Some((at, ac)) = a {
622                    if at != Type::Bool && at != Type::Never {
623                        out.push(Violation {
624                            principle: 2,
625                            node: arg.clone(),
626                            detail: format!("`!` operand is {at:?}, expected Bool"),
627                        });
628                    }
629                    Some((Type::Bool, ac))
630                } else {
631                    None
632                }
633            }
634            Node::Str(_) => Some((Type::String, Confidence::Structural)),
635            Node::Now => {
636                // Performs the Time effect (the existing P5 coverage check
637                // then requires the enclosing function to declare it) and
638                // yields an external-confidence Number.
639                effects.insert(Effect::Time);
640                Some((Type::Number, Confidence::External))
641            }
642            Node::List(elems) => {
643                if elems.is_empty() {
644                    out.push(Violation {
645                        principle: 2,
646                        node: hash.clone(),
647                        detail: "empty list literal needs a type annotation (v0.3)"
648                            .into(),
649                    });
650                    return Ok(None);
651                }
652                let mut elem_ty: Option<Type> = None;
653                let mut conf = Confidence::Persisted;
654                for e in &elems {
655                    if let Some((t, c)) =
656                        self.walk_expr(e, scope, sigs, out, effects, failures)?
657                    {
658                        match &elem_ty {
659                            None => elem_ty = Some(t),
660                            Some(et) => {
661                                if !compatible(et, &t) {
662                                    out.push(Violation {
663                                        principle: 2,
664                                        node: e.clone(),
665                                        detail: format!(
666                                            "list element is {t:?}, expected {et:?}"
667                                        ),
668                                    });
669                                }
670                            }
671                        }
672                        conf = conf.min(c);
673                    }
674                }
675                elem_ty.map(|t| (Type::List(Box::new(t)), conf))
676            }
677            Node::ListEmpty { elem } => {
678                Some((Type::List(Box::new(elem)), Confidence::Structural))
679            }
680            Node::ListCons { head, tail } => {
681                let ht = self.walk_expr(&head, scope, sigs, out, effects, failures)?;
682                let tt = self.walk_expr(&tail, scope, sigs, out, effects, failures)?;
683                match tt {
684                    Some((Type::List(et), tc)) => {
685                        if let Some((h, _)) = &ht {
686                            if !compatible(&et, h) {
687                                out.push(Violation {
688                                    principle: 2,
689                                    node: head.clone(),
690                                    detail: format!(
691                                        "cons head is {h:?} but the list is List<{et:?}>"
692                                    ),
693                                });
694                            }
695                        }
696                        let hc =
697                            ht.map(|(_, c)| c).unwrap_or(Confidence::Persisted);
698                        Some((Type::List(et), hc.min(tc)))
699                    }
700                    Some((other, _)) => {
701                        out.push(Violation {
702                            principle: 2,
703                            node: tail.clone(),
704                            detail: format!(
705                                "cons tail must be a List, got {other:?}"
706                            ),
707                        });
708                        None
709                    }
710                    None => None,
711                }
712            }
713            Node::OptionSome(v) => self
714                .walk_expr(&v, scope, sigs, out, effects, failures)?
715                .map(|(t, c)| (Type::Option(Box::new(t)), c)),
716            Node::OptionNone { elem } => {
717                Some((Type::Option(Box::new(elem)), Confidence::Structural))
718            }
719            Node::OptionElse { opt, default } => {
720                let ot = self.walk_expr(&opt, scope, sigs, out, effects, failures)?;
721                let dt =
722                    self.walk_expr(&default, scope, sigs, out, effects, failures)?;
723                match ot {
724                    Some((Type::Option(inner), oc)) => {
725                        if let Some((d, _)) = &dt {
726                            if !compatible(&inner, d) {
727                                out.push(Violation {
728                                    principle: 2,
729                                    node: default.clone(),
730                                    detail: format!(
731                                        "option default is {d:?} but the Option holds {inner:?}"
732                                    ),
733                                });
734                            }
735                        }
736                        let dc =
737                            dt.map(|(_, c)| c).unwrap_or(Confidence::Persisted);
738                        Some((*inner, oc.min(dc)))
739                    }
740                    Some((other, _)) => {
741                        out.push(Violation {
742                            principle: 2,
743                            node: opt.clone(),
744                            detail: format!(
745                                "option_else expects an Option, got {other:?}"
746                            ),
747                        });
748                        None
749                    }
750                    None => None,
751                }
752            }
753            Node::OptionMatch {
754                opt,
755                some_bind,
756                some_body,
757                none_body,
758            } => {
759                let ot = self.walk_expr(&opt, scope, sigs, out, effects, failures)?;
760                let (inner, oc) = match ot {
761                    Some((Type::Option(inner), oc)) => (*inner, oc),
762                    Some((Type::Never, oc)) => (Type::Never, oc),
763                    Some((other, _)) => {
764                        out.push(Violation {
765                            principle: 2,
766                            node: opt.clone(),
767                            detail: format!(
768                                "OptionMatch scrutinee is {other:?}, expected Option"
769                            ),
770                        });
771                        return Ok(None);
772                    }
773                    None => return Ok(None),
774                };
775                let mut s2 = scope.to_vec();
776                s2.push((some_bind.clone(), Some((inner, oc))));
777                let st = self.walk_expr(
778                    &some_body, &s2, sigs, out, effects, failures,
779                )?;
780                let nt = self.walk_expr(
781                    &none_body, scope, sigs, out, effects, failures,
782                )?;
783                match (st, nt) {
784                    (Some((s, sc)), Some((n, nc))) => {
785                        if !compatible(&s, &n) {
786                            out.push(Violation {
787                                principle: 2,
788                                node: hash.clone(),
789                                detail: format!(
790                                    "OptionMatch arms differ: {s:?} vs {n:?}"
791                                ),
792                            });
793                        }
794                        let ty = if s == Type::Never { n } else { s };
795                        Some((ty, oc.min(sc).min(nc)))
796                    }
797                    (Some(v), None) | (None, Some(v)) => Some(v),
798                    (None, None) => None,
799                }
800            }
801            Node::ListTryGet { list, index } => {
802                let lt = self.walk_expr(&list, scope, sigs, out, effects, failures)?;
803                let it = self.walk_expr(&index, scope, sigs, out, effects, failures)?;
804                if let Some((t, _)) = &it {
805                    if *t != Type::Number && *t != Type::Never {
806                        out.push(Violation {
807                            principle: 2,
808                            node: index.clone(),
809                            detail: format!("list index is {t:?}, expected Number"),
810                        });
811                    }
812                }
813                match lt {
814                    Some((Type::List(elem), lc)) => {
815                        let ic =
816                            it.map(|(_, c)| c).unwrap_or(Confidence::Persisted);
817                        Some((Type::Option(elem), lc.min(ic)))
818                    }
819                    Some((other, _)) => {
820                        out.push(Violation {
821                            principle: 2,
822                            node: list.clone(),
823                            detail: format!(
824                                "list_try_get on a non-List type {other:?}"
825                            ),
826                        });
827                        None
828                    }
829                    None => None,
830                }
831            }
832            Node::ListLen(arg) => {
833                match self.walk_expr(&arg, scope, sigs, out, effects, failures)? {
834                    Some((Type::List(_), c)) => Some((Type::Number, c)),
835                    Some((other, _)) => {
836                        out.push(Violation {
837                            principle: 2,
838                            node: arg.clone(),
839                            detail: format!("list_len expects a List, got {other:?}"),
840                        });
841                        None
842                    }
843                    None => None,
844                }
845            }
846            Node::ListGet { list, index } => {
847                let lt = self.walk_expr(&list, scope, sigs, out, effects, failures)?;
848                let it = self.walk_expr(&index, scope, sigs, out, effects, failures)?;
849                if let Some((t, _)) = &it {
850                    if *t != Type::Number && *t != Type::Never {
851                        out.push(Violation {
852                            principle: 2,
853                            node: index.clone(),
854                            detail: format!("list index is {t:?}, expected Number"),
855                        });
856                    }
857                }
858                match lt {
859                    Some((Type::List(elem), lc)) => {
860                        let ic = it.map(|(_, c)| c).unwrap_or(Confidence::Persisted);
861                        Some((*elem, lc.min(ic)))
862                    }
863                    Some((other, _)) => {
864                        out.push(Violation {
865                            principle: 2,
866                            node: list.clone(),
867                            detail: format!("list_get on a non-List type {other:?}"),
868                        });
869                        None
870                    }
871                    None => None,
872                }
873            }
874            Node::Map(pairs) => {
875                if pairs.is_empty() {
876                    out.push(Violation {
877                        principle: 2,
878                        node: hash.clone(),
879                        detail: "empty map literal needs a type annotation (v0.3)"
880                            .into(),
881                    });
882                    return Ok(None);
883                }
884                let mut kt: Option<Type> = None;
885                let mut vt: Option<Type> = None;
886                let mut conf = Confidence::Persisted;
887                for (k, v) in &pairs {
888                    if let Some((t, c)) =
889                        self.walk_expr(k, scope, sigs, out, effects, failures)?
890                    {
891                        match &kt {
892                            None => kt = Some(t),
893                            Some(e) => {
894                                if !compatible(e, &t) {
895                                    out.push(Violation {
896                                        principle: 2,
897                                        node: k.clone(),
898                                        detail: format!(
899                                            "map key is {t:?}, expected {e:?}"
900                                        ),
901                                    });
902                                }
903                            }
904                        }
905                        conf = conf.min(c);
906                    }
907                    if let Some((t, c)) =
908                        self.walk_expr(v, scope, sigs, out, effects, failures)?
909                    {
910                        match &vt {
911                            None => vt = Some(t),
912                            Some(e) => {
913                                if !compatible(e, &t) {
914                                    out.push(Violation {
915                                        principle: 2,
916                                        node: v.clone(),
917                                        detail: format!(
918                                            "map value is {t:?}, expected {e:?}"
919                                        ),
920                                    });
921                                }
922                            }
923                        }
924                        conf = conf.min(c);
925                    }
926                }
927                match (kt, vt) {
928                    (Some(k), Some(v)) => {
929                        Some((Type::Map(Box::new(k), Box::new(v)), conf))
930                    }
931                    _ => None,
932                }
933            }
934            Node::MapGet { map, key } => {
935                let mt = self.walk_expr(&map, scope, sigs, out, effects, failures)?;
936                let kt = self.walk_expr(&key, scope, sigs, out, effects, failures)?;
937                match mt {
938                    Some((Type::Map(k, v), mc)) => {
939                        if let Some((t, _)) = &kt {
940                            if !compatible(&k, t) {
941                                out.push(Violation {
942                                    principle: 2,
943                                    node: key.clone(),
944                                    detail: format!(
945                                        "map key is {t:?}, expected {k:?}"
946                                    ),
947                                });
948                            }
949                        }
950                        let kc = kt.map(|(_, c)| c).unwrap_or(Confidence::Persisted);
951                        Some((*v, mc.min(kc)))
952                    }
953                    Some((other, _)) => {
954                        out.push(Violation {
955                            principle: 2,
956                            node: map.clone(),
957                            detail: format!("map_get on a non-Map type {other:?}"),
958                        });
959                        None
960                    }
961                    None => None,
962                }
963            }
964            Node::MapTryGet { map, key } => {
965                let mt = self.walk_expr(&map, scope, sigs, out, effects, failures)?;
966                let kt = self.walk_expr(&key, scope, sigs, out, effects, failures)?;
967                match mt {
968                    Some((Type::Map(k, v), mc)) => {
969                        if let Some((t, _)) = &kt {
970                            if !compatible(&k, t) {
971                                out.push(Violation {
972                                    principle: 2,
973                                    node: key.clone(),
974                                    detail: format!(
975                                        "map key is {t:?}, expected {k:?}"
976                                    ),
977                                });
978                            }
979                        }
980                        let kc = kt.map(|(_, c)| c).unwrap_or(Confidence::Persisted);
981                        Some((Type::Option(v), mc.min(kc)))
982                    }
983                    Some((Type::Never, c)) => Some((Type::Never, c)),
984                    Some((other, _)) => {
985                        out.push(Violation {
986                            principle: 2,
987                            node: map.clone(),
988                            detail: format!("map_try_get on a non-Map type {other:?}"),
989                        });
990                        None
991                    }
992                    None => None,
993                }
994            }
995            Node::MapLen(arg) => {
996                match self.walk_expr(&arg, scope, sigs, out, effects, failures)? {
997                    Some((Type::Map(_, _), c)) => Some((Type::Number, c)),
998                    Some((other, _)) => {
999                        out.push(Violation {
1000                            principle: 2,
1001                            node: arg.clone(),
1002                            detail: format!("map_len expects a Map, got {other:?}"),
1003                        });
1004                        None
1005                    }
1006                    None => None,
1007                }
1008            }
1009            Node::Log(arg) => {
1010                // Performs the Log effect; passes the value through
1011                // unchanged (same type and confidence).
1012                effects.insert(Effect::Log);
1013                self.walk_expr(&arg, scope, sigs, out, effects, failures)?
1014            }
1015            Node::Publish(arg) => {
1016                // Performs the Live effect. `topic` must be a String;
1017                // yields Number (0). The checker forces `requires Live`
1018                // on any function that can publish — liveness visible.
1019                effects.insert(Effect::Live);
1020                let t =
1021                    self.walk_expr(&arg, scope, sigs, out, effects, failures)?;
1022                if let Some((ty, _)) = t {
1023                    if ty != Type::String && ty != Type::Never {
1024                        out.push(Violation {
1025                            principle: 2,
1026                            node: arg.clone(),
1027                            detail: format!(
1028                                "publish topic is {ty:?}, expected String"
1029                            ),
1030                        });
1031                    }
1032                }
1033                Some((Type::Number, Confidence::Structural))
1034            }
1035            Node::SetHeader { name, value } => {
1036                // Performs the Resp effect. `name` and `value` must be
1037                // String; yields Number (0) so it sequences like
1038                // `publish`. The checker forces `requires Resp` on any
1039                // function that can set a header — visible, not hidden.
1040                effects.insert(Effect::Resp);
1041                for (label, arg) in
1042                    [("name", &name), ("value", &value)]
1043                {
1044                    let t = self.walk_expr(
1045                        arg, scope, sigs, out, effects, failures,
1046                    )?;
1047                    if let Some((ty, _)) = t {
1048                        if ty != Type::String && ty != Type::Never {
1049                            out.push(Violation {
1050                                principle: 2,
1051                                node: (*arg).clone(),
1052                                detail: format!(
1053                                    "set_header {label} is {ty:?}, \
1054                                     expected String"
1055                                ),
1056                            });
1057                        }
1058                    }
1059                }
1060                Some((Type::Number, Confidence::Structural))
1061            }
1062            Node::Rand => {
1063                effects.insert(Effect::Rand);
1064                Some((Type::Number, Confidence::External))
1065            }
1066            Node::MutNew(v) => {
1067                effects.insert(Effect::Mut);
1068                self.walk_expr(&v, scope, sigs, out, effects, failures)?
1069                    .map(|(t, c)| (Type::Cell(Box::new(t)), c))
1070            }
1071            Node::MutGet(cell) => {
1072                match self.walk_expr(&cell, scope, sigs, out, effects, failures)? {
1073                    Some((Type::Cell(t), c)) => Some((*t, c)),
1074                    Some((other, _)) => {
1075                        out.push(Violation {
1076                            principle: 2,
1077                            node: cell.clone(),
1078                            detail: format!("cell_get on a non-Cell type {other:?}"),
1079                        });
1080                        None
1081                    }
1082                    None => None,
1083                }
1084            }
1085            Node::MutSet { cell, value } => {
1086                effects.insert(Effect::Mut);
1087                let ct = self.walk_expr(&cell, scope, sigs, out, effects, failures)?;
1088                let vt =
1089                    self.walk_expr(&value, scope, sigs, out, effects, failures)?;
1090                if let (Some((Type::Cell(et), _)), Some((vty, _))) = (&ct, &vt) {
1091                    if !compatible(et, vty) {
1092                        out.push(Violation {
1093                            principle: 2,
1094                            node: value.clone(),
1095                            detail: format!(
1096                                "cell holds {et:?} but assigned {vty:?}"
1097                            ),
1098                        });
1099                    }
1100                } else if let Some((other, _)) = &ct {
1101                    if !matches!(other, Type::Cell(_)) {
1102                        out.push(Violation {
1103                            principle: 2,
1104                            node: cell.clone(),
1105                            detail: format!("cell_set on a non-Cell type {other:?}"),
1106                        });
1107                    }
1108                }
1109                vt // pass-through
1110            }
1111            Node::DiskWrite { path, content } => {
1112                effects.insert(Effect::Disk);
1113                let p = self.walk_expr(&path, scope, sigs, out, effects, failures)?;
1114                let cn =
1115                    self.walk_expr(&content, scope, sigs, out, effects, failures)?;
1116                for (n, t) in [(&path, &p), (&content, &cn)] {
1117                    if let Some((ty, _)) = t {
1118                        if *ty != Type::String && *ty != Type::Never {
1119                            out.push(Violation {
1120                                principle: 2,
1121                                node: n.clone(),
1122                                detail: format!("disk_write expects String, got {ty:?}"),
1123                            });
1124                        }
1125                    }
1126                }
1127                Some((Type::Number, Confidence::External))
1128            }
1129            Node::DiskRead(path) => {
1130                effects.insert(Effect::Disk);
1131                if let Some((t, _)) =
1132                    self.walk_expr(&path, scope, sigs, out, effects, failures)?
1133                {
1134                    if t != Type::String && t != Type::Never {
1135                        out.push(Violation {
1136                            principle: 2,
1137                            node: path.clone(),
1138                            detail: format!("disk_read expects String, got {t:?}"),
1139                        });
1140                    }
1141                }
1142                // Returns the file's contents (host→wasm allocation), at
1143                // external confidence (it comes from outside the program).
1144                Some((Type::String, Confidence::External))
1145            }
1146            Node::NetGet(url) => {
1147                effects.insert(Effect::Net);
1148                if let Some((t, _)) =
1149                    self.walk_expr(&url, scope, sigs, out, effects, failures)?
1150                {
1151                    if t != Type::String && t != Type::Never {
1152                        out.push(Violation {
1153                            principle: 2,
1154                            node: url.clone(),
1155                            detail: format!("net_get expects String, got {t:?}"),
1156                        });
1157                    }
1158                }
1159                Some((Type::Number, Confidence::External))
1160            }
1161            Node::DbQuery { sql, params } => {
1162                effects.insert(Effect::Db);
1163                if let Some((t, _)) =
1164                    self.walk_expr(&sql, scope, sigs, out, effects, failures)?
1165                {
1166                    if t != Type::String && t != Type::Never {
1167                        out.push(Violation {
1168                            principle: 2,
1169                            node: sql.clone(),
1170                            detail: format!("db_query expects String, got {t:?}"),
1171                        });
1172                    }
1173                }
1174                if let Some((t, _)) =
1175                    self.walk_expr(&params, scope, sigs, out, effects, failures)?
1176                {
1177                    let ok = matches!(&t, Type::List(e) if **e == Type::String)
1178                        || t == Type::Never;
1179                    if !ok {
1180                        out.push(Violation {
1181                            principle: 2,
1182                            node: params.clone(),
1183                            detail: format!(
1184                                "db_query params must be List<String>, got {t:?}"
1185                            ),
1186                        });
1187                    }
1188                }
1189                // Reading back from the system of record yields a `String`
1190                // result at `persisted` confidence — the decided definition.
1191                Some((Type::String, Confidence::Persisted))
1192            }
1193            Node::StrConcat(a, b) => {
1194                let at = self.walk_expr(&a, scope, sigs, out, effects, failures)?;
1195                let bt = self.walk_expr(&b, scope, sigs, out, effects, failures)?;
1196                for (n, t) in [(&a, &at), (&b, &bt)] {
1197                    if let Some((ty, _)) = t {
1198                        if *ty != Type::String && *ty != Type::Never {
1199                            out.push(Violation {
1200                                principle: 2,
1201                                node: n.clone(),
1202                                detail: format!("str_concat expects String, got {ty:?}"),
1203                            });
1204                        }
1205                    }
1206                }
1207                let c = at
1208                    .map(|(_, c)| c)
1209                    .unwrap_or(Confidence::Persisted)
1210                    .min(bt.map(|(_, c)| c).unwrap_or(Confidence::Persisted));
1211                Some((Type::String, c))
1212            }
1213            Node::StrSlice { s, start, len } => {
1214                let st = self.walk_expr(&s, scope, sigs, out, effects, failures)?;
1215                let stt =
1216                    self.walk_expr(&start, scope, sigs, out, effects, failures)?;
1217                let lnt = self.walk_expr(&len, scope, sigs, out, effects, failures)?;
1218                if let Some((t, _)) = &st {
1219                    if *t != Type::String && *t != Type::Never {
1220                        out.push(Violation {
1221                            principle: 2,
1222                            node: s.clone(),
1223                            detail: format!("str_slice expects String, got {t:?}"),
1224                        });
1225                    }
1226                }
1227                for (n, t) in [(&start, &stt), (&len, &lnt)] {
1228                    if let Some((ty, _)) = t {
1229                        if *ty != Type::Number && *ty != Type::Never {
1230                            out.push(Violation {
1231                                principle: 2,
1232                                node: n.clone(),
1233                                detail: format!("str_slice index is {ty:?}, expected Number"),
1234                            });
1235                        }
1236                    }
1237                }
1238                let c = [st, stt, lnt]
1239                    .into_iter()
1240                    .flatten()
1241                    .map(|(_, c)| c)
1242                    .min()
1243                    .unwrap_or(Confidence::Structural);
1244                Some((Type::String, c))
1245            }
1246            Node::StrEq(a, b)
1247            | Node::StrContains { haystack: a, needle: b }
1248            | Node::StrStartsWith { s: a, prefix: b } => {
1249                let at = self.walk_expr(&a, scope, sigs, out, effects, failures)?;
1250                let bt = self.walk_expr(&b, scope, sigs, out, effects, failures)?;
1251                for (n, t) in [(&a, &at), (&b, &bt)] {
1252                    if let Some((ty, _)) = t {
1253                        if *ty != Type::String && *ty != Type::Never {
1254                            out.push(Violation {
1255                                principle: 2,
1256                                node: n.clone(),
1257                                detail: format!("string op expects String, got {ty:?}"),
1258                            });
1259                        }
1260                    }
1261                }
1262                let c = at
1263                    .map(|(_, c)| c)
1264                    .unwrap_or(Confidence::Persisted)
1265                    .min(bt.map(|(_, c)| c).unwrap_or(Confidence::Persisted));
1266                Some((Type::Bool, c))
1267            }
1268            Node::StrIndexOf { haystack, needle } => {
1269                let ht =
1270                    self.walk_expr(&haystack, scope, sigs, out, effects, failures)?;
1271                let nt =
1272                    self.walk_expr(&needle, scope, sigs, out, effects, failures)?;
1273                for (node, t) in [(&haystack, &ht), (&needle, &nt)] {
1274                    if let Some((ty, _)) = t {
1275                        if *ty != Type::String && *ty != Type::Never {
1276                            out.push(Violation {
1277                                principle: 2,
1278                                node: node.clone(),
1279                                detail: format!(
1280                                    "string op expects String, got {ty:?}"
1281                                ),
1282                            });
1283                        }
1284                    }
1285                }
1286                let c = ht
1287                    .map(|(_, c)| c)
1288                    .unwrap_or(Confidence::Persisted)
1289                    .min(nt.map(|(_, c)| c).unwrap_or(Confidence::Persisted));
1290                Some((Type::Number, c))
1291            }
1292            Node::StrLen(arg) => {
1293                match self.walk_expr(&arg, scope, sigs, out, effects, failures)? {
1294                    Some((t, c)) => {
1295                        if t != Type::String && t != Type::Never {
1296                            out.push(Violation {
1297                                principle: 2,
1298                                node: arg.clone(),
1299                                detail: format!("str_len expects String, got {t:?}"),
1300                            });
1301                        }
1302                        Some((Type::Number, c))
1303                    }
1304                    None => None,
1305                }
1306            }
1307            Node::StrLower(arg) => {
1308                match self.walk_expr(&arg, scope, sigs, out, effects, failures)? {
1309                    Some((t, c)) => {
1310                        if t != Type::String && t != Type::Never {
1311                            out.push(Violation {
1312                                principle: 2,
1313                                node: arg.clone(),
1314                                detail: format!(
1315                                    "str_lower expects String, got {t:?}"
1316                                ),
1317                            });
1318                        }
1319                        Some((Type::String, c))
1320                    }
1321                    None => None,
1322                }
1323            }
1324            Node::StrFromCode(arg) => {
1325                match self.walk_expr(&arg, scope, sigs, out, effects, failures)? {
1326                    Some((t, c)) => {
1327                        if t != Type::Number && t != Type::Never {
1328                            out.push(Violation {
1329                                principle: 2,
1330                                node: arg.clone(),
1331                                detail: format!(
1332                                    "str_from_code expects Number, got {t:?}"
1333                                ),
1334                            });
1335                        }
1336                        Some((Type::String, c))
1337                    }
1338                    None => None,
1339                }
1340            }
1341            Node::NumberToStr(arg) => {
1342                match self.walk_expr(&arg, scope, sigs, out, effects, failures)? {
1343                    Some((t, c)) => {
1344                        if t != Type::Number && t != Type::Never {
1345                            out.push(Violation {
1346                                principle: 2,
1347                                node: arg.clone(),
1348                                detail: format!(
1349                                    "number_to_str expects Number, got {t:?}"
1350                                ),
1351                            });
1352                        }
1353                        Some((Type::String, c))
1354                    }
1355                    None => None,
1356                }
1357            }
1358            Node::StrToNumber(arg) => {
1359                match self.walk_expr(&arg, scope, sigs, out, effects, failures)? {
1360                    Some((t, c)) => {
1361                        if t != Type::String && t != Type::Never {
1362                            out.push(Violation {
1363                                principle: 2,
1364                                node: arg.clone(),
1365                                detail: format!(
1366                                    "str_to_number expects String, got {t:?}"
1367                                ),
1368                            });
1369                        }
1370                        Some((Type::Number, c))
1371                    }
1372                    None => None,
1373                }
1374            }
1375            Node::StrToNumberOpt(arg) => {
1376                match self.walk_expr(&arg, scope, sigs, out, effects, failures)? {
1377                    Some((t, c)) => {
1378                        if t != Type::String && t != Type::Never {
1379                            out.push(Violation {
1380                                principle: 2,
1381                                node: arg.clone(),
1382                                detail: format!(
1383                                    "str_to_number_opt expects String, got {t:?}"
1384                                ),
1385                            });
1386                        }
1387                        Some((Type::Option(Box::new(Type::Number)), c))
1388                    }
1389                    None => None,
1390                }
1391            }
1392            Node::Hole { .. } => None,
1393            Node::Ref(name) => {
1394                match scope.iter().rev().find(|(n, _)| n == &name) {
1395                    Some((_, v)) => v.clone(),
1396                    None => {
1397                        out.push(Violation {
1398                            principle: 1,
1399                            node: hash.clone(),
1400                            detail: format!("unresolved reference: `{name}`"),
1401                        });
1402                        None
1403                    }
1404                }
1405            }
1406            Node::Step { value, .. } => {
1407                self.walk_expr(&value, scope, sigs, out, effects, failures)?
1408            }
1409            Node::Fail(f) => {
1410                // Diverges: contributes the variant to the propagated failure
1411                // set (the existing P6 coverage check enforces `on_failure`),
1412                // has type Never, and carries top confidence so it never
1413                // drags a weakest-input minimum.
1414                failures.insert(f);
1415                Some((Type::Never, Confidence::Persisted))
1416            }
1417            Node::Handle { body, handlers } => {
1418                // Body failures are scoped here: handled variants are caught
1419                // and do not propagate; everything else does.
1420                let mut body_f = BTreeSet::new();
1421                let b = self.walk_expr(&body, scope, sigs, out, effects, &mut body_f)?;
1422                for f in &body_f {
1423                    if !handlers.iter().any(|(v, _)| v == f) {
1424                        failures.insert(f.clone());
1425                    }
1426                }
1427                match b {
1428                    None => None,
1429                    Some((bt, bc)) => {
1430                        let mut conf = bc;
1431                        for (_, recover) in &handlers {
1432                            if let Some((rt, rc)) = self
1433                                .walk_expr(recover, scope, sigs, out, effects, failures)?
1434                            {
1435                                if !compatible(&rt, &bt) {
1436                                    out.push(Violation {
1437                                        principle: 2,
1438                                        node: recover.clone(),
1439                                        detail: format!(
1440                                            "handler recovers as {rt:?} but the value is {bt:?}"
1441                                        ),
1442                                    });
1443                                }
1444                                conf = conf.min(rc);
1445                            }
1446                        }
1447                        Some((bt, conf))
1448                    }
1449                }
1450            }
1451            Node::If {
1452                cond,
1453                then_branch,
1454                else_branch,
1455            } => {
1456                let c = self.walk_expr(&cond, scope, sigs, out, effects, failures)?;
1457                let t = self.walk_expr(&then_branch, scope, sigs, out, effects, failures)?;
1458                let e = self.walk_expr(&else_branch, scope, sigs, out, effects, failures)?;
1459                if let Some((ct, _)) = &c {
1460                    if *ct != Type::Bool && *ct != Type::Never {
1461                        out.push(Violation {
1462                            principle: 2,
1463                            node: cond.clone(),
1464                            detail: format!("condition is {ct:?}, expected Bool"),
1465                        });
1466                    }
1467                }
1468                match (t, e) {
1469                    (Some((tt, tc)), Some((et, ec))) => {
1470                        if !compatible(&tt, &et) {
1471                            out.push(Violation {
1472                                principle: 2,
1473                                node: hash.clone(),
1474                                detail: format!(
1475                                    "branch types differ: {tt:?} vs {et:?}"
1476                                ),
1477                            });
1478                        }
1479                        // A `fail` branch is Never; the if's type is the other
1480                        // branch's. Weakest input over condition and branches.
1481                        let rty = if tt == Type::Never { et } else { tt };
1482                        let mut conf = tc.min(ec);
1483                        if let Some((_, cc)) = c {
1484                            conf = conf.min(cc);
1485                        }
1486                        Some((rty, conf))
1487                    }
1488                    _ => None,
1489                }
1490            }
1491            Node::BinOp { op, lhs, rhs } => {
1492                let l = self.walk_expr(&lhs, scope, sigs, out, effects, failures)?;
1493                let r = self.walk_expr(&rhs, scope, sigs, out, effects, failures)?;
1494                match (l, r) {
1495                    (Some((lt, lc)), Some((rt, rc))) => {
1496                        // Weakest-input propagation (the decided rule, now with
1497                        // a real site): a derived value's confidence is the
1498                        // minimum of its inputs'.
1499                        let conf = lc.min(rc);
1500                        if op.is_logical() {
1501                            for (operand, ty) in [(&lhs, &lt), (&rhs, &rt)] {
1502                                if *ty != Type::Bool && *ty != Type::Never {
1503                                    out.push(Violation {
1504                                        principle: 2,
1505                                        node: operand.clone(),
1506                                        detail: format!(
1507                                            "logical operand is {ty:?}, expected Bool"
1508                                        ),
1509                                    });
1510                                }
1511                            }
1512                            Some((Type::Bool, conf))
1513                        } else if op.is_comparison() {
1514                            if !compatible(&lt, &rt) {
1515                                out.push(Violation {
1516                                    principle: 2,
1517                                    node: hash.clone(),
1518                                    detail: format!(
1519                                        "comparison operands differ: {lt:?} vs {rt:?}"
1520                                    ),
1521                                });
1522                            }
1523                            Some((Type::Bool, conf))
1524                        } else {
1525                            if lt != Type::Number && lt != Type::Never {
1526                                out.push(Violation {
1527                                    principle: 2,
1528                                    node: lhs.clone(),
1529                                    detail: format!(
1530                                        "arithmetic operand is {lt:?}, expected Number"
1531                                    ),
1532                                });
1533                            }
1534                            if rt != Type::Number && rt != Type::Never {
1535                                out.push(Violation {
1536                                    principle: 2,
1537                                    node: rhs.clone(),
1538                                    detail: format!(
1539                                        "arithmetic operand is {rt:?}, expected Number"
1540                                    ),
1541                                });
1542                            }
1543                            Some((Type::Number, conf))
1544                        }
1545                    }
1546                    _ => None,
1547                }
1548            }
1549            Node::Call { func, args } => {
1550                let Some(sig) = sigs.get(&func) else {
1551                    out.push(Violation {
1552                        principle: 1,
1553                        node: hash.clone(),
1554                        detail: format!("unresolved function: `{func}`"),
1555                    });
1556                    for arg in &args {
1557                        self.walk_expr(arg, scope, sigs, out, effects, failures)?;
1558                    }
1559                    return Ok(None);
1560                };
1561                if args.len() != sig.params.len() {
1562                    out.push(Violation {
1563                        principle: 2,
1564                        node: hash.clone(),
1565                        detail: format!(
1566                            "`{func}` takes {} argument(s), {} given",
1567                            sig.params.len(),
1568                            args.len()
1569                        ),
1570                    });
1571                }
1572                // Unify each parameter type (which may mention a type
1573                // variable) against the actual argument type, building a
1574                // substitution. For a monomorphic function this is just
1575                // structural equality.
1576                let mut subst: HashMap<String, Type> = HashMap::new();
1577                for (i, arg) in args.iter().enumerate() {
1578                    let inferred =
1579                        self.walk_expr(arg, scope, sigs, out, effects, failures)?;
1580                    if let (Some((at, ac)), Some(p)) = (inferred, sig.params.get(i)) {
1581                        if !unify(&p.ty, &at, &mut subst) {
1582                            out.push(Violation {
1583                                principle: 2,
1584                                node: arg.clone(),
1585                                detail: format!(
1586                                    "argument `{}` is {:?} but `{func}` expects {:?}",
1587                                    p.name, at, p.ty
1588                                ),
1589                            });
1590                        }
1591                        if ac < p.min_confidence {
1592                            out.push(Violation {
1593                                principle: 7,
1594                                node: arg.clone(),
1595                                detail: format!(
1596                                    "argument `{}` is {:?} but `{func}` requires at least {:?}",
1597                                    p.name, ac, p.min_confidence
1598                                ),
1599                            });
1600                        }
1601                    }
1602                }
1603                effects.extend(sig.requires.iter().copied());
1604                failures.extend(sig.on_failure.iter().cloned());
1605                let result_ty = substitute(&sig.produces.ty, &subst);
1606                // A type parameter occurring in the result must also occur
1607                // in some parameter, else no argument can pin it (e.g.
1608                // `make<T>() -> T`). A still-`Var` result *after*
1609                // unification is fine — it may be bound to the caller's
1610                // own opaque type variable, as in generic recursion
1611                // (`fold` calling `fold_at`).
1612                for tp in &sig.type_params {
1613                    if ty_mentions(&sig.produces.ty, tp)
1614                        && !sig.params.iter().any(|p| ty_mentions(&p.ty, tp))
1615                    {
1616                        out.push(Violation {
1617                            principle: 2,
1618                            node: hash.clone(),
1619                            detail: format!(
1620                                "type parameter `{tp}` of `{func}` appears only \
1621                                 in the result and cannot be inferred"
1622                            ),
1623                        });
1624                    }
1625                }
1626                Some((result_ty, sig.produces.confidence))
1627            }
1628            Node::FuncRef(name) => {
1629                let Some(sig) = sigs.get(&name) else {
1630                    out.push(Violation {
1631                        principle: 1,
1632                        node: hash.clone(),
1633                        detail: format!("unresolved function: `{name}`"),
1634                    });
1635                    return Ok(None);
1636                };
1637                // K1 boundary (documented): a function *value* lowers to one
1638                // `call_indirect` of a fixed type, so it cannot be generic
1639                // (no instantiation site) nor fallible (its `[tag,val]`
1640                // return would leak as the value). Both are honest
1641                // restrictions, not fakes — closures/lambdas come next.
1642                if !sig.type_params.is_empty() {
1643                    out.push(Violation {
1644                        principle: 2,
1645                        node: hash.clone(),
1646                        detail: format!(
1647                            "cannot take a function value of generic `{name}` (v0.4)"
1648                        ),
1649                    });
1650                }
1651                if !sig.on_failure.is_empty() {
1652                    out.push(Violation {
1653                        principle: 2,
1654                        node: hash.clone(),
1655                        detail: format!(
1656                            "cannot take a function value of fallible `{name}` (v0.4)"
1657                        ),
1658                    });
1659                }
1660                let fn_ty = Type::Fn {
1661                    params: sig.params.iter().map(|p| p.ty.clone()).collect(),
1662                    ret: Box::new(sig.produces.ty.clone()),
1663                    effects: sig.requires.clone(),
1664                };
1665                Some((fn_ty, Confidence::Structural))
1666            }
1667            Node::CallValue { callee, args } => {
1668                let c =
1669                    self.walk_expr(&callee, scope, sigs, out, effects, failures)?;
1670                let (cty, cconf) = match c {
1671                    Some(v) => v,
1672                    None => {
1673                        for arg in &args {
1674                            self.walk_expr(
1675                                arg, scope, sigs, out, effects, failures,
1676                            )?;
1677                        }
1678                        return Ok(None);
1679                    }
1680                };
1681                let Type::Fn {
1682                    params,
1683                    ret,
1684                    effects: fx,
1685                } = cty
1686                else {
1687                    if cty != Type::Never {
1688                        out.push(Violation {
1689                            principle: 2,
1690                            node: callee.clone(),
1691                            detail: format!(
1692                                "callee is {cty:?}, not a function value"
1693                            ),
1694                        });
1695                    }
1696                    for arg in &args {
1697                        self.walk_expr(
1698                            arg, scope, sigs, out, effects, failures,
1699                        )?;
1700                    }
1701                    return Ok(None);
1702                };
1703                if args.len() != params.len() {
1704                    out.push(Violation {
1705                        principle: 2,
1706                        node: hash.clone(),
1707                        detail: format!(
1708                            "function value takes {} argument(s), {} given",
1709                            params.len(),
1710                            args.len()
1711                        ),
1712                    });
1713                }
1714                // Weakest-input propagation across callee and arguments.
1715                let mut conf = cconf;
1716                for (i, arg) in args.iter().enumerate() {
1717                    let inferred =
1718                        self.walk_expr(arg, scope, sigs, out, effects, failures)?;
1719                    if let (Some((at, ac)), Some(pt)) = (inferred, params.get(i)) {
1720                        if !compatible(pt, &at) {
1721                            out.push(Violation {
1722                                principle: 2,
1723                                node: arg.clone(),
1724                                detail: format!(
1725                                    "argument {i} is {at:?} but the function \
1726                                     value expects {pt:?}"
1727                                ),
1728                            });
1729                        }
1730                        conf = conf.min(ac);
1731                    }
1732                }
1733                effects.extend(fx.iter().copied());
1734                Some((*ret, conf))
1735            }
1736            Node::Lambda { params, body } => {
1737                // The body sees the enclosing scope plus the lambda's own
1738                // params. Effects and failures are isolated: making a
1739                // closure performs nothing — its effects belong to the
1740                // `Fn` and fire at the call site (`CallValue` unions them).
1741                let mut s2 = scope.to_vec();
1742                for p in &params {
1743                    s2.push((
1744                        p.name.clone(),
1745                        Some((p.ty.clone(), p.min_confidence)),
1746                    ));
1747                }
1748                let mut lam_fx = BTreeSet::new();
1749                let mut lam_fail = BTreeSet::new();
1750                let bt = self.walk_expr(
1751                    &body, &s2, sigs, out, &mut lam_fx, &mut lam_fail,
1752                )?;
1753                if !lam_fail.is_empty() {
1754                    out.push(Violation {
1755                        principle: 6,
1756                        node: hash.clone(),
1757                        detail: format!(
1758                            "a lambda may not raise an uncaught failure: {} (v0.4)",
1759                            lam_fail.iter().cloned().collect::<Vec<_>>().join(", ")
1760                        ),
1761                    });
1762                }
1763                let ret = bt.map(|(t, _)| t).unwrap_or(Type::Never);
1764                let fn_ty = Type::Fn {
1765                    params: params.iter().map(|p| p.ty.clone()).collect(),
1766                    ret: Box::new(ret),
1767                    effects: lam_fx,
1768                };
1769                Some((fn_ty, Confidence::Structural))
1770            }
1771            Node::Record { type_name, fields } => {
1772                match self.records.get(&type_name).cloned() {
1773                    None => {
1774                        out.push(Violation {
1775                            principle: 1,
1776                            node: hash.clone(),
1777                            detail: format!("unknown record type: `{type_name}`"),
1778                        });
1779                        for (_, fh) in &fields {
1780                            self.walk_expr(fh, scope, sigs, out, effects, failures)?;
1781                        }
1782                        None
1783                    }
1784                    Some(def_fields) => {
1785                        let mut conf = Confidence::Persisted;
1786                        for (fname, fty) in &def_fields {
1787                            match fields.iter().find(|(n, _)| n == fname) {
1788                                None => out.push(Violation {
1789                                    principle: 2,
1790                                    node: hash.clone(),
1791                                    detail: format!(
1792                                        "missing field `{fname}` for `{type_name}`"
1793                                    ),
1794                                }),
1795                                Some((_, fh)) => {
1796                                    if let Some((vt, vc)) = self.walk_expr(
1797                                        fh, scope, sigs, out, effects, failures,
1798                                    )? {
1799                                        if !compatible(&vt, fty) {
1800                                            out.push(Violation {
1801                                                principle: 2,
1802                                                node: fh.clone(),
1803                                                detail: format!(
1804                                                    "field `{fname}` is {vt:?}, expected {fty:?}"
1805                                                ),
1806                                            });
1807                                        }
1808                                        conf = conf.min(vc);
1809                                    }
1810                                }
1811                            }
1812                        }
1813                        for (n, fh) in &fields {
1814                            if !def_fields.iter().any(|(dn, _)| dn == n) {
1815                                out.push(Violation {
1816                                    principle: 2,
1817                                    node: fh.clone(),
1818                                    detail: format!("`{type_name}` has no field `{n}`"),
1819                                });
1820                                self.walk_expr(fh, scope, sigs, out, effects, failures)?;
1821                            }
1822                        }
1823                        if def_fields.is_empty() {
1824                            conf = Confidence::Structural;
1825                        }
1826                        Some((Type::Named(type_name.clone()), conf))
1827                    }
1828                }
1829            }
1830            Node::Field {
1831                base,
1832                type_name,
1833                field,
1834            } => {
1835                match self.walk_expr(&base, scope, sigs, out, effects, failures)? {
1836                    Some((Type::Named(rec), bc)) => {
1837                        if rec != type_name {
1838                            out.push(Violation {
1839                                principle: 2,
1840                                node: hash.clone(),
1841                                detail: format!(
1842                                    "field access typed as `{type_name}` but base is `{rec}`"
1843                                ),
1844                            });
1845                        }
1846                        match self.records.get(&rec) {
1847                        Some(fs) => match fs.iter().find(|(n, _)| n == &field) {
1848                            Some((_, fty)) => Some((fty.clone(), bc)),
1849                            None => {
1850                                out.push(Violation {
1851                                    principle: 2,
1852                                    node: hash.clone(),
1853                                    detail: format!("`{rec}` has no field `{field}`"),
1854                                });
1855                                None
1856                            }
1857                        },
1858                        None => {
1859                            out.push(Violation {
1860                                principle: 1,
1861                                node: hash.clone(),
1862                                detail: format!("unknown record type: `{rec}`"),
1863                            });
1864                            None
1865                        }
1866                        }
1867                    }
1868                    Some((other, _)) => {
1869                        out.push(Violation {
1870                            principle: 2,
1871                            node: base.clone(),
1872                            detail: format!(
1873                                "field access on non-record type {other:?}"
1874                            ),
1875                        });
1876                        None
1877                    }
1878                    None => None,
1879                }
1880            }
1881            Node::Variant {
1882                type_name,
1883                case,
1884                fields,
1885            } => match self.variants.get(&type_name).cloned() {
1886                None => {
1887                    out.push(Violation {
1888                        principle: 1,
1889                        node: hash.clone(),
1890                        detail: format!("unknown variant type: `{type_name}`"),
1891                    });
1892                    for (_, fh) in &fields {
1893                        self.walk_expr(fh, scope, sigs, out, effects, failures)?;
1894                    }
1895                    None
1896                }
1897                Some(cases) => match cases.iter().find(|(c, _)| c == &case) {
1898                    None => {
1899                        out.push(Violation {
1900                            principle: 2,
1901                            node: hash.clone(),
1902                            detail: format!("`{type_name}` has no case `{case}`"),
1903                        });
1904                        for (_, fh) in &fields {
1905                            self.walk_expr(fh, scope, sigs, out, effects, failures)?;
1906                        }
1907                        None
1908                    }
1909                    Some((_, payload)) => {
1910                        let payload = payload.clone();
1911                        let mut conf = Confidence::Persisted;
1912                        for (fname, fty) in &payload {
1913                            match fields.iter().find(|(n, _)| n == fname) {
1914                                None => out.push(Violation {
1915                                    principle: 2,
1916                                    node: hash.clone(),
1917                                    detail: format!(
1918                                        "missing field `{fname}` for `{type_name}.{case}`"
1919                                    ),
1920                                }),
1921                                Some((_, fh)) => {
1922                                    if let Some((vt, vc)) = self.walk_expr(
1923                                        fh, scope, sigs, out, effects, failures,
1924                                    )? {
1925                                        if !compatible(&vt, fty) {
1926                                            out.push(Violation {
1927                                                principle: 2,
1928                                                node: fh.clone(),
1929                                                detail: format!(
1930                                                    "field `{fname}` is {vt:?}, expected {fty:?}"
1931                                                ),
1932                                            });
1933                                        }
1934                                        conf = conf.min(vc);
1935                                    }
1936                                }
1937                            }
1938                        }
1939                        for (n, fh) in &fields {
1940                            if !payload.iter().any(|(dn, _)| dn == n) {
1941                                out.push(Violation {
1942                                    principle: 2,
1943                                    node: fh.clone(),
1944                                    detail: format!(
1945                                        "`{type_name}.{case}` has no field `{n}`"
1946                                    ),
1947                                });
1948                                self.walk_expr(fh, scope, sigs, out, effects, failures)?;
1949                            }
1950                        }
1951                        if payload.is_empty() {
1952                            conf = Confidence::Structural;
1953                        }
1954                        Some((Type::Named(type_name.clone()), conf))
1955                    }
1956                },
1957            },
1958            Node::Match {
1959                scrutinee,
1960                type_name,
1961                arms,
1962            } => {
1963                match self.walk_expr(&scrutinee, scope, sigs, out, effects, failures)? {
1964                    Some((Type::Named(vname), sconf)) => {
1965                        if vname != type_name {
1966                            out.push(Violation {
1967                                principle: 2,
1968                                node: hash.clone(),
1969                                detail: format!(
1970                                    "match typed as `{type_name}` but scrutinee is `{vname}`"
1971                                ),
1972                            });
1973                        }
1974                        match self.variants.get(&vname).cloned() {
1975                            None => {
1976                                out.push(Violation {
1977                                    principle: 1,
1978                                    node: hash.clone(),
1979                                    detail: format!("unknown variant type: `{vname}`"),
1980                                });
1981                                None
1982                            }
1983                            Some(cases) => {
1984                                for (cname, _) in &cases {
1985                                    if !arms.iter().any(|a| &a.case == cname) {
1986                                        out.push(Violation {
1987                                            principle: 2,
1988                                            node: hash.clone(),
1989                                            detail: format!(
1990                                                "non-exhaustive match: case `{cname}` not covered"
1991                                            ),
1992                                        });
1993                                    }
1994                                }
1995                                let mut result_ty: Option<Type> = None;
1996                                let mut conf = sconf;
1997                                for arm in &arms {
1998                                    let Some((_, payload)) =
1999                                        cases.iter().find(|(c, _)| c == &arm.case)
2000                                    else {
2001                                        out.push(Violation {
2002                                            principle: 2,
2003                                            node: hash.clone(),
2004                                            detail: format!(
2005                                                "`{vname}` has no case `{}`",
2006                                                arm.case
2007                                            ),
2008                                        });
2009                                        continue;
2010                                    };
2011                                    if arm.bindings.len() != payload.len() {
2012                                        out.push(Violation {
2013                                            principle: 2,
2014                                            node: hash.clone(),
2015                                            detail: format!(
2016                                                "case `{}` has {} field(s), {} bound",
2017                                                arm.case,
2018                                                payload.len(),
2019                                                arm.bindings.len()
2020                                            ),
2021                                        });
2022                                    }
2023                                    let mut s2 = scope.to_vec();
2024                                    for (i, b) in arm.bindings.iter().enumerate() {
2025                                        let entry = payload
2026                                            .get(i)
2027                                            .map(|(_, ft)| (ft.clone(), sconf));
2028                                        s2.push((b.clone(), entry));
2029                                    }
2030                                    if let Some((bt, bc)) = self.walk_expr(
2031                                        &arm.body, &s2, sigs, out, effects, failures,
2032                                    )? {
2033                                        match &result_ty {
2034                                            None => result_ty = Some(bt),
2035                                            Some(rt) => {
2036                                                if !compatible(rt, &bt) {
2037                                                    out.push(Violation {
2038                                                        principle: 2,
2039                                                        node: hash.clone(),
2040                                                        detail: format!(
2041                                                            "match arms differ: {rt:?} vs {bt:?}"
2042                                                        ),
2043                                                    });
2044                                                }
2045                                            }
2046                                        }
2047                                        conf = conf.min(bc);
2048                                    }
2049                                }
2050                                result_ty.map(|t| (t, conf))
2051                            }
2052                        }
2053                    }
2054                    Some((other, _)) => {
2055                        out.push(Violation {
2056                            principle: 2,
2057                            node: scrutinee.clone(),
2058                            detail: format!("match on non-variant type {other:?}"),
2059                        });
2060                        None
2061                    }
2062                    None => None,
2063                }
2064            }
2065            Node::Function { .. }
2066            | Node::Module { .. }
2067            | Node::RecordDef { .. }
2068            | Node::VariantDef { .. } => None,
2069        })
2070    }
2071}
2072
2073/// Two types are compatible if equal, or if either is `Never` — an
2074/// expression that diverges can stand wherever a value is expected.
2075fn compatible(a: &Type, b: &Type) -> bool {
2076    a == b || *a == Type::Never || *b == Type::Never
2077}
2078
2079/// Unify a (possibly type-variable-bearing) parameter type against an actual
2080/// type, recording type-variable bindings. Monomorphic types unify only by
2081/// structural equality; `Never` unifies with anything.
2082fn unify(pat: &Type, actual: &Type, subst: &mut HashMap<String, Type>) -> bool {
2083    if *actual == Type::Never {
2084        return true;
2085    }
2086    match pat {
2087        Type::Var(v) => match subst.get(v) {
2088            Some(bound) => compatible(&bound.clone(), actual),
2089            None => {
2090                subst.insert(v.clone(), actual.clone());
2091                true
2092            }
2093        },
2094        Type::List(a) => {
2095            if let Type::List(b) = actual {
2096                unify(a, b, subst)
2097            } else {
2098                false
2099            }
2100        }
2101        Type::Option(a) => {
2102            if let Type::Option(b) = actual {
2103                unify(a, b, subst)
2104            } else {
2105                false
2106            }
2107        }
2108        Type::Cell(a) => {
2109            if let Type::Cell(b) = actual {
2110                unify(a, b, subst)
2111            } else {
2112                false
2113            }
2114        }
2115        Type::Map(ka, va) => {
2116            if let Type::Map(kb, vb) = actual {
2117                unify(ka, kb, subst) && unify(va, vb, subst)
2118            } else {
2119                false
2120            }
2121        }
2122        Type::Result(oa, ea) => {
2123            if let Type::Result(ob, eb) = actual {
2124                unify(oa, ob, subst) && unify(ea, eb, subst)
2125            } else {
2126                false
2127            }
2128        }
2129        Type::Fn {
2130            params: pa,
2131            ret: ra,
2132            ..
2133        } => {
2134            if let Type::Fn {
2135                params: pb,
2136                ret: rb,
2137                ..
2138            } = actual
2139            {
2140                pa.len() == pb.len()
2141                    && pa.iter().zip(pb).all(|(x, y)| unify(x, y, subst))
2142                    && unify(ra, rb, subst)
2143            } else {
2144                false
2145            }
2146        }
2147        _ => pat == actual,
2148    }
2149}
2150
2151/// Apply a type-variable substitution.
2152fn substitute(t: &Type, subst: &HashMap<String, Type>) -> Type {
2153    match t {
2154        Type::Var(v) => subst.get(v).cloned().unwrap_or_else(|| t.clone()),
2155        Type::List(a) => Type::List(Box::new(substitute(a, subst))),
2156        Type::Option(a) => Type::Option(Box::new(substitute(a, subst))),
2157        Type::Cell(a) => Type::Cell(Box::new(substitute(a, subst))),
2158        Type::Map(k, v) => Type::Map(
2159            Box::new(substitute(k, subst)),
2160            Box::new(substitute(v, subst)),
2161        ),
2162        Type::Result(o, e) => Type::Result(
2163            Box::new(substitute(o, subst)),
2164            Box::new(substitute(e, subst)),
2165        ),
2166        Type::Fn {
2167            params,
2168            ret,
2169            effects,
2170        } => Type::Fn {
2171            params: params.iter().map(|p| substitute(p, subst)).collect(),
2172            ret: Box::new(substitute(ret, subst)),
2173            effects: effects.clone(),
2174        },
2175        _ => t.clone(),
2176    }
2177}
2178
2179/// Whether type `t` mentions the named type variable.
2180fn ty_mentions(t: &Type, name: &str) -> bool {
2181    match t {
2182        Type::Var(v) => v == name,
2183        Type::List(a) | Type::Option(a) | Type::Cell(a) => ty_mentions(a, name),
2184        Type::Map(k, v) | Type::Result(k, v) => {
2185            ty_mentions(k, name) || ty_mentions(v, name)
2186        }
2187        Type::Fn { params, ret, .. } => {
2188            params.iter().any(|p| ty_mentions(p, name)) || ty_mentions(ret, name)
2189        }
2190        _ => false,
2191    }
2192}
2193
2194/// The child hashes a node references, in order.
2195pub(crate) fn child_hashes(node: &Node) -> Vec<&NodeHash> {
2196    match node {
2197        Node::Lit(_)
2198        | Node::FloatLit(_)
2199        | Node::DecimalLit(_)
2200        | Node::Bool(_)
2201        | Node::Str(_)
2202        | Node::Now
2203        | Node::Rand
2204        | Node::Ref(_)
2205        | Node::FuncRef(_)
2206        | Node::Hole { .. }
2207        | Node::Fail(_)
2208        | Node::RecordDef { .. }
2209        | Node::VariantDef { .. } => Vec::new(),
2210        Node::StrLen(arg)
2211        | Node::StrLower(arg)
2212        | Node::StrFromCode(arg)
2213        | Node::IntToFloat(arg)
2214        | Node::FloatToInt(arg)
2215        | Node::IntToDecimal(arg)
2216        | Node::DecimalToInt(arg)
2217        | Node::DecimalRaw(arg)
2218        | Node::Not(arg)
2219        | Node::NumberToStr(arg)
2220        | Node::StrToNumber(arg)
2221        | Node::StrToNumberOpt(arg) => vec![arg],
2222        Node::StrConcat(a, b) | Node::StrEq(a, b) => vec![a, b],
2223        Node::StrSlice { s, start, len } => vec![s, start, len],
2224        Node::StrContains { haystack, needle } => vec![haystack, needle],
2225        Node::StrStartsWith { s, prefix } => vec![s, prefix],
2226        Node::StrIndexOf { haystack, needle } => vec![haystack, needle],
2227        Node::List(elems) => elems.iter().collect(),
2228        Node::ListEmpty { .. } => Vec::new(),
2229        Node::ListCons { head, tail } => vec![head, tail],
2230        Node::OptionSome(v) => vec![v],
2231        Node::OptionNone { .. } => Vec::new(),
2232        Node::OptionElse { opt, default } => vec![opt, default],
2233        Node::OptionMatch {
2234            opt,
2235            some_body,
2236            none_body,
2237            ..
2238        } => vec![opt, some_body, none_body],
2239        Node::ListTryGet { list, index } => vec![list, index],
2240        Node::ListLen(arg) => vec![arg],
2241        Node::ListGet { list, index } => vec![list, index],
2242        Node::Map(pairs) => {
2243            let mut v = Vec::with_capacity(pairs.len() * 2);
2244            for (k, val) in pairs {
2245                v.push(k);
2246                v.push(val);
2247            }
2248            v
2249        }
2250        Node::MapGet { map, key } => vec![map, key],
2251        Node::MapTryGet { map, key } => vec![map, key],
2252        Node::MapLen(arg) => vec![arg],
2253        Node::Log(arg) | Node::Publish(arg) => vec![arg],
2254        Node::SetHeader { name, value } => vec![name, value],
2255        Node::MutNew(v) => vec![v],
2256        Node::MutGet(cell) => vec![cell],
2257        Node::MutSet { cell, value } => vec![cell, value],
2258        Node::DiskWrite { path, content } => vec![path, content],
2259        Node::DiskRead(path) => vec![path],
2260        Node::NetGet(url) => vec![url],
2261        Node::DbQuery { sql, params } => vec![sql, params],
2262        Node::Record { fields, .. } => fields.iter().map(|(_, h)| h).collect(),
2263        Node::Variant { fields, .. } => fields.iter().map(|(_, h)| h).collect(),
2264        Node::Field { base, .. } => vec![base],
2265        Node::Match {
2266            scrutinee, arms, ..
2267        } => {
2268            let mut v = vec![scrutinee];
2269            v.extend(arms.iter().map(|a| &a.body));
2270            v
2271        }
2272        Node::Handle { body, handlers } => {
2273            let mut v = vec![body];
2274            v.extend(handlers.iter().map(|(_, h)| h));
2275            v
2276        }
2277        Node::Call { args, .. } => args.iter().collect(),
2278        Node::CallValue { callee, args } => {
2279            let mut v = vec![callee];
2280            v.extend(args.iter());
2281            v
2282        }
2283        Node::Lambda { body, .. } => vec![body],
2284        Node::Step { value, .. } => vec![value],
2285        Node::BinOp { lhs, rhs, .. }
2286        | Node::FloatOp { lhs, rhs, .. }
2287        | Node::DecimalOp { lhs, rhs, .. } => vec![lhs, rhs],
2288        Node::If {
2289            cond,
2290            then_branch,
2291            else_branch,
2292        } => vec![cond, then_branch, else_branch],
2293        Node::Function { body, result, .. } => {
2294            let mut v: Vec<&NodeHash> = body.iter().collect();
2295            v.push(result);
2296            v
2297        }
2298        Node::Module {
2299            types, functions, ..
2300        } => {
2301            let mut v: Vec<&NodeHash> = types.iter().collect();
2302            v.extend(functions.iter());
2303            v
2304        }
2305    }
2306}
2307
2308/// The structural inverse of [`child_hashes`]: rebuild `node` with its
2309/// child hashes replaced by `kids`, in the **exact order**
2310/// `child_hashes` yields them, preserving every non-hash field (ops,
2311/// names, types, case labels, bindings). `kids.len()` must equal
2312/// `child_hashes(node).len()` — this is the substitution primitive
2313/// `replace_node`/`fill_hole` build on; the invariant
2314/// `child_hashes(&with_child_hashes(n, child_hashes(n))) == child_hashes(n)`
2315/// is asserted by `with_child_hashes_round_trips`.
2316pub(crate) fn with_child_hashes(node: &Node, kids: &[NodeHash]) -> Node {
2317    let k = |i: usize| kids[i].clone();
2318    match node {
2319        // Leaves — no children; identity.
2320        Node::Lit(_)
2321        | Node::FloatLit(_)
2322        | Node::DecimalLit(_)
2323        | Node::Bool(_)
2324        | Node::Str(_)
2325        | Node::Now
2326        | Node::Rand
2327        | Node::Ref(_)
2328        | Node::FuncRef(_)
2329        | Node::Hole { .. }
2330        | Node::Fail(_)
2331        | Node::RecordDef { .. }
2332        | Node::VariantDef { .. }
2333        | Node::ListEmpty { .. }
2334        | Node::OptionNone { .. } => node.clone(),
2335
2336        Node::StrLen(_) => Node::StrLen(k(0)),
2337        Node::StrLower(_) => Node::StrLower(k(0)),
2338        Node::StrFromCode(_) => Node::StrFromCode(k(0)),
2339        Node::IntToFloat(_) => Node::IntToFloat(k(0)),
2340        Node::FloatToInt(_) => Node::FloatToInt(k(0)),
2341        Node::IntToDecimal(_) => Node::IntToDecimal(k(0)),
2342        Node::DecimalToInt(_) => Node::DecimalToInt(k(0)),
2343        Node::DecimalRaw(_) => Node::DecimalRaw(k(0)),
2344        Node::Not(_) => Node::Not(k(0)),
2345        Node::NumberToStr(_) => Node::NumberToStr(k(0)),
2346        Node::StrToNumber(_) => Node::StrToNumber(k(0)),
2347        Node::StrToNumberOpt(_) => Node::StrToNumberOpt(k(0)),
2348        Node::ListLen(_) => Node::ListLen(k(0)),
2349        Node::MapLen(_) => Node::MapLen(k(0)),
2350        Node::OptionSome(_) => Node::OptionSome(k(0)),
2351        Node::Log(_) => Node::Log(k(0)),
2352        Node::Publish(_) => Node::Publish(k(0)),
2353        Node::SetHeader { .. } => Node::SetHeader {
2354            name: k(0),
2355            value: k(1),
2356        },
2357        Node::MutNew(_) => Node::MutNew(k(0)),
2358        Node::MutGet(_) => Node::MutGet(k(0)),
2359        Node::DiskRead(_) => Node::DiskRead(k(0)),
2360        Node::NetGet(_) => Node::NetGet(k(0)),
2361
2362        Node::StrConcat(_, _) => Node::StrConcat(k(0), k(1)),
2363        Node::StrEq(_, _) => Node::StrEq(k(0), k(1)),
2364        Node::StrContains { .. } => Node::StrContains {
2365            haystack: k(0),
2366            needle: k(1),
2367        },
2368        Node::StrStartsWith { .. } => Node::StrStartsWith {
2369            s: k(0),
2370            prefix: k(1),
2371        },
2372        Node::StrIndexOf { .. } => Node::StrIndexOf {
2373            haystack: k(0),
2374            needle: k(1),
2375        },
2376        Node::StrSlice { .. } => Node::StrSlice {
2377            s: k(0),
2378            start: k(1),
2379            len: k(2),
2380        },
2381        Node::ListCons { .. } => Node::ListCons {
2382            head: k(0),
2383            tail: k(1),
2384        },
2385        Node::OptionElse { .. } => Node::OptionElse {
2386            opt: k(0),
2387            default: k(1),
2388        },
2389        Node::OptionMatch {
2390            some_bind,
2391            ..
2392        } => Node::OptionMatch {
2393            opt: k(0),
2394            some_bind: some_bind.clone(),
2395            some_body: k(1),
2396            none_body: k(2),
2397        },
2398        Node::ListTryGet { .. } => Node::ListTryGet {
2399            list: k(0),
2400            index: k(1),
2401        },
2402        Node::ListGet { .. } => Node::ListGet {
2403            list: k(0),
2404            index: k(1),
2405        },
2406        Node::MapGet { .. } => Node::MapGet {
2407            map: k(0),
2408            key: k(1),
2409        },
2410        Node::MapTryGet { .. } => Node::MapTryGet {
2411            map: k(0),
2412            key: k(1),
2413        },
2414        Node::MutSet { .. } => Node::MutSet {
2415            cell: k(0),
2416            value: k(1),
2417        },
2418        Node::DiskWrite { .. } => Node::DiskWrite {
2419            path: k(0),
2420            content: k(1),
2421        },
2422        Node::DbQuery { .. } => Node::DbQuery {
2423            sql: k(0),
2424            params: k(1),
2425        },
2426        Node::BinOp { op, .. } => Node::BinOp {
2427            op: *op,
2428            lhs: k(0),
2429            rhs: k(1),
2430        },
2431        Node::FloatOp { op, .. } => Node::FloatOp {
2432            op: *op,
2433            lhs: k(0),
2434            rhs: k(1),
2435        },
2436        Node::DecimalOp { op, .. } => Node::DecimalOp {
2437            op: *op,
2438            lhs: k(0),
2439            rhs: k(1),
2440        },
2441        Node::If { .. } => Node::If {
2442            cond: k(0),
2443            then_branch: k(1),
2444            else_branch: k(2),
2445        },
2446        Node::List(elems) => {
2447            Node::List((0..elems.len()).map(k).collect())
2448        }
2449        Node::Map(pairs) => Node::Map(
2450            (0..pairs.len()).map(|i| (k(2 * i), k(2 * i + 1))).collect(),
2451        ),
2452        Node::Record {
2453            type_name, fields, ..
2454        } => Node::Record {
2455            type_name: type_name.clone(),
2456            fields: fields
2457                .iter()
2458                .enumerate()
2459                .map(|(i, (n, _))| (n.clone(), k(i)))
2460                .collect(),
2461        },
2462        Node::Variant {
2463            type_name,
2464            case,
2465            fields,
2466        } => Node::Variant {
2467            type_name: type_name.clone(),
2468            case: case.clone(),
2469            fields: fields
2470                .iter()
2471                .enumerate()
2472                .map(|(i, (n, _))| (n.clone(), k(i)))
2473                .collect(),
2474        },
2475        Node::Field {
2476            type_name, field, ..
2477        } => Node::Field {
2478            base: k(0),
2479            type_name: type_name.clone(),
2480            field: field.clone(),
2481        },
2482        Node::Match {
2483            type_name, arms, ..
2484        } => Node::Match {
2485            scrutinee: k(0),
2486            type_name: type_name.clone(),
2487            arms: arms
2488                .iter()
2489                .enumerate()
2490                .map(|(i, a)| MatchArm {
2491                    body: k(i + 1),
2492                    ..a.clone()
2493                })
2494                .collect(),
2495        },
2496        Node::Handle { handlers, .. } => Node::Handle {
2497            body: k(0),
2498            handlers: handlers
2499                .iter()
2500                .enumerate()
2501                .map(|(i, (n, _))| (n.clone(), k(i + 1)))
2502                .collect(),
2503        },
2504        Node::Call { func, args } => Node::Call {
2505            func: func.clone(),
2506            args: (0..args.len()).map(k).collect(),
2507        },
2508        Node::CallValue { args, .. } => Node::CallValue {
2509            callee: k(0),
2510            args: (0..args.len()).map(|i| k(i + 1)).collect(),
2511        },
2512        Node::Lambda { params, .. } => Node::Lambda {
2513            params: params.clone(),
2514            body: k(0),
2515        },
2516        Node::Step { binding, .. } => Node::Step {
2517            binding: binding.clone(),
2518            value: k(0),
2519        },
2520        Node::Function {
2521            name,
2522            type_params,
2523            params,
2524            produces,
2525            requires,
2526            on_failure,
2527            body,
2528            ..
2529        } => Node::Function {
2530            name: name.clone(),
2531            type_params: type_params.clone(),
2532            params: params.clone(),
2533            produces: produces.clone(),
2534            requires: requires.clone(),
2535            on_failure: on_failure.clone(),
2536            body: (0..body.len()).map(k).collect(),
2537            result: k(body.len()),
2538        },
2539        Node::Module {
2540            name,
2541            types,
2542            functions,
2543        } => Node::Module {
2544            name: name.clone(),
2545            types: (0..types.len()).map(k).collect(),
2546            functions: (0..functions.len())
2547                .map(|i| k(types.len() + i))
2548                .collect(),
2549        },
2550    }
2551}
2552
2553#[cfg(test)]
2554mod tests {
2555    use super::*;
2556    use crate::node::{BinOp, MatchArm, Param};
2557
2558    fn p(name: &str, ty: Type, c: Confidence) -> Param {
2559        Param {
2560            name: name.into(),
2561            ty,
2562            min_confidence: c,
2563        }
2564    }
2565
2566    fn produces(ty: Type, confidence: Confidence) -> Produces {
2567        Produces { ty, confidence }
2568    }
2569
2570    #[allow(clippy::too_many_arguments)]
2571    fn function(
2572        store: &Store,
2573        name: &str,
2574        params: Vec<Param>,
2575        prod: Produces,
2576        requires: BTreeSet<Effect>,
2577        on_failure: Vec<&str>,
2578        body: Vec<NodeHash>,
2579        result: NodeHash,
2580    ) -> NodeHash {
2581        store
2582            .put(&Node::Function {
2583                name: name.into(),
2584                type_params: vec![],
2585                params,
2586                produces: prod,
2587                requires,
2588                on_failure: on_failure.into_iter().map(String::from).collect(),
2589                body,
2590                result,
2591            })
2592            .unwrap()
2593    }
2594
2595    /// `add(a: Number@External, b: Number@External) -> Number@Structural`,
2596    /// pure, no failures — a callee used by other tests. Its result is a
2597    /// structural literal, so it honors its own `produces`.
2598    fn add_fn(s: &Store) -> NodeHash {
2599        let sum = s.put(&Node::Lit(0)).unwrap();
2600        function(
2601            s,
2602            "add",
2603            vec![
2604                p("a", Type::Number, Confidence::External),
2605                p("b", Type::Number, Confidence::External),
2606            ],
2607            produces(Type::Number, Confidence::Structural),
2608            BTreeSet::new(),
2609            vec![],
2610            vec![],
2611            sum,
2612        )
2613    }
2614
2615    #[test]
2616    fn a_well_typed_module_is_clean_and_complete() {
2617        let s = Store::open_in_memory().unwrap();
2618        let add = add_fn(&s);
2619        let n = s.put(&Node::Ref("n".into())).unwrap();
2620        let call = s
2621            .put(&Node::Call {
2622                func: "add".into(),
2623                args: vec![n.clone(), n],
2624            })
2625            .unwrap();
2626        let step = s
2627            .put(&Node::Step {
2628                binding: "d".into(),
2629                value: call,
2630            })
2631            .unwrap();
2632        let res = s.put(&Node::Ref("d".into())).unwrap();
2633        let double = function(
2634            &s,
2635            "double",
2636            vec![p("n", Type::Number, Confidence::External)],
2637            produces(Type::Number, Confidence::Structural),
2638            BTreeSet::new(),
2639            vec![],
2640            vec![step],
2641            res,
2642        );
2643        let m = s
2644            .put(&Node::Module {
2645                name: "m".into(),
2646                types: vec![],
2647                functions: vec![add, double],
2648            })
2649            .unwrap();
2650
2651        let r = Checker::new(&s).check(&m).unwrap();
2652        assert!(r.ok(), "unexpected: {:?}", r.violations);
2653        assert_eq!(r.status, Status::Complete);
2654        assert_eq!(r.failures, Failures::Exhaustive);
2655    }
2656
2657    #[test]
2658    fn the_boolean_layer_type_checks() {
2659        let s = Store::open_in_memory().unwrap();
2660        // `!(true) || (1 < 2)` : Bool — the literal, the unary, and a
2661        // logical operator over a comparison.
2662        let t = s.put(&Node::Bool(true)).unwrap();
2663        let nott = s.put(&Node::Not(t.clone())).unwrap();
2664        let one = s.put(&Node::Lit(1)).unwrap();
2665        let two = s.put(&Node::Lit(2)).unwrap();
2666        let lt = s
2667            .put(&Node::BinOp {
2668                op: BinOp::Lt,
2669                lhs: one.clone(),
2670                rhs: two,
2671            })
2672            .unwrap();
2673        let or = s
2674            .put(&Node::BinOp {
2675                op: BinOp::Or,
2676                lhs: nott,
2677                rhs: lt,
2678            })
2679            .unwrap();
2680        let ok = function(
2681            &s,
2682            "ok",
2683            vec![],
2684            produces(Type::Bool, Confidence::Structural),
2685            BTreeSet::new(),
2686            vec![],
2687            vec![],
2688            or,
2689        );
2690        let r = Checker::new(&s).check(&ok).unwrap();
2691        assert!(r.ok(), "unexpected: {:?}", r.violations);
2692
2693        // `!5` — a Number operand to `!` is a Principle 2 violation.
2694        let five = s.put(&Node::Lit(5)).unwrap();
2695        let badnot = s.put(&Node::Not(five)).unwrap();
2696        let bf = function(
2697            &s,
2698            "bf",
2699            vec![],
2700            produces(Type::Bool, Confidence::Structural),
2701            BTreeSet::new(),
2702            vec![],
2703            vec![],
2704            badnot,
2705        );
2706        let r2 = Checker::new(&s).check(&bf).unwrap();
2707        assert!(!r2.ok());
2708        assert!(r2
2709            .violations
2710            .iter()
2711            .any(|v| v.principle == 2 && v.detail.contains("expected Bool")));
2712
2713        // `1 && true` — a Number operand to `&&` is a Principle 2 violation.
2714        let andbad = s
2715            .put(&Node::BinOp {
2716                op: BinOp::And,
2717                lhs: one,
2718                rhs: t,
2719            })
2720            .unwrap();
2721        let ab = function(
2722            &s,
2723            "ab",
2724            vec![],
2725            produces(Type::Bool, Confidence::Structural),
2726            BTreeSet::new(),
2727            vec![],
2728            vec![],
2729            andbad,
2730        );
2731        let r3 = Checker::new(&s).check(&ab).unwrap();
2732        assert!(!r3.ok());
2733        assert!(r3
2734            .violations
2735            .iter()
2736            .any(|v| v.principle == 2 && v.detail.contains("logical operand")));
2737    }
2738
2739    #[test]
2740    fn function_values_type_check() {
2741        let s = Store::open_in_memory().unwrap();
2742        let fn_num_num = Type::Fn {
2743            params: vec![Type::Number],
2744            ret: Box::new(Type::Number),
2745            effects: BTreeSet::new(),
2746        };
2747        // double(n) -> Number ; apply(f: Fn(Number)->Number, x) -> Number
2748        // is `f(x)` ; main = apply(&double, 21).
2749        let dbl = function(
2750            &s,
2751            "double",
2752            vec![p("n", Type::Number, Confidence::Structural)],
2753            produces(Type::Number, Confidence::Structural),
2754            BTreeSet::new(),
2755            vec![],
2756            vec![],
2757            s.put(&Node::Ref("n".into())).unwrap(),
2758        );
2759        let call_f = s
2760            .put(&Node::CallValue {
2761                callee: s.put(&Node::Ref("f".into())).unwrap(),
2762                args: vec![s.put(&Node::Ref("x".into())).unwrap()],
2763            })
2764            .unwrap();
2765        let apply = function(
2766            &s,
2767            "apply",
2768            vec![
2769                p("f", fn_num_num.clone(), Confidence::Structural),
2770                p("x", Type::Number, Confidence::Structural),
2771            ],
2772            produces(Type::Number, Confidence::Structural),
2773            BTreeSet::new(),
2774            vec![],
2775            vec![],
2776            call_f,
2777        );
2778        let main = function(
2779            &s,
2780            "main",
2781            vec![],
2782            produces(Type::Number, Confidence::Structural),
2783            BTreeSet::new(),
2784            vec![],
2785            vec![],
2786            s.put(&Node::Call {
2787                func: "apply".into(),
2788                args: vec![
2789                    s.put(&Node::FuncRef("double".into())).unwrap(),
2790                    s.put(&Node::Lit(21)).unwrap(),
2791                ],
2792            })
2793            .unwrap(),
2794        );
2795        let m = s
2796            .put(&Node::Module {
2797                name: "m".into(),
2798                types: vec![],
2799                functions: vec![dbl, apply, main],
2800            })
2801            .unwrap();
2802        let r = Checker::new(&s).check(&m).unwrap();
2803        assert!(r.ok(), "unexpected: {:?}", r.violations);
2804
2805        // `&identity` — a function value of a generic function is rejected.
2806        let id = identity_fn(&s);
2807        let badgen = function(
2808            &s,
2809            "bg",
2810            vec![],
2811            produces(fn_num_num.clone(), Confidence::Structural),
2812            BTreeSet::new(),
2813            vec![],
2814            vec![],
2815            s.put(&Node::FuncRef("identity".into())).unwrap(),
2816        );
2817        let mg = s
2818            .put(&Node::Module {
2819                name: "mg".into(),
2820                types: vec![],
2821                functions: vec![id, badgen],
2822            })
2823            .unwrap();
2824        let rg = Checker::new(&s).check(&mg).unwrap();
2825        assert!(rg
2826            .violations
2827            .iter()
2828            .any(|v| v.principle == 2 && v.detail.contains("generic")));
2829
2830        // `&boom` — a function value of a fallible function is rejected.
2831        let boom = function(
2832            &s,
2833            "boom",
2834            vec![],
2835            produces(Type::Number, Confidence::Structural),
2836            BTreeSet::new(),
2837            vec!["Boom"],
2838            vec![],
2839            s.put(&Node::Fail("Boom".into())).unwrap(),
2840        );
2841        let badfal = function(
2842            &s,
2843            "bf2",
2844            vec![],
2845            produces(fn_num_num, Confidence::Structural),
2846            BTreeSet::new(),
2847            vec![],
2848            vec![],
2849            s.put(&Node::FuncRef("boom".into())).unwrap(),
2850        );
2851        let mf = s
2852            .put(&Node::Module {
2853                name: "mf".into(),
2854                types: vec![],
2855                functions: vec![boom, badfal],
2856            })
2857            .unwrap();
2858        let rf = Checker::new(&s).check(&mf).unwrap();
2859        assert!(rf
2860            .violations
2861            .iter()
2862            .any(|v| v.principle == 2 && v.detail.contains("fallible")));
2863
2864        // Calling a non-function value is a Principle 2 violation.
2865        let badcall = function(
2866            &s,
2867            "bc",
2868            vec![],
2869            produces(Type::Number, Confidence::Structural),
2870            BTreeSet::new(),
2871            vec![],
2872            vec![],
2873            s.put(&Node::CallValue {
2874                callee: s.put(&Node::Lit(5)).unwrap(),
2875                args: vec![],
2876            })
2877            .unwrap(),
2878        );
2879        let rc = Checker::new(&s).check(&badcall).unwrap();
2880        assert!(rc
2881            .violations
2882            .iter()
2883            .any(|v| v.principle == 2 && v.detail.contains("not a function value")));
2884    }
2885
2886    #[test]
2887    fn closures_type_check_and_isolate_effects() {
2888        let s = Store::open_in_memory().unwrap();
2889        let lam_param = |name: &str| Param {
2890            name: name.into(),
2891            ty: Type::Number,
2892            min_confidence: Confidence::External,
2893        };
2894        // mk(k) -> Fn(Number)->Number is `|x| x + k` (captures k).
2895        let body = s
2896            .put(&Node::BinOp {
2897                op: BinOp::Add,
2898                lhs: s.put(&Node::Ref("x".into())).unwrap(),
2899                rhs: s.put(&Node::Ref("k".into())).unwrap(),
2900            })
2901            .unwrap();
2902        let lam = s
2903            .put(&Node::Lambda {
2904                params: vec![lam_param("x")],
2905                body,
2906            })
2907            .unwrap();
2908        let fn_num_num = Type::Fn {
2909            params: vec![Type::Number],
2910            ret: Box::new(Type::Number),
2911            effects: BTreeSet::new(),
2912        };
2913        let mk = function(
2914            &s,
2915            "mk",
2916            vec![p("k", Type::Number, Confidence::Structural)],
2917            produces(fn_num_num.clone(), Confidence::Structural),
2918            BTreeSet::new(),
2919            vec![],
2920            vec![],
2921            lam,
2922        );
2923        let r = Checker::new(&s)
2924            .check(
2925                &s.put(&Node::Module {
2926                    name: "m".into(),
2927                    types: vec![],
2928                    functions: vec![mk],
2929                })
2930                .unwrap(),
2931            )
2932            .unwrap();
2933        assert!(r.ok(), "unexpected: {:?}", r.violations);
2934
2935        // Effect isolation: `mklog = |x| log(x)` returns a closure whose
2936        // Fn type carries Log. *Creating* it performs nothing — mklog
2937        // needs no `requires` — but the returned type honestly declares
2938        // the effect. A function that *calls* it must declare Log (P5).
2939        let fn_log = Type::Fn {
2940            params: vec![Type::Number],
2941            ret: Box::new(Type::Number),
2942            effects: [Effect::Log].into_iter().collect(),
2943        };
2944        let log_body = s
2945            .put(&Node::Log(s.put(&Node::Ref("x".into())).unwrap()))
2946            .unwrap();
2947        let log_lam = s
2948            .put(&Node::Lambda {
2949                params: vec![lam_param("x")],
2950                body: log_body,
2951            })
2952            .unwrap();
2953        let mklog = function(
2954            &s,
2955            "mklog",
2956            vec![],
2957            produces(fn_log.clone(), Confidence::Structural),
2958            BTreeSet::new(), // no requires — creating a closure is pure
2959            vec![],
2960            vec![],
2961            log_lam,
2962        );
2963        let r2 = Checker::new(&s)
2964            .check(
2965                &s.put(&Node::Module {
2966                    name: "m2".into(),
2967                    types: vec![],
2968                    functions: vec![mklog],
2969                })
2970                .unwrap(),
2971            )
2972            .unwrap();
2973        assert!(
2974            r2.ok(),
2975            "creating an effectful closure must be pure: {:?}",
2976            r2.violations
2977        );
2978
2979        // Calling a Log-effecting function value without declaring Log
2980        // is a Principle 5 violation.
2981        let call_f = s
2982            .put(&Node::CallValue {
2983                callee: s.put(&Node::Ref("f".into())).unwrap(),
2984                args: vec![s.put(&Node::Lit(0)).unwrap()],
2985            })
2986            .unwrap();
2987        let runner = function(
2988            &s,
2989            "runner",
2990            vec![p("f", fn_log, Confidence::Structural)],
2991            produces(Type::Number, Confidence::Structural),
2992            BTreeSet::new(), // missing Log
2993            vec![],
2994            vec![],
2995            call_f,
2996        );
2997        let r3 = Checker::new(&s)
2998            .check(
2999                &s.put(&Node::Module {
3000                    name: "m3".into(),
3001                    types: vec![],
3002                    functions: vec![runner],
3003                })
3004                .unwrap(),
3005            )
3006            .unwrap();
3007        assert!(r3
3008            .violations
3009            .iter()
3010            .any(|v| v.principle == 5 && v.detail.contains("Log")));
3011
3012        // A lambda may not raise an uncaught failure (v0.4 boundary).
3013        let fail_lam = s
3014            .put(&Node::Lambda {
3015                params: vec![lam_param("x")],
3016                body: s.put(&Node::Fail("Boom".into())).unwrap(),
3017            })
3018            .unwrap();
3019        let mkfail = function(
3020            &s,
3021            "mkfail",
3022            vec![],
3023            produces(fn_num_num, Confidence::Structural),
3024            BTreeSet::new(),
3025            vec![],
3026            vec![],
3027            fail_lam,
3028        );
3029        let r4 = Checker::new(&s)
3030            .check(
3031                &s.put(&Node::Module {
3032                    name: "m4".into(),
3033                    types: vec![],
3034                    functions: vec![mkfail],
3035                })
3036                .unwrap(),
3037            )
3038            .unwrap();
3039        assert!(r4
3040            .violations
3041            .iter()
3042            .any(|v| v.principle == 6 && v.detail.contains("uncaught failure")));
3043    }
3044
3045    #[test]
3046    fn option_match_type_checks() {
3047        let s = Store::open_in_memory().unwrap();
3048        // f(o: Option<Number>) -> Number = match o { Some(v) -> v+1, None -> 0 }
3049        let good = s
3050            .put(&Node::OptionMatch {
3051                opt: s.put(&Node::Ref("o".into())).unwrap(),
3052                some_bind: "v".into(),
3053                some_body: s
3054                    .put(&Node::BinOp {
3055                        op: BinOp::Add,
3056                        lhs: s.put(&Node::Ref("v".into())).unwrap(),
3057                        rhs: s.put(&Node::Lit(1)).unwrap(),
3058                    })
3059                    .unwrap(),
3060                none_body: s.put(&Node::Lit(0)).unwrap(),
3061            })
3062            .unwrap();
3063        let f = function(
3064            &s,
3065            "f",
3066            vec![p(
3067                "o",
3068                Type::Option(Box::new(Type::Number)),
3069                Confidence::Structural,
3070            )],
3071            produces(Type::Number, Confidence::Structural),
3072            BTreeSet::new(),
3073            vec![],
3074            vec![],
3075            good,
3076        );
3077        let r = Checker::new(&s).check(&f).unwrap();
3078        assert!(r.ok(), "unexpected: {:?}", r.violations);
3079
3080        // Scrutinee not an Option -> Principle 2.
3081        let bad_scrut = s
3082            .put(&Node::OptionMatch {
3083                opt: s.put(&Node::Lit(5)).unwrap(),
3084                some_bind: "v".into(),
3085                some_body: s.put(&Node::Ref("v".into())).unwrap(),
3086                none_body: s.put(&Node::Lit(0)).unwrap(),
3087            })
3088            .unwrap();
3089        let bf = function(
3090            &s,
3091            "bf",
3092            vec![],
3093            produces(Type::Number, Confidence::Structural),
3094            BTreeSet::new(),
3095            vec![],
3096            vec![],
3097            bad_scrut,
3098        );
3099        let r2 = Checker::new(&s).check(&bf).unwrap();
3100        assert!(r2
3101            .violations
3102            .iter()
3103            .any(|v| v.principle == 2 && v.detail.contains("expected Option")));
3104
3105        // Arms of different types -> Principle 2.
3106        let bad_arms = s
3107            .put(&Node::OptionMatch {
3108                opt: s.put(&Node::Ref("o".into())).unwrap(),
3109                some_bind: "v".into(),
3110                some_body: s.put(&Node::Str("x".into())).unwrap(),
3111                none_body: s.put(&Node::Lit(0)).unwrap(),
3112            })
3113            .unwrap();
3114        let ba = function(
3115            &s,
3116            "ba",
3117            vec![p(
3118                "o",
3119                Type::Option(Box::new(Type::Number)),
3120                Confidence::Structural,
3121            )],
3122            produces(Type::String, Confidence::Structural),
3123            BTreeSet::new(),
3124            vec![],
3125            vec![],
3126            bad_arms,
3127        );
3128        let r3 = Checker::new(&s).check(&ba).unwrap();
3129        assert!(r3
3130            .violations
3131            .iter()
3132            .any(|v| v.principle == 2 && v.detail.contains("arms differ")));
3133    }
3134
3135    #[test]
3136    fn float_type_checks() {
3137        let s = Store::open_in_memory().unwrap();
3138        let f = |v: f64| s.put(&Node::FloatLit(v.to_bits())).unwrap();
3139
3140        // f() -> Float = 1.5 * 2.0
3141        let good = function(
3142            &s,
3143            "g",
3144            vec![],
3145            produces(Type::Float, Confidence::Structural),
3146            BTreeSet::new(),
3147            vec![],
3148            vec![],
3149            s.put(&Node::FloatOp {
3150                op: BinOp::Mul,
3151                lhs: f(1.5),
3152                rhs: f(2.0),
3153            })
3154            .unwrap(),
3155        );
3156        let r = Checker::new(&s).check(&good).unwrap();
3157        assert!(r.ok(), "unexpected: {:?}", r.violations);
3158
3159        // Float op with a Number operand -> Principle 2.
3160        let mixed = function(
3161            &s,
3162            "mx",
3163            vec![],
3164            produces(Type::Float, Confidence::Structural),
3165            BTreeSet::new(),
3166            vec![],
3167            vec![],
3168            s.put(&Node::FloatOp {
3169                op: BinOp::Add,
3170                lhs: f(1.0),
3171                rhs: s.put(&Node::Lit(2)).unwrap(),
3172            })
3173            .unwrap(),
3174        );
3175        let r2 = Checker::new(&s).check(&mixed).unwrap();
3176        assert!(r2
3177            .violations
3178            .iter()
3179            .any(|v| v.principle == 2 && v.detail.contains("expected Float")));
3180
3181        // `%` is not defined on Float -> Principle 2.
3182        let modf = function(
3183            &s,
3184            "mf",
3185            vec![],
3186            produces(Type::Float, Confidence::Structural),
3187            BTreeSet::new(),
3188            vec![],
3189            vec![],
3190            s.put(&Node::FloatOp {
3191                op: BinOp::Mod,
3192                lhs: f(5.0),
3193                rhs: f(2.0),
3194            })
3195            .unwrap(),
3196        );
3197        let r3 = Checker::new(&s).check(&modf).unwrap();
3198        assert!(r3
3199            .violations
3200            .iter()
3201            .any(|v| v.principle == 2 && v.detail.contains("not defined on Float")));
3202    }
3203
3204    #[test]
3205    fn decimal_type_checks() {
3206        let s = Store::open_in_memory().unwrap();
3207        let d = |v: i64| s.put(&Node::DecimalLit(v)).unwrap();
3208
3209        let good = function(
3210            &s,
3211            "g",
3212            vec![],
3213            produces(Type::Decimal, Confidence::Structural),
3214            BTreeSet::new(),
3215            vec![],
3216            vec![],
3217            s.put(&Node::DecimalOp {
3218                op: BinOp::Mul,
3219                lhs: d(12500),
3220                rhs: d(40000),
3221            })
3222            .unwrap(),
3223        );
3224        assert!(Checker::new(&s).check(&good).unwrap().ok());
3225
3226        // Number operand to a Decimal op -> Principle 2.
3227        let mixed = function(
3228            &s,
3229            "mx",
3230            vec![],
3231            produces(Type::Decimal, Confidence::Structural),
3232            BTreeSet::new(),
3233            vec![],
3234            vec![],
3235            s.put(&Node::DecimalOp {
3236                op: BinOp::Add,
3237                lhs: d(10000),
3238                rhs: s.put(&Node::Lit(2)).unwrap(),
3239            })
3240            .unwrap(),
3241        );
3242        let r2 = Checker::new(&s).check(&mixed).unwrap();
3243        assert!(r2
3244            .violations
3245            .iter()
3246            .any(|v| v.principle == 2 && v.detail.contains("expected Decimal")));
3247
3248        // `%` is not defined on Decimal -> Principle 2.
3249        let modd = function(
3250            &s,
3251            "md",
3252            vec![],
3253            produces(Type::Decimal, Confidence::Structural),
3254            BTreeSet::new(),
3255            vec![],
3256            vec![],
3257            s.put(&Node::DecimalOp {
3258                op: BinOp::Mod,
3259                lhs: d(50000),
3260                rhs: d(20000),
3261            })
3262            .unwrap(),
3263        );
3264        let r3 = Checker::new(&s).check(&modd).unwrap();
3265        assert!(r3
3266            .violations
3267            .iter()
3268            .any(|v| v.principle == 2 && v.detail.contains("not defined on Decimal")));
3269    }
3270
3271    #[test]
3272    fn publish_requires_the_live_effect() {
3273        let s = Store::open_in_memory().unwrap();
3274        let body = || s.put(&Node::Publish(
3275            s.put(&Node::Str("items".into())).unwrap(),
3276        )).unwrap();
3277        // requires Live → clean.
3278        let ok = function(
3279            &s,
3280            "notify",
3281            vec![],
3282            produces(Type::Number, Confidence::Structural),
3283            [Effect::Live].into_iter().collect(),
3284            vec![],
3285            vec![],
3286            body(),
3287        );
3288        assert!(Checker::new(&s).check(&ok).unwrap().ok());
3289        // missing requires Live → Principle 5.
3290        let bad = function(
3291            &s,
3292            "notify2",
3293            vec![],
3294            produces(Type::Number, Confidence::Structural),
3295            BTreeSet::new(),
3296            vec![],
3297            vec![],
3298            body(),
3299        );
3300        let r = Checker::new(&s).check(&bad).unwrap();
3301        assert!(
3302            r.violations
3303                .iter()
3304                .any(|v| v.principle == 5 && v.detail.contains("Live")),
3305            "publish without `requires Live` must be P5: {:?}",
3306            r.violations
3307        );
3308    }
3309
3310    #[test]
3311    fn numeric_and_closure_soundness() {
3312        // Adversarial: every program here MUST be rejected. Number,
3313        // Decimal, and Float-bits are all i64 at runtime, so a missed
3314        // rejection is silent corruption, not a trap — the worst failure
3315        // for a trust-the-checker language. A failing assertion here is a
3316        // real soundness hole, not a test bug.
3317        let s = Store::open_in_memory().unwrap();
3318        let flt = |v: f64| s.put(&Node::FloatLit(v.to_bits())).unwrap();
3319        let dec = |v: i64| s.put(&Node::DecimalLit(v)).unwrap();
3320        let lit = |v: i64| s.put(&Node::Lit(v)).unwrap();
3321        let rf = |n: &str| s.put(&Node::Ref(n.into())).unwrap();
3322        let rejects = |f: &NodeHash, why: &str| {
3323            let r = Checker::new(&s).check(f).unwrap();
3324            assert!(
3325                !r.ok() && r.violations.iter().any(|v| v.principle == 2),
3326                "UNSOUND: checker accepted `{why}` — {:?}",
3327                r.violations
3328            );
3329        };
3330        let module = |fns: Vec<NodeHash>| {
3331            s.put(&Node::Module {
3332                name: "m".into(),
3333                types: vec![],
3334                functions: fns,
3335            })
3336            .unwrap()
3337        };
3338        let pnum = |n: &str| p(n, Type::Number, Confidence::Structural);
3339
3340        // 1. Float returned where Number is declared.
3341        rejects(
3342            &function(
3343                &s,
3344                "r1",
3345                vec![],
3346                produces(Type::Number, Confidence::Structural),
3347                BTreeSet::new(),
3348                vec![],
3349                vec![],
3350                flt(1.0),
3351            ),
3352            "Float as Number result",
3353        );
3354        // 2. Number arithmetic with a Float operand.
3355        rejects(
3356            &function(
3357                &s,
3358                "r2",
3359                vec![],
3360                produces(Type::Number, Confidence::Structural),
3361                BTreeSet::new(),
3362                vec![],
3363                vec![],
3364                s.put(&Node::BinOp {
3365                    op: BinOp::Add,
3366                    lhs: lit(1),
3367                    rhs: flt(2.0),
3368                })
3369                .unwrap(),
3370            ),
3371            "Number + Float",
3372        );
3373        // 3. Float op with a Decimal operand (both i64 — the silent one).
3374        rejects(
3375            &function(
3376                &s,
3377                "r3",
3378                vec![],
3379                produces(Type::Float, Confidence::Structural),
3380                BTreeSet::new(),
3381                vec![],
3382                vec![],
3383                s.put(&Node::FloatOp {
3384                    op: BinOp::Add,
3385                    lhs: flt(1.0),
3386                    rhs: dec(20000),
3387                })
3388                .unwrap(),
3389            ),
3390            "Float + Decimal",
3391        );
3392        // 4. Float passed to a Number parameter across a call.
3393        let idn = function(
3394            &s,
3395            "idn",
3396            vec![pnum("n")],
3397            produces(Type::Number, Confidence::Structural),
3398            BTreeSet::new(),
3399            vec![],
3400            vec![],
3401            rf("n"),
3402        );
3403        let c4 = function(
3404            &s,
3405            "c4",
3406            vec![],
3407            produces(Type::Number, Confidence::Structural),
3408            BTreeSet::new(),
3409            vec![],
3410            vec![],
3411            s.put(&Node::Call {
3412                func: "idn".into(),
3413                args: vec![flt(3.0)],
3414            })
3415            .unwrap(),
3416        );
3417        rejects(&module(vec![idn, c4]), "Float arg to Number param");
3418        // 5. CallValue arity mismatch (closure of 1 param, 2 args).
3419        rejects(
3420            &function(
3421                &s,
3422                "r5",
3423                vec![],
3424                produces(Type::Number, Confidence::Structural),
3425                BTreeSet::new(),
3426                vec![],
3427                vec![],
3428                s.put(&Node::CallValue {
3429                    callee: s
3430                        .put(&Node::Lambda {
3431                            params: vec![pnum("x")],
3432                            body: rf("x"),
3433                        })
3434                        .unwrap(),
3435                    args: vec![lit(1), lit(2)],
3436                })
3437                .unwrap(),
3438            ),
3439            "closure arity mismatch",
3440        );
3441        // 6. CallValue arg-type mismatch (Float into Fn(Number)).
3442        rejects(
3443            &function(
3444                &s,
3445                "r6",
3446                vec![],
3447                produces(Type::Number, Confidence::Structural),
3448                BTreeSet::new(),
3449                vec![],
3450                vec![],
3451                s.put(&Node::CallValue {
3452                    callee: s
3453                        .put(&Node::Lambda {
3454                            params: vec![pnum("x")],
3455                            body: rf("x"),
3456                        })
3457                        .unwrap(),
3458                    args: vec![flt(1.0)],
3459                })
3460                .unwrap(),
3461            ),
3462            "Float into Fn(Number)",
3463        );
3464        // 7. Lambda return type vs. expected Fn return (unify Fn ret).
3465        let apply = function(
3466            &s,
3467            "apply",
3468            vec![p(
3469                "f",
3470                Type::Fn {
3471                    params: vec![Type::Number],
3472                    ret: Box::new(Type::Number),
3473                    effects: BTreeSet::new(),
3474                },
3475                Confidence::Structural,
3476            )],
3477            produces(Type::Number, Confidence::Structural),
3478            BTreeSet::new(),
3479            vec![],
3480            vec![],
3481            s.put(&Node::CallValue {
3482                callee: rf("f"),
3483                args: vec![lit(1)],
3484            })
3485            .unwrap(),
3486        );
3487        let c7 = function(
3488            &s,
3489            "c7",
3490            vec![],
3491            produces(Type::Number, Confidence::Structural),
3492            BTreeSet::new(),
3493            vec![],
3494            vec![],
3495            s.put(&Node::Call {
3496                func: "apply".into(),
3497                args: vec![s
3498                    .put(&Node::Lambda {
3499                        params: vec![pnum("x")],
3500                        body: s.put(&Node::Str("nope".into())).unwrap(),
3501                    })
3502                    .unwrap()],
3503            })
3504            .unwrap(),
3505        );
3506        rejects(
3507            &module(vec![apply, c7]),
3508            "Fn(Number)->String where Fn(Number)->Number expected",
3509        );
3510        // 8. Type parameter that occurs only in the result (uninferable).
3511        let phantom = s
3512            .put(&Node::Function {
3513                name: "phantom".into(),
3514                type_params: vec!["T".into()],
3515                params: vec![pnum("x")],
3516                produces: produces(
3517                    Type::Option(Box::new(Type::Var("T".into()))),
3518                    Confidence::Structural,
3519                ),
3520                requires: BTreeSet::new(),
3521                on_failure: vec![],
3522                body: vec![],
3523                result: s
3524                    .put(&Node::OptionNone {
3525                        elem: Type::Var("T".into()),
3526                    })
3527                    .unwrap(),
3528            })
3529            .unwrap();
3530        let usep = function(
3531            &s,
3532            "usep",
3533            vec![],
3534            produces(
3535                Type::Option(Box::new(Type::Number)),
3536                Confidence::Structural,
3537            ),
3538            BTreeSet::new(),
3539            vec![],
3540            vec![],
3541            s.put(&Node::Call {
3542                func: "phantom".into(),
3543                args: vec![lit(0)],
3544            })
3545            .unwrap(),
3546        );
3547        rejects(
3548            &module(vec![phantom, usep]),
3549            "type param only in result (uninferable)",
3550        );
3551        // 9. OptionMatch arms of different numeric types.
3552        rejects(
3553            &function(
3554                &s,
3555                "r9",
3556                vec![p(
3557                    "o",
3558                    Type::Option(Box::new(Type::Number)),
3559                    Confidence::Structural,
3560                )],
3561                produces(Type::Float, Confidence::Structural),
3562                BTreeSet::new(),
3563                vec![],
3564                vec![],
3565                s.put(&Node::OptionMatch {
3566                    opt: rf("o"),
3567                    some_bind: "v".into(),
3568                    some_body: flt(1.0),
3569                    none_body: lit(0),
3570                })
3571                .unwrap(),
3572            ),
3573            "OptionMatch Float/Number arms",
3574        );
3575        // 10. IntToFloat applied to a String.
3576        rejects(
3577            &function(
3578                &s,
3579                "r10",
3580                vec![],
3581                produces(Type::Float, Confidence::Structural),
3582                BTreeSet::new(),
3583                vec![],
3584                vec![],
3585                s.put(&Node::IntToFloat(
3586                    s.put(&Node::Str("x".into())).unwrap(),
3587                ))
3588                .unwrap(),
3589            ),
3590            "to_float(String)",
3591        );
3592    }
3593
3594    #[test]
3595    fn effect_failure_confidence_soundness() {
3596        // The other three core guarantees, adversarially. A hole here is
3597        // a "pure" function that does I/O, an uncaught failure that
3598        // escapes typing, or unvalidated data reaching a validated sink —
3599        // each a direct thesis violation, and each silent.
3600        let s = Store::open_in_memory().unwrap();
3601        let lit = |v: i64| s.put(&Node::Lit(v)).unwrap();
3602        let rf = |n: &str| s.put(&Node::Ref(n.into())).unwrap();
3603        let module = |fns: Vec<NodeHash>| {
3604            s.put(&Node::Module {
3605                name: "m".into(),
3606                types: vec![],
3607                functions: fns,
3608            })
3609            .unwrap()
3610        };
3611        let rej = |f: &NodeHash, principle: u8, why: &str| {
3612            let r = Checker::new(&s).check(f).unwrap();
3613            assert!(
3614                !r.ok() && r.violations.iter().any(|v| v.principle == principle),
3615                "UNSOUND: accepted `{why}` (expected P{principle}) — {:?}",
3616                r.violations
3617            );
3618        };
3619        let truth = || {
3620            s.put(&Node::BinOp {
3621                op: BinOp::Eq,
3622                lhs: lit(0),
3623                rhs: lit(0),
3624            })
3625            .unwrap()
3626        };
3627
3628        // P5 — a Log effect, undeclared.
3629        rej(
3630            &function(
3631                &s,
3632                "e1",
3633                vec![],
3634                produces(Type::Number, Confidence::Structural),
3635                BTreeSet::new(),
3636                vec![],
3637                vec![],
3638                s.put(&Node::Log(lit(1))).unwrap(),
3639            ),
3640            5,
3641            "Log with requires {}",
3642        );
3643        // P5 — effect performed inside an If branch must still surface.
3644        rej(
3645            &function(
3646                &s,
3647                "e2",
3648                vec![],
3649                produces(Type::Number, Confidence::Structural),
3650                BTreeSet::new(),
3651                vec![],
3652                vec![],
3653                s.put(&Node::If {
3654                    cond: truth(),
3655                    then_branch: s.put(&Node::Log(lit(1))).unwrap(),
3656                    else_branch: lit(0),
3657                })
3658                .unwrap(),
3659            ),
3660            5,
3661            "Log inside an If branch with requires {}",
3662        );
3663        // P5 — effect propagates across a call (callee Time, caller {}).
3664        let timed = function(
3665            &s,
3666            "timed",
3667            vec![],
3668            produces(Type::Number, Confidence::Structural),
3669            BTreeSet::from([Effect::Time]),
3670            vec![],
3671            vec![],
3672            s.put(&Node::Now).unwrap(),
3673        );
3674        let caller5 = function(
3675            &s,
3676            "caller5",
3677            vec![],
3678            produces(Type::Number, Confidence::Structural),
3679            BTreeSet::new(),
3680            vec![],
3681            vec![],
3682            s.put(&Node::Call {
3683                func: "timed".into(),
3684                args: vec![],
3685            })
3686            .unwrap(),
3687        );
3688        rej(
3689            &module(vec![timed, caller5]),
3690            5,
3691            "calling a Time fn without declaring Time",
3692        );
3693        // P6 — Fail inside an If branch, uncovered.
3694        rej(
3695            &function(
3696                &s,
3697                "f1",
3698                vec![],
3699                produces(Type::Number, Confidence::Structural),
3700                BTreeSet::new(),
3701                vec![],
3702                vec![],
3703                s.put(&Node::If {
3704                    cond: truth(),
3705                    then_branch: s.put(&Node::Fail("Boom".into())).unwrap(),
3706                    else_branch: lit(0),
3707                })
3708                .unwrap(),
3709            ),
3710            6,
3711            "Fail in a branch, on_failure []",
3712        );
3713        // P6 — a partial Handle leaves a different failure uncaught.
3714        rej(
3715            &function(
3716                &s,
3717                "f2",
3718                vec![],
3719                produces(Type::Number, Confidence::Structural),
3720                BTreeSet::new(),
3721                vec![],
3722                vec![],
3723                s.put(&Node::Handle {
3724                    body: s.put(&Node::Fail("Y".into())).unwrap(),
3725                    handlers: vec![("X".into(), lit(0))],
3726                })
3727                .unwrap(),
3728            ),
3729            6,
3730            "Handle X but body fails Y",
3731        );
3732        // P6 — failure propagates across a call.
3733        let raiser = function(
3734            &s,
3735            "raiser",
3736            vec![],
3737            produces(Type::Number, Confidence::Structural),
3738            BTreeSet::new(),
3739            vec!["Boom"],
3740            vec![],
3741            s.put(&Node::Fail("Boom".into())).unwrap(),
3742        );
3743        let caller6 = function(
3744            &s,
3745            "caller6",
3746            vec![],
3747            produces(Type::Number, Confidence::Structural),
3748            BTreeSet::new(),
3749            vec![],
3750            vec![],
3751            s.put(&Node::Call {
3752                func: "raiser".into(),
3753                args: vec![],
3754            })
3755            .unwrap(),
3756        );
3757        rej(
3758            &module(vec![raiser, caller6]),
3759            6,
3760            "calling a fallible fn without covering its failure",
3761        );
3762        // P7 — External value into a Validated-min parameter.
3763        let sink = function(
3764            &s,
3765            "sink",
3766            vec![p("x", Type::Number, Confidence::Validated)],
3767            produces(Type::Number, Confidence::Structural),
3768            BTreeSet::new(),
3769            vec![],
3770            vec![],
3771            rf("x"),
3772        );
3773        let weak = function(
3774            &s,
3775            "weak",
3776            vec![p("e", Type::Number, Confidence::External)],
3777            produces(Type::Number, Confidence::Structural),
3778            BTreeSet::new(),
3779            vec![],
3780            vec![],
3781            s.put(&Node::Call {
3782                func: "sink".into(),
3783                args: vec![rf("e")],
3784            })
3785            .unwrap(),
3786        );
3787        rej(
3788            &module(vec![sink, weak]),
3789            7,
3790            "External arg into a Validated param",
3791        );
3792        // P7 — weakest-input propagation: External + 1 is still External.
3793        let sink2 = function(
3794            &s,
3795            "sink2",
3796            vec![p("x", Type::Number, Confidence::Validated)],
3797            produces(Type::Number, Confidence::Structural),
3798            BTreeSet::new(),
3799            vec![],
3800            vec![],
3801            rf("x"),
3802        );
3803        let derived = function(
3804            &s,
3805            "derived",
3806            vec![p("e", Type::Number, Confidence::External)],
3807            produces(Type::Number, Confidence::Structural),
3808            BTreeSet::new(),
3809            vec![],
3810            vec![],
3811            s.put(&Node::Call {
3812                func: "sink2".into(),
3813                args: vec![s
3814                    .put(&Node::BinOp {
3815                        op: BinOp::Add,
3816                        lhs: rf("e"),
3817                        rhs: lit(1),
3818                    })
3819                    .unwrap()],
3820            })
3821            .unwrap(),
3822        );
3823        rej(
3824            &module(vec![sink2, derived]),
3825            7,
3826            "weakest-input: (External + 1) into Validated param",
3827        );
3828    }
3829
3830    #[test]
3831    fn a_type_mismatch_is_a_principle_2_violation() {
3832        let s = Store::open_in_memory().unwrap();
3833        // f() -> String  but result is a Number literal.
3834        let lit = s.put(&Node::Lit(1)).unwrap();
3835        let f = function(
3836            &s,
3837            "f",
3838            vec![],
3839            produces(Type::String, Confidence::Structural),
3840            BTreeSet::new(),
3841            vec![],
3842            vec![],
3843            lit.clone(),
3844        );
3845        let r = Checker::new(&s).check(&f).unwrap();
3846        assert!(!r.ok());
3847        let v = &r.violations[0];
3848        assert_eq!(v.principle, 2);
3849        assert_eq!(v.node, lit);
3850        assert!(v.detail.contains("String"));
3851    }
3852
3853    #[test]
3854    fn passing_weaker_confidence_than_required_is_a_principle_7_violation() {
3855        let s = Store::open_in_memory().unwrap();
3856        // need(x: Number@Validated) and a caller passing an External param.
3857        let xref = s.put(&Node::Ref("x".into())).unwrap();
3858        let need = function(
3859            &s,
3860            "need",
3861            vec![p("x", Type::Number, Confidence::Validated)],
3862            produces(Type::Number, Confidence::Structural),
3863            BTreeSet::new(),
3864            vec![],
3865            vec![],
3866            xref,
3867        );
3868        let raw = s.put(&Node::Ref("raw".into())).unwrap();
3869        let call = s
3870            .put(&Node::Call {
3871                func: "need".into(),
3872                args: vec![raw.clone()],
3873            })
3874            .unwrap();
3875        let caller = function(
3876            &s,
3877            "caller",
3878            vec![p("raw", Type::Number, Confidence::External)],
3879            produces(Type::Number, Confidence::Structural),
3880            BTreeSet::new(),
3881            vec![],
3882            vec![],
3883            call,
3884        );
3885        let m = s
3886            .put(&Node::Module {
3887                name: "m".into(),
3888                types: vec![],
3889                functions: vec![need, caller],
3890            })
3891            .unwrap();
3892        let r = Checker::new(&s).check(&m).unwrap();
3893        assert!(r
3894            .violations
3895            .iter()
3896            .any(|v| v.principle == 7 && v.node == raw));
3897    }
3898
3899    #[test]
3900    fn an_undeclared_effect_is_a_principle_5_violation() {
3901        let s = Store::open_in_memory().unwrap();
3902        // writer requires Db; caller calls it but declares no effects.
3903        let unit = s.put(&Node::Lit(0)).unwrap();
3904        let mut db = BTreeSet::new();
3905        db.insert(Effect::Db);
3906        let writer = function(
3907            &s,
3908            "writer",
3909            vec![],
3910            produces(Type::Number, Confidence::Structural),
3911            db,
3912            vec![],
3913            vec![],
3914            unit,
3915        );
3916        let call = s
3917            .put(&Node::Call {
3918                func: "writer".into(),
3919                args: vec![],
3920            })
3921            .unwrap();
3922        let caller = function(
3923            &s,
3924            "caller",
3925            vec![],
3926            produces(Type::Number, Confidence::Structural),
3927            BTreeSet::new(), // declares no effects, but calls writer (Db)
3928            vec![],
3929            vec![],
3930            call,
3931        );
3932        let m = s
3933            .put(&Node::Module {
3934                name: "m".into(),
3935                types: vec![],
3936                functions: vec![writer, caller],
3937            })
3938            .unwrap();
3939        let r = Checker::new(&s).check(&m).unwrap();
3940        assert!(r.violations.iter().any(|v| v.principle == 5));
3941        assert!(r.effects.contains(&Effect::Db));
3942    }
3943
3944    #[test]
3945    fn an_uncovered_failure_is_a_principle_6_violation() {
3946        let s = Store::open_in_memory().unwrap();
3947        let unit = s.put(&Node::Lit(0)).unwrap();
3948        let risky = function(
3949            &s,
3950            "risky",
3951            vec![],
3952            produces(Type::Number, Confidence::Structural),
3953            BTreeSet::new(),
3954            vec!["Boom"],
3955            vec![],
3956            unit.clone(),
3957        );
3958        let call = s
3959            .put(&Node::Call {
3960                func: "risky".into(),
3961                args: vec![],
3962            })
3963            .unwrap();
3964        let caller = function(
3965            &s,
3966            "caller",
3967            vec![],
3968            produces(Type::Number, Confidence::Structural),
3969            BTreeSet::new(),
3970            vec![], // does not cover `Boom`
3971            vec![],
3972            call,
3973        );
3974        let m = s
3975            .put(&Node::Module {
3976                name: "m".into(),
3977                types: vec![],
3978                functions: vec![risky, caller],
3979            })
3980            .unwrap();
3981        let r = Checker::new(&s).check(&m).unwrap();
3982        assert!(r.violations.iter().any(|v| v.principle == 6));
3983        assert_eq!(r.failures, Failures::Uncovered(vec!["Boom".into()]));
3984    }
3985
3986    #[test]
3987    fn an_unresolved_reference_is_a_principle_1_violation() {
3988        let s = Store::open_in_memory().unwrap();
3989        let ghost = s.put(&Node::Ref("ghost".into())).unwrap();
3990        let f = function(
3991            &s,
3992            "f",
3993            vec![],
3994            produces(Type::Number, Confidence::Structural),
3995            BTreeSet::new(),
3996            vec![],
3997            vec![],
3998            ghost.clone(),
3999        );
4000        let r = Checker::new(&s).check(&f).unwrap();
4001        assert!(r
4002            .violations
4003            .iter()
4004            .any(|v| v.principle == 1 && v.node == ghost));
4005    }
4006
4007    #[test]
4008    fn a_hole_makes_it_incomplete_but_not_invalid() {
4009        let s = Store::open_in_memory().unwrap();
4010        let hole = s
4011            .put(&Node::Hole {
4012                expects: "Number".into(),
4013            })
4014            .unwrap();
4015        let f = function(
4016            &s,
4017            "f",
4018            vec![],
4019            produces(Type::Number, Confidence::Structural),
4020            BTreeSet::new(),
4021            vec![],
4022            vec![hole.clone()],
4023            s.put(&Node::Lit(0)).unwrap(),
4024        );
4025        let r = Checker::new(&s).check(&f).unwrap();
4026        assert_eq!(r.status, Status::Incomplete);
4027        assert_eq!(r.holes, vec![hole]);
4028        assert!(r.ok());
4029    }
4030
4031    #[test]
4032    fn a_missing_child_is_a_principle_4_violation() {
4033        let s = Store::open_in_memory().unwrap();
4034        let dangling = Node::Ref("never".into()).hash();
4035        let step = s
4036            .put(&Node::Step {
4037                binding: "x".into(),
4038                value: dangling.clone(),
4039            })
4040            .unwrap();
4041        let f = function(
4042            &s,
4043            "f",
4044            vec![],
4045            produces(Type::Number, Confidence::Structural),
4046            BTreeSet::new(),
4047            vec![],
4048            vec![step],
4049            s.put(&Node::Lit(0)).unwrap(),
4050        );
4051        let r = Checker::new(&s).check(&f).unwrap();
4052        assert!(r
4053            .violations
4054            .iter()
4055            .any(|v| v.principle == 4 && v.node == dangling));
4056    }
4057
4058    #[test]
4059    fn a_step_binding_is_not_in_scope_for_its_own_value() {
4060        let s = Store::open_in_memory().unwrap();
4061        let aref = s.put(&Node::Ref("a".into())).unwrap();
4062        let step = s
4063            .put(&Node::Step {
4064                binding: "a".into(),
4065                value: aref.clone(),
4066            })
4067            .unwrap();
4068        let f = function(
4069            &s,
4070            "f",
4071            vec![],
4072            produces(Type::Number, Confidence::Structural),
4073            BTreeSet::new(),
4074            vec![],
4075            vec![step],
4076            s.put(&Node::Lit(0)).unwrap(),
4077        );
4078        let r = Checker::new(&s).check(&f).unwrap();
4079        assert!(r
4080            .violations
4081            .iter()
4082            .any(|v| v.principle == 1 && v.node == aref));
4083    }
4084
4085    #[test]
4086    fn unchanged_subtrees_are_not_recomputed() {
4087        let s = Store::open_in_memory().unwrap();
4088        let f = function(
4089            &s,
4090            "f",
4091            vec![],
4092            produces(Type::Number, Confidence::Structural),
4093            BTreeSet::new(),
4094            vec![],
4095            vec![],
4096            s.put(&Node::Lit(1)).unwrap(),
4097        );
4098        let mut c = Checker::new(&s);
4099        c.check(&f).unwrap();
4100        let after_first = c.computed_count();
4101        assert!(after_first > 0);
4102        c.check(&f).unwrap();
4103        assert_eq!(c.computed_count(), after_first);
4104    }
4105
4106    /// `a + b` with `a@Validated, b@External` yields `Number@External`
4107    /// (weakest input). Declaring `produces … @ Validated` is then a P7
4108    /// violation — the decided weakest-input rule, now with a real site.
4109    #[test]
4110    fn arithmetic_confidence_is_the_weakest_input() {
4111        let s = Store::open_in_memory().unwrap();
4112        let a = s.put(&Node::Ref("a".into())).unwrap();
4113        let b = s.put(&Node::Ref("b".into())).unwrap();
4114        let sum = s
4115            .put(&Node::BinOp {
4116                op: BinOp::Add,
4117                lhs: a,
4118                rhs: b,
4119            })
4120            .unwrap();
4121        let f = function(
4122            &s,
4123            "combine",
4124            vec![
4125                p("a", Type::Number, Confidence::Validated),
4126                p("b", Type::Number, Confidence::External),
4127            ],
4128            produces(Type::Number, Confidence::Validated),
4129            BTreeSet::new(),
4130            vec![],
4131            vec![],
4132            sum.clone(),
4133        );
4134        let r = Checker::new(&s).check(&f).unwrap();
4135        assert!(
4136            r.violations
4137                .iter()
4138                .any(|v| v.principle == 7 && v.node == sum),
4139            "expected weakest-input P7, got {:?}",
4140            r.violations
4141        );
4142        assert_eq!(r.confidence, Some(Confidence::External));
4143    }
4144
4145    #[test]
4146    fn a_comparison_yields_bool() {
4147        let s = Store::open_in_memory().unwrap();
4148        let a = s.put(&Node::Ref("a".into())).unwrap();
4149        let b = s.put(&Node::Ref("b".into())).unwrap();
4150        let cmp = s
4151            .put(&Node::BinOp {
4152                op: BinOp::Lt,
4153                lhs: a,
4154                rhs: b,
4155            })
4156            .unwrap();
4157        let f = function(
4158            &s,
4159            "less",
4160            vec![
4161                p("a", Type::Number, Confidence::Structural),
4162                p("b", Type::Number, Confidence::Structural),
4163            ],
4164            produces(Type::Bool, Confidence::Structural),
4165            BTreeSet::new(),
4166            vec![],
4167            vec![],
4168            cmp,
4169        );
4170        let r = Checker::new(&s).check(&f).unwrap();
4171        assert!(r.ok(), "unexpected: {:?}", r.violations);
4172    }
4173
4174    #[test]
4175    fn arithmetic_on_a_bool_is_a_principle_2_violation() {
4176        let s = Store::open_in_memory().unwrap();
4177        let a = s.put(&Node::Ref("a".into())).unwrap();
4178        let b = s.put(&Node::Ref("b".into())).unwrap();
4179        let cmp = s
4180            .put(&Node::BinOp {
4181                op: BinOp::Lt,
4182                lhs: a.clone(),
4183                rhs: b,
4184            })
4185            .unwrap();
4186        // (a < b) + a  — Bool operand in arithmetic.
4187        let bad = s
4188            .put(&Node::BinOp {
4189                op: BinOp::Add,
4190                lhs: cmp,
4191                rhs: a,
4192            })
4193            .unwrap();
4194        let f = function(
4195            &s,
4196            "bad",
4197            vec![
4198                p("a", Type::Number, Confidence::Structural),
4199                p("b", Type::Number, Confidence::Structural),
4200            ],
4201            produces(Type::Number, Confidence::Structural),
4202            BTreeSet::new(),
4203            vec![],
4204            vec![],
4205            bad,
4206        );
4207        let r = Checker::new(&s).check(&f).unwrap();
4208        assert!(r.violations.iter().any(|v| v.principle == 2));
4209    }
4210
4211    #[test]
4212    fn a_non_bool_condition_is_a_principle_2_violation() {
4213        let s = Store::open_in_memory().unwrap();
4214        let one = s.put(&Node::Lit(1)).unwrap();
4215        let zero = s.put(&Node::Lit(0)).unwrap();
4216        let iff = s
4217            .put(&Node::If {
4218                cond: one.clone(), // Number, not Bool
4219                then_branch: one.clone(),
4220                else_branch: zero,
4221            })
4222            .unwrap();
4223        let f = function(
4224            &s,
4225            "f",
4226            vec![],
4227            produces(Type::Number, Confidence::Structural),
4228            BTreeSet::new(),
4229            vec![],
4230            vec![],
4231            iff,
4232        );
4233        let r = Checker::new(&s).check(&f).unwrap();
4234        assert!(r
4235            .violations
4236            .iter()
4237            .any(|v| v.principle == 2 && v.node == one));
4238    }
4239
4240    /// `abs(n) = if n < 0 then 0 - n else n end` — a real conditional that
4241    /// type-checks clean.
4242    #[test]
4243    fn a_well_typed_conditional_is_clean() {
4244        let s = Store::open_in_memory().unwrap();
4245        let n = s.put(&Node::Ref("n".into())).unwrap();
4246        let zero = s.put(&Node::Lit(0)).unwrap();
4247        let cond = s
4248            .put(&Node::BinOp {
4249                op: BinOp::Lt,
4250                lhs: n.clone(),
4251                rhs: zero.clone(),
4252            })
4253            .unwrap();
4254        let neg = s
4255            .put(&Node::BinOp {
4256                op: BinOp::Sub,
4257                lhs: zero,
4258                rhs: n.clone(),
4259            })
4260            .unwrap();
4261        let iff = s
4262            .put(&Node::If {
4263                cond,
4264                then_branch: neg,
4265                else_branch: n,
4266            })
4267            .unwrap();
4268        let f = function(
4269            &s,
4270            "abs",
4271            vec![p("n", Type::Number, Confidence::External)],
4272            produces(Type::Number, Confidence::External),
4273            BTreeSet::new(),
4274            vec![],
4275            vec![],
4276            iff,
4277        );
4278        let r = Checker::new(&s).check(&f).unwrap();
4279        assert!(r.ok(), "unexpected: {:?}", r.violations);
4280        assert_eq!(r.confidence, Some(Confidence::External));
4281    }
4282
4283    #[test]
4284    fn an_uncovered_fail_is_a_principle_6_violation() {
4285        let s = Store::open_in_memory().unwrap();
4286        let boom = s.put(&Node::Fail("Boom".into())).unwrap();
4287        let f = function(
4288            &s,
4289            "f",
4290            vec![],
4291            produces(Type::Number, Confidence::Structural),
4292            BTreeSet::new(),
4293            vec![], // does NOT declare Boom
4294            vec![],
4295            boom,
4296        );
4297        let r = Checker::new(&s).check(&f).unwrap();
4298        assert!(r.violations.iter().any(|v| v.principle == 6));
4299        assert_eq!(r.failures, Failures::Uncovered(vec!["Boom".into()]));
4300    }
4301
4302    /// `safe_div(a, b) on_failure DivByZero =
4303    ///    if b == 0 then fail DivByZero else a / b end`
4304    /// — a real fallible function that type-checks clean. The `fail` branch
4305    /// is `Never` and unifies with the `Number` branch; the failure is
4306    /// covered by `on_failure`.
4307    #[test]
4308    fn a_fallible_function_with_a_covered_failure_is_clean() {
4309        let s = Store::open_in_memory().unwrap();
4310        let a = s.put(&Node::Ref("a".into())).unwrap();
4311        let b = s.put(&Node::Ref("b".into())).unwrap();
4312        let zero = s.put(&Node::Lit(0)).unwrap();
4313        let is_zero = s
4314            .put(&Node::BinOp {
4315                op: BinOp::Eq,
4316                lhs: b.clone(),
4317                rhs: zero,
4318            })
4319            .unwrap();
4320        let boom = s.put(&Node::Fail("DivByZero".into())).unwrap();
4321        let div = s
4322            .put(&Node::BinOp {
4323                op: BinOp::Div,
4324                lhs: a,
4325                rhs: b,
4326            })
4327            .unwrap();
4328        let iff = s
4329            .put(&Node::If {
4330                cond: is_zero,
4331                then_branch: boom,
4332                else_branch: div,
4333            })
4334            .unwrap();
4335        let f = function(
4336            &s,
4337            "safe_div",
4338            vec![
4339                p("a", Type::Number, Confidence::Structural),
4340                p("b", Type::Number, Confidence::Structural),
4341            ],
4342            produces(Type::Number, Confidence::Structural),
4343            BTreeSet::new(),
4344            vec!["DivByZero"],
4345            vec![],
4346            iff,
4347        );
4348        let r = Checker::new(&s).check(&f).unwrap();
4349        assert!(r.ok(), "unexpected: {:?}", r.violations);
4350        assert_eq!(r.failures, Failures::Exhaustive);
4351    }
4352
4353    #[test]
4354    fn handle_catches_a_failure_so_it_need_not_propagate() {
4355        let s = Store::open_in_memory().unwrap();
4356        // risky() on_failure [Boom]
4357        let unit = s.put(&Node::Lit(0)).unwrap();
4358        let risky = function(
4359            &s,
4360            "risky",
4361            vec![],
4362            produces(Type::Number, Confidence::Structural),
4363            BTreeSet::new(),
4364            vec!["Boom"],
4365            vec![],
4366            unit,
4367        );
4368        // caller: step x = risky() on Boom -> 0 ;  result x
4369        let call = s
4370            .put(&Node::Call {
4371                func: "risky".into(),
4372                args: vec![],
4373            })
4374            .unwrap();
4375        let zero = s.put(&Node::Lit(0)).unwrap();
4376        let handle = s
4377            .put(&Node::Handle {
4378                body: call,
4379                handlers: vec![("Boom".into(), zero)],
4380            })
4381            .unwrap();
4382        let step = s
4383            .put(&Node::Step {
4384                binding: "x".into(),
4385                value: handle,
4386            })
4387            .unwrap();
4388        let xref = s.put(&Node::Ref("x".into())).unwrap();
4389        let caller = function(
4390            &s,
4391            "caller",
4392            vec![],
4393            produces(Type::Number, Confidence::Structural),
4394            BTreeSet::new(),
4395            vec![], // declares NO failures — Boom is handled, so this is fine
4396            vec![step],
4397            xref,
4398        );
4399        let m = s
4400            .put(&Node::Module {
4401                name: "m".into(),
4402                types: vec![],
4403                functions: vec![risky, caller],
4404            })
4405            .unwrap();
4406        let r = Checker::new(&s).check(&m).unwrap();
4407        assert!(r.ok(), "unexpected: {:?}", r.violations);
4408        assert_eq!(r.failures, Failures::Exhaustive);
4409    }
4410
4411    fn point_def(s: &Store) -> NodeHash {
4412        s.put(&Node::RecordDef {
4413            name: "Point".into(),
4414            fields: vec![
4415                ("x".into(), Type::Number),
4416                ("y".into(), Type::Number),
4417            ],
4418        })
4419        .unwrap()
4420    }
4421
4422    #[test]
4423    fn a_record_module_typechecks_clean() {
4424        let s = Store::open_in_memory().unwrap();
4425        let pd = point_def(&s);
4426
4427        // mk(a) -> Point { x: a, y: 0 }
4428        let a = s.put(&Node::Ref("a".into())).unwrap();
4429        let zero = s.put(&Node::Lit(0)).unwrap();
4430        let rec = s
4431            .put(&Node::Record {
4432                type_name: "Point".into(),
4433                fields: vec![("x".into(), a), ("y".into(), zero)],
4434            })
4435            .unwrap();
4436        let mk = function(
4437            &s,
4438            "mk",
4439            vec![p("a", Type::Number, Confidence::Structural)],
4440            produces(Type::Named("Point".into()), Confidence::Structural),
4441            BTreeSet::new(),
4442            vec![],
4443            vec![],
4444            rec,
4445        );
4446
4447        // getx(pt) -> pt.x
4448        let pref = s.put(&Node::Ref("pt".into())).unwrap();
4449        let fx = s
4450            .put(&Node::Field {
4451                base: pref,
4452                type_name: "Point".into(),
4453                field: "x".into(),
4454            })
4455            .unwrap();
4456        let getx = function(
4457            &s,
4458            "getx",
4459            vec![p(
4460                "pt",
4461                Type::Named("Point".into()),
4462                Confidence::Structural,
4463            )],
4464            produces(Type::Number, Confidence::Structural),
4465            BTreeSet::new(),
4466            vec![],
4467            vec![],
4468            fx,
4469        );
4470
4471        let m = s
4472            .put(&Node::Module {
4473                name: "m".into(),
4474                types: vec![pd],
4475                functions: vec![mk, getx],
4476            })
4477            .unwrap();
4478        let r = Checker::new(&s).check(&m).unwrap();
4479        assert!(r.ok(), "unexpected: {:?}", r.violations);
4480    }
4481
4482    #[test]
4483    fn a_wrong_field_type_is_a_principle_2_violation() {
4484        let s = Store::open_in_memory().unwrap();
4485        let pd = point_def(&s);
4486        // x given a Bool (a < b) instead of Number.
4487        let a = s.put(&Node::Ref("a".into())).unwrap();
4488        let b = s.put(&Node::Ref("b".into())).unwrap();
4489        let cmp = s
4490            .put(&Node::BinOp {
4491                op: BinOp::Lt,
4492                lhs: a,
4493                rhs: b,
4494            })
4495            .unwrap();
4496        let zero = s.put(&Node::Lit(0)).unwrap();
4497        let rec = s
4498            .put(&Node::Record {
4499                type_name: "Point".into(),
4500                fields: vec![("x".into(), cmp), ("y".into(), zero)],
4501            })
4502            .unwrap();
4503        let mk = function(
4504            &s,
4505            "mk",
4506            vec![
4507                p("a", Type::Number, Confidence::Structural),
4508                p("b", Type::Number, Confidence::Structural),
4509            ],
4510            produces(Type::Named("Point".into()), Confidence::Structural),
4511            BTreeSet::new(),
4512            vec![],
4513            vec![],
4514            rec,
4515        );
4516        let m = s
4517            .put(&Node::Module {
4518                name: "m".into(),
4519                types: vec![pd],
4520                functions: vec![mk],
4521            })
4522            .unwrap();
4523        let r = Checker::new(&s).check(&m).unwrap();
4524        assert!(r.violations.iter().any(|v| v.principle == 2));
4525    }
4526
4527    #[test]
4528    fn an_unknown_record_type_is_a_principle_1_violation() {
4529        let s = Store::open_in_memory().unwrap();
4530        let z = s.put(&Node::Lit(0)).unwrap();
4531        let rec = s
4532            .put(&Node::Record {
4533                type_name: "Nope".into(),
4534                fields: vec![("a".into(), z)],
4535            })
4536            .unwrap();
4537        let f = function(
4538            &s,
4539            "f",
4540            vec![],
4541            produces(Type::Named("Nope".into()), Confidence::Structural),
4542            BTreeSet::new(),
4543            vec![],
4544            vec![],
4545            rec,
4546        );
4547        let r = Checker::new(&s).check(&f).unwrap();
4548        assert!(r.violations.iter().any(|v| v.principle == 1));
4549    }
4550
4551    /// `type Status = variant { Active, Closed(reason: Number) }` and a
4552    /// function that constructs and exhaustively matches it.
4553    fn status_def(s: &Store) -> NodeHash {
4554        s.put(&Node::VariantDef {
4555            name: "Status".into(),
4556            cases: vec![
4557                ("Active".into(), vec![]),
4558                ("Closed".into(), vec![("reason".into(), Type::Number)]),
4559            ],
4560        })
4561        .unwrap()
4562    }
4563
4564    #[test]
4565    fn a_variant_module_with_exhaustive_match_is_clean() {
4566        let s = Store::open_in_memory().unwrap();
4567        let sd = status_def(&s);
4568
4569        // describe(st) -> match st { Active -> 0, Closed(reason) -> reason }
4570        let st = s.put(&Node::Ref("st".into())).unwrap();
4571        let zero = s.put(&Node::Lit(0)).unwrap();
4572        let rref = s.put(&Node::Ref("reason".into())).unwrap();
4573        let m = s
4574            .put(&Node::Match {
4575                scrutinee: st,
4576                type_name: "Status".into(),
4577                arms: vec![
4578                    MatchArm {
4579                        case: "Active".into(),
4580                        bindings: vec![],
4581                        body: zero,
4582                    },
4583                    MatchArm {
4584                        case: "Closed".into(),
4585                        bindings: vec!["reason".into()],
4586                        body: rref,
4587                    },
4588                ],
4589            })
4590            .unwrap();
4591        let describe = function(
4592            &s,
4593            "describe",
4594            vec![p(
4595                "st",
4596                Type::Named("Status".into()),
4597                Confidence::Structural,
4598            )],
4599            produces(Type::Number, Confidence::Structural),
4600            BTreeSet::new(),
4601            vec![],
4602            vec![],
4603            m,
4604        );
4605        let module = s
4606            .put(&Node::Module {
4607                name: "m".into(),
4608                types: vec![sd],
4609                functions: vec![describe],
4610            })
4611            .unwrap();
4612        let r = Checker::new(&s).check(&module).unwrap();
4613        assert!(r.ok(), "unexpected: {:?}", r.violations);
4614    }
4615
4616    #[test]
4617    fn a_non_exhaustive_match_is_a_principle_2_violation() {
4618        let s = Store::open_in_memory().unwrap();
4619        let sd = status_def(&s);
4620        let st = s.put(&Node::Ref("st".into())).unwrap();
4621        let zero = s.put(&Node::Lit(0)).unwrap();
4622        // Only covers Active; Closed missing.
4623        let m = s
4624            .put(&Node::Match {
4625                scrutinee: st,
4626                type_name: "Status".into(),
4627                arms: vec![MatchArm {
4628                    case: "Active".into(),
4629                    bindings: vec![],
4630                    body: zero,
4631                }],
4632            })
4633            .unwrap();
4634        let f = function(
4635            &s,
4636            "f",
4637            vec![p(
4638                "st",
4639                Type::Named("Status".into()),
4640                Confidence::Structural,
4641            )],
4642            produces(Type::Number, Confidence::Structural),
4643            BTreeSet::new(),
4644            vec![],
4645            vec![],
4646            m,
4647        );
4648        let module = s
4649            .put(&Node::Module {
4650                name: "m".into(),
4651                types: vec![sd],
4652                functions: vec![f],
4653            })
4654            .unwrap();
4655        let r = Checker::new(&s).check(&module).unwrap();
4656        assert!(r
4657            .violations
4658            .iter()
4659            .any(|v| v.principle == 2 && v.detail.contains("Closed")));
4660    }
4661
4662    #[test]
4663    fn constructing_an_unknown_case_is_a_principle_2_violation() {
4664        let s = Store::open_in_memory().unwrap();
4665        let sd = status_def(&s);
4666        let bad = s
4667            .put(&Node::Variant {
4668                type_name: "Status".into(),
4669                case: "Nope".into(),
4670                fields: vec![],
4671            })
4672            .unwrap();
4673        let f = function(
4674            &s,
4675            "mk",
4676            vec![],
4677            produces(Type::Named("Status".into()), Confidence::Structural),
4678            BTreeSet::new(),
4679            vec![],
4680            vec![],
4681            bad,
4682        );
4683        let module = s
4684            .put(&Node::Module {
4685                name: "m".into(),
4686                types: vec![sd],
4687                functions: vec![f],
4688            })
4689            .unwrap();
4690        let r = Checker::new(&s).check(&module).unwrap();
4691        assert!(r
4692            .violations
4693            .iter()
4694            .any(|v| v.principle == 2 && v.detail.contains("Nope")));
4695    }
4696
4697    /// `identity<T>(x: T) -> T` used at `Number` — the type parameter is
4698    /// inferred from the argument and substituted into the result.
4699    fn identity_fn(s: &Store) -> NodeHash {
4700        let x = s.put(&Node::Ref("x".into())).unwrap();
4701        s.put(&Node::Function {
4702            name: "identity".into(),
4703            type_params: vec!["T".into()],
4704            params: vec![p("x", Type::Var("T".into()), Confidence::Structural)],
4705            produces: Produces {
4706                ty: Type::Var("T".into()),
4707                confidence: Confidence::Structural,
4708            },
4709            requires: BTreeSet::new(),
4710            on_failure: vec![],
4711            body: vec![],
4712            result: x,
4713        })
4714        .unwrap()
4715    }
4716
4717    #[test]
4718    fn a_generic_function_resolves_at_the_call_site() {
4719        let s = Store::open_in_memory().unwrap();
4720        let id = identity_fn(&s);
4721        let n = s.put(&Node::Ref("n".into())).unwrap();
4722        let call = s
4723            .put(&Node::Call {
4724                func: "identity".into(),
4725                args: vec![n],
4726            })
4727            .unwrap();
4728        let f = function(
4729            &s,
4730            "use_id",
4731            vec![p("n", Type::Number, Confidence::Structural)],
4732            produces(Type::Number, Confidence::Structural),
4733            BTreeSet::new(),
4734            vec![],
4735            vec![],
4736            call,
4737        );
4738        let m = s
4739            .put(&Node::Module {
4740                name: "m".into(),
4741                types: vec![],
4742                functions: vec![id, f],
4743            })
4744            .unwrap();
4745        let r = Checker::new(&s).check(&m).unwrap();
4746        assert!(r.ok(), "unexpected: {:?}", r.violations);
4747    }
4748
4749    #[test]
4750    fn a_generic_argument_conflict_is_a_principle_2_violation() {
4751        let s = Store::open_in_memory().unwrap();
4752        // pair<T>(a: T, b: T) -> T
4753        let aref = s.put(&Node::Ref("a".into())).unwrap();
4754        let pair = s
4755            .put(&Node::Function {
4756                name: "pair".into(),
4757                type_params: vec!["T".into()],
4758                params: vec![
4759                    p("a", Type::Var("T".into()), Confidence::Structural),
4760                    p("b", Type::Var("T".into()), Confidence::Structural),
4761                ],
4762                produces: Produces {
4763                    ty: Type::Var("T".into()),
4764                    confidence: Confidence::Structural,
4765                },
4766                requires: BTreeSet::new(),
4767                on_failure: vec![],
4768                body: vec![],
4769                result: aref,
4770            })
4771            .unwrap();
4772        // g(n) = pair(n, n < n)  — T bound to Number then to Bool.
4773        let n1 = s.put(&Node::Ref("n".into())).unwrap();
4774        let n2 = s.put(&Node::Ref("n".into())).unwrap();
4775        let n3 = s.put(&Node::Ref("n".into())).unwrap();
4776        let cmp = s
4777            .put(&Node::BinOp {
4778                op: BinOp::Lt,
4779                lhs: n2,
4780                rhs: n3,
4781            })
4782            .unwrap();
4783        let call = s
4784            .put(&Node::Call {
4785                func: "pair".into(),
4786                args: vec![n1, cmp],
4787            })
4788            .unwrap();
4789        let g = function(
4790            &s,
4791            "g",
4792            vec![p("n", Type::Number, Confidence::Structural)],
4793            produces(Type::Number, Confidence::Structural),
4794            BTreeSet::new(),
4795            vec![],
4796            vec![],
4797            call,
4798        );
4799        let m = s
4800            .put(&Node::Module {
4801                name: "m".into(),
4802                types: vec![],
4803                functions: vec![pair, g],
4804            })
4805            .unwrap();
4806        let r = Checker::new(&s).check(&m).unwrap();
4807        assert!(r.violations.iter().any(|v| v.principle == 2));
4808    }
4809
4810    #[test]
4811    fn string_literal_and_str_len_typecheck() {
4812        let s = Store::open_in_memory().unwrap();
4813        // greet() -> String { yield "hello" }
4814        let hello = s.put(&Node::Str("hello".into())).unwrap();
4815        let greet = function(
4816            &s,
4817            "greet",
4818            vec![],
4819            produces(Type::String, Confidence::Structural),
4820            BTreeSet::new(),
4821            vec![],
4822            vec![],
4823            hello,
4824        );
4825        // size() -> Number { yield str_len("hello") }
4826        let h2 = s.put(&Node::Str("hello".into())).unwrap();
4827        let sl = s.put(&Node::StrLen(h2)).unwrap();
4828        let size = function(
4829            &s,
4830            "size",
4831            vec![],
4832            produces(Type::Number, Confidence::Structural),
4833            BTreeSet::new(),
4834            vec![],
4835            vec![],
4836            sl,
4837        );
4838        let m = s
4839            .put(&Node::Module {
4840                name: "m".into(),
4841                types: vec![],
4842                functions: vec![greet, size],
4843            })
4844            .unwrap();
4845        let r = Checker::new(&s).check(&m).unwrap();
4846        assert!(r.ok(), "unexpected: {:?}", r.violations);
4847    }
4848
4849    #[test]
4850    fn str_len_on_a_number_is_a_principle_2_violation() {
4851        let s = Store::open_in_memory().unwrap();
4852        let n = s.put(&Node::Lit(3)).unwrap();
4853        let sl = s.put(&Node::StrLen(n)).unwrap();
4854        let f = function(
4855            &s,
4856            "f",
4857            vec![],
4858            produces(Type::Number, Confidence::Structural),
4859            BTreeSet::new(),
4860            vec![],
4861            vec![],
4862            sl,
4863        );
4864        let r = Checker::new(&s).check(&f).unwrap();
4865        assert!(r.violations.iter().any(|v| v.principle == 2));
4866    }
4867
4868    #[test]
4869    fn a_homogeneous_list_typechecks_and_a_mixed_one_does_not() {
4870        let s = Store::open_in_memory().unwrap();
4871        // ok: [1,2,3] -> List<Number>; list_get(...,0) -> Number
4872        let e0 = s.put(&Node::Lit(1)).unwrap();
4873        let e1 = s.put(&Node::Lit(2)).unwrap();
4874        let lst = s.put(&Node::List(vec![e0, e1])).unwrap();
4875        let idx = s.put(&Node::Lit(0)).unwrap();
4876        let g = s
4877            .put(&Node::ListGet {
4878                list: lst,
4879                index: idx,
4880            })
4881            .unwrap();
4882        let ok = function(
4883            &s,
4884            "ok",
4885            vec![],
4886            produces(Type::Number, Confidence::Structural),
4887            BTreeSet::new(),
4888            vec![],
4889            vec![],
4890            g,
4891        );
4892        assert!(Checker::new(&s).check(&ok).unwrap().ok());
4893
4894        // bad: [1, "x"] -> P2 (mixed element types)
4895        let n = s.put(&Node::Lit(1)).unwrap();
4896        let t = s.put(&Node::Str("x".into())).unwrap();
4897        let mixed = s.put(&Node::List(vec![n, t])).unwrap();
4898        let len = s.put(&Node::ListLen(mixed)).unwrap();
4899        let bad = function(
4900            &s,
4901            "bad",
4902            vec![],
4903            produces(Type::Number, Confidence::Structural),
4904            BTreeSet::new(),
4905            vec![],
4906            vec![],
4907            len,
4908        );
4909        let r = Checker::new(&s).check(&bad).unwrap();
4910        assert!(r.violations.iter().any(|v| v.principle == 2));
4911    }
4912}