Skip to main content

decl_lang/
checker.rs

1//! Static checks over the AST + resolved types — a port of the reference
2//! implementation's checker.ts (chapters 3–4). Implemented:
3//!   E3001 duplicate module name         E3003 unknown type name
4//!   E4010 mixed range endpoints         E4011 empty range / array size
5//!   E4012 structurally empty intersection
6//!   E4013 non-discriminable record union arms
7//!   E4014 more than one non-record object arm in a union
8//!   E4015 map key not string-shaped     E4030 inheritance widening
9//!   E4032 illegal member-kind transition
10//!   E4052 ?? mixed with &&/|| unparenthesized
11//!   E4094 context variable without / with an invalid context declaration
12//! plus the expression pass of infer.rs (inference, assignability, absence).
13use crate::ast::*;
14use crate::engine::Engine;
15use crate::infer::Ty;
16use crate::infer::*;
17use crate::semantics::*;
18use crate::subsume::{structurally_empty, subsumes};
19use std::cell::{Cell, RefCell};
20use std::collections::{HashMap, HashSet};
21use std::rc::Rc;
22
23// ---------------- generic AST traversal (the reference walks object values) ----------------
24/// every expression reachable from `e`, in the reference's object-value
25/// order; `into_types` also descends into type ASTs embedded in
26/// expressions (match arm types), as the untyped walks do
27fn walk_expr_tree(e: &Rc<Expr>, into_types: bool, f: &mut dyn FnMut(&Rc<Expr>)) {
28    f(e);
29    let mut go = |x: &Rc<Expr>| walk_expr_tree(x, into_types, f);
30    match &**e {
31        Expr::Lit(_)
32        | Expr::UnitLit { .. }
33        | Expr::Name(_)
34        | Expr::Ctx(_)
35        | Expr::Referrers { .. }
36        | Expr::Pattern(_) => {}
37        Expr::Template(parts) => {
38            for p in parts {
39                if let TPart::Expr(x) = p {
40                    go(x);
41                }
42            }
43        }
44        Expr::Obj(entries) => entries.iter().for_each(|(_, v)| go(v)),
45        Expr::Arr(items) => items.iter().for_each(|(_, v)| go(v)),
46        Expr::Comp { head, clauses } => {
47            go(head);
48            for c in clauses {
49                go(&c.iter);
50                c.filters.iter().for_each(&mut go);
51            }
52        }
53        Expr::MapComp { key, val, clauses } => {
54            go(key);
55            go(val);
56            for c in clauses {
57                go(&c.iter);
58                c.filters.iter().for_each(&mut go);
59            }
60        }
61        Expr::Bin { l, r, .. } => {
62            go(l);
63            go(r);
64        }
65        Expr::Un { x, .. } | Expr::Paren(x) => go(x),
66        Expr::If { c, t, f: ff } => {
67            go(c);
68            go(t);
69            go(ff);
70        }
71        Expr::Lambda { body, .. } => go(body),
72        Expr::Call { fun, args } => {
73            go(fun);
74            args.iter().for_each(&mut go);
75        }
76        Expr::Member { x, .. } => go(x),
77        Expr::Index { x, i } => {
78            go(x);
79            go(i);
80        }
81        Expr::With { base, patch } => {
82            go(base);
83            go(patch);
84        }
85        Expr::Match { subject, arms } => {
86            go(subject);
87            for a in arms {
88                if into_types {
89                    if let Some(t) = &a.ty {
90                        walk_type_exprs(t, into_types, f);
91                    }
92                }
93                walk_expr_tree(&a.body, into_types, f);
94            }
95        }
96    }
97}
98/// every expression embedded in a type AST (predicates, member defaults, asserts)
99fn walk_type_exprs(t: &TypeAst, into_types: bool, f: &mut dyn FnMut(&Rc<Expr>)) {
100    match t {
101        TypeAst::Prim { .. }
102        | TypeAst::Lit { .. }
103        | TypeAst::Range { .. }
104        | TypeAst::Pattern { .. } => {}
105        TypeAst::Record { members, .. } => members
106            .iter()
107            .for_each(|m| walk_member_exprs(m, into_types, f)),
108        TypeAst::Map { key, val, .. } => {
109            walk_type_exprs(key, into_types, f);
110            walk_type_exprs(val, into_types, f);
111        }
112        TypeAst::Array { elem, .. } => walk_type_exprs(elem, into_types, f),
113        TypeAst::Union { arms, .. } | TypeAst::Isect { arms, .. } => {
114            arms.iter().for_each(|a| walk_type_exprs(a, into_types, f))
115        }
116        TypeAst::Func { params, ret, .. } => {
117            params
118                .iter()
119                .for_each(|p| walk_type_exprs(p, into_types, f));
120            walk_type_exprs(ret, into_types, f);
121        }
122        TypeAst::Named {
123            args, preds, ext, ..
124        } => {
125            args.iter().for_each(|a| walk_type_exprs(a, into_types, f));
126            if let Some(ps) = preds {
127                ps.iter().for_each(|p| walk_expr_tree(p, into_types, f));
128            }
129            if let Some(x) = ext {
130                walk_type_exprs(x, into_types, f);
131            }
132        }
133    }
134}
135fn walk_member_exprs(m: &MemberAst, into_types: bool, f: &mut dyn FnMut(&Rc<Expr>)) {
136    match m {
137        MemberAst::Value { ty, dflt, .. } => {
138            walk_type_exprs(ty, into_types, f);
139            if let Some(d) = dflt {
140                walk_expr_tree(d, into_types, f);
141            }
142        }
143        MemberAst::Derived { ty, expr, .. } => {
144            if let Some(t) = ty {
145                walk_type_exprs(t, into_types, f);
146            }
147            walk_expr_tree(expr, into_types, f);
148        }
149        MemberAst::Context { ty, .. } => walk_type_exprs(ty, into_types, f),
150        MemberAst::Assert { cond, tail, .. } => {
151            walk_expr_tree(cond, into_types, f);
152            match tail {
153                Some(Tail::Inline { template, .. }) => {
154                    for p in template {
155                        if let TPart::Expr(x) = p {
156                            walk_expr_tree(x, into_types, f);
157                        }
158                    }
159                }
160                Some(Tail::Ref { args, .. }) => {
161                    args.iter().for_each(|a| walk_expr_tree(a, into_types, f))
162                }
163                None => {}
164            }
165        }
166        MemberAst::When { cond, body, .. } => {
167            walk_expr_tree(cond, into_types, f);
168            body.iter()
169                .for_each(|b| walk_member_exprs(b, into_types, f));
170        }
171    }
172}
173
174fn str_shaped(k: &RT) -> bool {
175    match &k.k {
176        RTk::Prim(n) => n == "string",
177        RTk::Pattern { .. } => true,
178        RTk::Lit(Value::Str(_)) => true,
179        RTk::Union(arms) => arms.iter().all(str_shaped),
180        RTk::Pred { base, .. } => str_shaped(base),
181        _ => false,
182    }
183}
184fn kind_name(k: MKind) -> &'static str {
185    match k {
186        MKind::Req => "req",
187        MKind::Opt => "opt",
188        MKind::Dflt => "dflt",
189        MKind::Der => "der",
190    }
191}
192
193/// the language server's tables, filled while the checker runs (infer.rs hooks)
194pub struct CheckHooks {
195    pub record: Option<Rc<dyn Fn(&Rc<Expr>, &Ty)>>,
196    pub resolve_hook: Option<Rc<dyn Fn(&Rc<Expr>, Option<Target>)>>,
197}
198
199pub fn check_module(
200    decls: &[Decl],
201    linked: Option<Rc<Env>>,
202    hooks: Option<&CheckHooks>,
203) -> Vec<Diag> {
204    let out: Rc<RefCell<Vec<Diag>>> = Rc::new(RefCell::new(vec![]));
205    // reports are anchored to the expression under inference (the context's
206    // `pos`, shared by child contexts), else to the declaration being checked
207    let pos: Rc<RefCell<Option<Rc<Expr>>>> = Rc::new(RefCell::new(None));
208    let cur_decl: Rc<Cell<Option<Loc>>> = Rc::new(Cell::new(None));
209    let report: Report = {
210        let out = out.clone();
211        let pos = pos.clone();
212        let cur = cur_decl.clone();
213        Rc::new(move |code: &str, message: String| {
214            let loc = pos.borrow().as_ref().and_then(expr_loc).or(cur.get());
215            let mut d = Diag::error(message, String::new(), Some(code));
216            d.loc = loc;
217            out.borrow_mut().push(d)
218        })
219    };
220    let rep = |code: &str, message: String| report(code, message);
221
222    let env = match &linked {
223        Some(e) => e.clone(),
224        None => {
225            let e = Env::new();
226            e.load(decls);
227            e
228        }
229    };
230    // installs env.const_eval / env.expr_eval (§4.13, §3.16); kept alive for the check
231    let _eng = if env.const_eval.borrow().is_none() {
232        Some(Engine::new(env.clone()))
233    } else {
234        None
235    };
236    *env.const_diag_sink.borrow_mut() = Some(out.clone()); // constant-evaluation errors surface here
237    for n in env.duplicates.borrow().iter() {
238        rep("E3001", format!("duplicate name {n} in module"));
239    }
240    out.borrow_mut().extend(env.finalize_unit_space()); // §3.16 unit/dimension spaces
241
242    // ---------- §4.13: constant positions ----------
243    let tparams: RefCell<HashSet<String>> = RefCell::new(HashSet::new());
244    let is_input = |n: &str| env.inputs.borrow().contains_key(n);
245    let is_output = |n: &str| env.outputs.borrow().iter().any(|(o, _, _)| o == n);
246    let check_endpoint = |v: Option<&Value>, where_: &str| {
247        let Some(Value::Str(v)) = v else { return };
248        if tparams.borrow().contains(v) || v.contains('.') {
249            return;
250        }
251        if is_input(v) || is_output(v) {
252            rep(
253                "E4021",
254                format!("non-constant {where_}: {v} is an input/output, not a module const"),
255            );
256        } else if !env.consts.borrow().contains_key(v) {
257            rep("E3003", format!("unknown name {v} in a {where_}"));
258        }
259    };
260    let const_violation = |e: &Rc<Expr>| -> Option<String> {
261        let mut found: Option<String> = None;
262        walk_expr_tree(e, true, &mut |x| {
263            if found.is_some() {
264                return;
265            }
266            found = match &**x {
267                Expr::Ctx(n) => Some(format!("context variable {n}")),
268                Expr::Referrers { .. } => Some("$referrers".into()),
269                Expr::Name(n) if is_input(n) => Some(format!("input {n}")),
270                Expr::Name(n) if is_output(n) => Some(format!("output {n}")),
271                _ => None,
272            };
273        });
274        found
275    };
276
277    // ---------- AST-level walks ----------
278    let is_bool_op = |e: &Expr| matches!(e, Expr::Bin { op, .. } if op == "&&" || op == "||");
279    let walk_expr = |e: &Rc<Expr>| {
280        walk_expr_tree(e, true, &mut |x| {
281            if let Expr::Bin { op, l, r } = &**x {
282                if op == "??" && (is_bool_op(l) || is_bool_op(r)) {
283                    rep(
284                        "E4052",
285                        "`??` mixed with `&&`/`||` without parentheses".into(),
286                    );
287                }
288            }
289        });
290    };
291
292    // D30: context obligations
293    fn ctx_uses(m: &MemberAst, used: &mut Vec<String>) {
294        let mut scan = |e: &Rc<Expr>| {
295            walk_expr_tree(e, false, &mut |x| {
296                if let Expr::Ctx(n) = &**x {
297                    if ["$parent", "$root", "$key"].contains(&n.as_str()) && !used.contains(n) {
298                        used.push(n.clone());
299                    }
300                }
301            });
302        };
303        match m {
304            MemberAst::Value { dflt: Some(d), .. } => scan(d),
305            MemberAst::Derived { expr, .. } => scan(expr),
306            MemberAst::Assert { cond, .. } => scan(cond),
307            MemberAst::When { cond, body, .. } => {
308                scan(cond);
309                for b in body {
310                    ctx_uses(b, used);
311                }
312            }
313            _ => {}
314        }
315    }
316    let check_record_ctx = |members: &[MemberAst], depth: i32, decl_name: Option<&str>| {
317        let declared: Vec<(&String, &TypeAst)> = members
318            .iter()
319            .filter_map(|m| {
320                if let MemberAst::Context { variable, ty, .. } = m {
321                    Some((variable, ty))
322                } else {
323                    None
324                }
325            })
326            .collect();
327        for (v, t) in &declared {
328            let is_ref = matches!(t, TypeAst::Named { name, .. } if name == "ref");
329            if (v.as_str() == "$parent" || v.as_str() == "$root") && !is_ref {
330                rep(
331                    "E4094",
332                    format!(
333                        "{v} declaration must be ref<...> ({})",
334                        decl_name.unwrap_or("anonymous")
335                    ),
336                );
337            }
338            if v.as_str() == "$key" && is_ref {
339                rep(
340                    "E4094",
341                    "$key declares a plain value type, not ref<...>".into(),
342                );
343            }
344        }
345        if depth > 1 {
346            return; // lexically nested: parent evident, no declaration required
347        }
348        let mut used: Vec<String> = vec![];
349        for m in members {
350            ctx_uses(m, &mut used);
351        }
352        for u in used {
353            if !declared.iter().any(|(v, _)| **v == u) {
354                rep(
355                    "E4094",
356                    format!(
357                        "{u} used without a context declaration in {}",
358                        decl_name.unwrap_or("anonymous type")
359                    ),
360                );
361            }
362        }
363    };
364
365    // inheritance (extension)
366    let check_extension = |name: &str, args: &[TypeAst], ext: &TypeAst, decl_name: Option<&str>| {
367        let Ok(base) = env.resolve(
368            &TypeAst::Named {
369                name: name.to_string(),
370                args: args.to_vec(),
371                preds: None,
372                ext: None,
373                loc: None,
374            },
375            None,
376        ) else {
377            return;
378        }; // unknown base reported by the resolution pass
379        let RTk::Rec(brec) = &base.k else {
380            rep("E4031", format!("extending non-record type {name}"));
381            return;
382        };
383        let TypeAst::Record {
384            members: ext_members,
385            ..
386        } = ext
387        else {
388            return;
389        };
390        let bmembers = brec.members.borrow();
391        for om in ext_members {
392            let (oname, o_kind, o_type): (&str, &str, Option<&TypeAst>) = match om {
393                MemberAst::Assert { .. } | MemberAst::When { .. } | MemberAst::Context { .. } => {
394                    continue
395                }
396                MemberAst::Derived {
397                    name, ty, hidden, ..
398                } => (name, if *hidden { "hidden" } else { "der" }, ty.as_ref()),
399                MemberAst::Value {
400                    name,
401                    opt,
402                    ty,
403                    dflt,
404                    ..
405                } => (
406                    name,
407                    if dflt.is_some() {
408                        "dflt"
409                    } else if *opt {
410                        "opt"
411                    } else {
412                        "req"
413                    },
414                    Some(ty),
415                ),
416            };
417            let Some(bm) = bmembers.iter().find(|x| x.name == oname) else {
418                continue;
419            }; // addition
420            let b_kind = if bm.kind == MKind::Der && bm.hidden {
421                "hidden"
422            } else {
423                kind_name(bm.kind)
424            };
425            // §5.9: overriding narrows; a hidden member stays hidden, a visible one visible
426            let allowed: &[&str] = match b_kind {
427                "req" => &["req", "dflt", "der"],
428                "opt" => &["req", "opt", "dflt", "der"],
429                "dflt" => &["req", "dflt", "der"],
430                "der" => &["der"],
431                _ => &["hidden"],
432            };
433            if !allowed.contains(&o_kind) {
434                rep(
435                    "E4032",
436                    format!(
437                        "illegal member-kind transition for {oname}: {b_kind} -> {o_kind} ({})",
438                        decl_name.unwrap_or(name)
439                    ),
440                );
441                continue;
442            }
443            let ot = try_resolve(&env, o_type);
444            if let (Some(ot), Some(bt)) = (ot, &bm.ty) {
445                if !subsumes(&env, &ot, bt) {
446                    rep(
447                        "E4030",
448                        format!(
449                            "override widens inherited member {oname} ({})",
450                            decl_name.unwrap_or(name)
451                        ),
452                    );
453                }
454            }
455        }
456    };
457
458    struct Walk<'a> {
459        rep: &'a dyn Fn(&str, String),
460        check_endpoint: &'a dyn Fn(Option<&Value>, &str),
461        const_violation: &'a dyn Fn(&Rc<Expr>) -> Option<String>,
462        walk_expr: &'a dyn Fn(&Rc<Expr>),
463        check_record_ctx: &'a dyn Fn(&[MemberAst], i32, Option<&str>),
464        check_extension: &'a dyn Fn(&str, &[TypeAst], &TypeAst, Option<&str>),
465    }
466    impl<'a> Walk<'a> {
467        fn walk_type(&self, t: Option<&TypeAst>, depth: i32, decl_name: Option<&str>) {
468            let Some(t) = t else { return };
469            match t {
470                TypeAst::Range { lo, hi, .. } => {
471                    let kinds = [js_typeof(lo), js_typeof(hi)];
472                    if kinds[0] != kinds[1] && !kinds.contains(&"string") {
473                        (self.rep)(
474                            "E4010",
475                            format!("mixed range endpoints: {}..{}", js_str(lo), js_str(hi)),
476                        );
477                    }
478                    (self.check_endpoint)(Some(lo), "range endpoint");
479                    (self.check_endpoint)(Some(hi), "range endpoint");
480                }
481                TypeAst::Record { members, .. } => {
482                    (self.check_record_ctx)(members, depth, decl_name);
483                    for m in members {
484                        self.walk_member(m, depth, decl_name);
485                    }
486                }
487                TypeAst::Map { key, val, .. } => {
488                    self.walk_type(Some(key), depth, decl_name);
489                    self.walk_type(Some(val), depth, decl_name);
490                }
491                TypeAst::Array { elem, lo, hi, .. } => {
492                    (self.check_endpoint)(lo.as_ref(), "array size");
493                    (self.check_endpoint)(hi.as_ref(), "array size");
494                    self.walk_type(Some(elem), depth, decl_name);
495                }
496                TypeAst::Union { arms, .. } | TypeAst::Isect { arms, .. } => arms
497                    .iter()
498                    .for_each(|a| self.walk_type(Some(a), depth, decl_name)),
499                TypeAst::Func { params, ret, .. } => {
500                    params
501                        .iter()
502                        .for_each(|p| self.walk_type(Some(p), depth, decl_name));
503                    self.walk_type(Some(ret), depth, decl_name);
504                }
505                TypeAst::Named {
506                    name,
507                    args,
508                    preds,
509                    ext,
510                    ..
511                } => {
512                    args.iter()
513                        .for_each(|a| self.walk_type(Some(a), depth, decl_name));
514                    if let Some(x) = ext {
515                        (self.check_extension)(name, args, x, decl_name);
516                        self.walk_type(Some(x), depth + 1, decl_name);
517                    }
518                    for p in preds.iter().flatten() {
519                        if let Some(bad) = (self.const_violation)(p) {
520                            (self.rep)(
521                                "E4021",
522                                format!("non-constant predicate argument: {bad} (§4.13)"),
523                            );
524                        }
525                        (self.walk_expr)(p);
526                    }
527                }
528                TypeAst::Prim { .. } | TypeAst::Lit { .. } | TypeAst::Pattern { .. } => {}
529            }
530        }
531        fn walk_member(&self, m: &MemberAst, depth: i32, decl_name: Option<&str>) {
532            match m {
533                MemberAst::Value { ty, dflt, .. } => {
534                    self.walk_type(Some(ty), depth + 1, decl_name);
535                    if let Some(d) = dflt {
536                        (self.walk_expr)(d);
537                    }
538                }
539                MemberAst::Derived { ty, expr, .. } => {
540                    self.walk_type(ty.as_ref(), depth + 1, decl_name);
541                    (self.walk_expr)(expr);
542                }
543                MemberAst::Context { ty, .. } => self.walk_type(Some(ty), depth + 1, decl_name),
544                MemberAst::Assert { cond, .. } => (self.walk_expr)(cond),
545                MemberAst::When { cond, body, .. } => {
546                    (self.walk_expr)(cond);
547                    body.iter()
548                        .for_each(|x| self.walk_member(x, depth, decl_name));
549                }
550            }
551        }
552    }
553    let walk = Walk {
554        rep: &rep,
555        check_endpoint: &check_endpoint,
556        const_violation: &const_violation,
557        walk_expr: &walk_expr,
558        check_record_ctx: &check_record_ctx,
559        check_extension: &check_extension,
560    };
561
562    // ---------- resolution-level checks ----------
563    let resolve_reported: RefCell<HashSet<String>> = RefCell::new(HashSet::new());
564    let pattern_unknown = regex::Regex::new(r"pattern interpolation of .*: unknown type").unwrap();
565    let map_resolve_err = |msg: &str, where_: &str| {
566        let key = format!("{msg}|{where_}");
567        if !resolve_reported.borrow_mut().insert(key) {
568            return; // one resolution failure, one report
569        }
570        if msg.contains("unknown dimension") || msg.contains("circular dimension") {
571            rep("E3003", format!("{msg} (in {where_})"));
572        } else if msg.contains("unknown unit") {
573            rep("E4073", format!("{msg} (in {where_})"));
574        } else if pattern_unknown.is_match(msg) || msg.contains("unknown type") {
575            rep("E3003", format!("{msg} (in {where_})"));
576        } else if msg.contains("generic arity") {
577            rep("E4022", format!("{msg} (in {where_})"));
578        } else if msg.contains("outside parameter") {
579            rep("E4023", format!("{msg} (in {where_})"));
580        } else if msg.contains("non-constant value argument") {
581            rep("E4021", format!("{msg} (in {where_})"));
582        } else if msg.contains("pattern interpolation") {
583            rep("E4117", format!("{msg} (in {where_})"));
584        } else if msg.contains("malformed pattern") {
585            rep("E4119", format!("{msg} (in {where_})"));
586        } else {
587            rep("E4001", format!("{msg} (in {where_})")); // never drop a resolution failure silently
588        }
589    };
590    let resolve_or_report = |t: Option<&TypeAst>, where_: &str| -> Option<RT> {
591        let t = t?;
592        match env.resolve(t, None) {
593            Ok(rt) => Some(rt),
594            Err(msg) => {
595                map_resolve_err(&msg, where_);
596                None
597            }
598        }
599    };
600
601    fn check_resolved(
602        env: &Rc<Env>,
603        rep: &dyn Fn(&str, String),
604        rt: &RT,
605        name: &str,
606        seen: &mut HashSet<usize>,
607    ) {
608        let id = Rc::as_ptr(rt) as usize;
609        if seen.contains(&id) {
610            return;
611        }
612        seen.insert(id);
613        match &rt.k {
614            RTk::Range { lo, hi, .. } => {
615                let ks = [js_typeof(lo), js_typeof(hi)];
616                if !ks.contains(&"string") && ks[0] != ks[1] {
617                    rep(
618                        "E4010",
619                        format!("mixed range endpoints after constant substitution in {name}"),
620                    );
621                }
622                if structurally_empty(env, rt) {
623                    rep("E4011", format!("empty range in {name}"));
624                }
625            }
626            RTk::Arr { elem, .. } => {
627                if structurally_empty(env, rt) {
628                    rep("E4011", format!("empty array size in {name}"));
629                }
630                check_resolved(env, rep, elem, name, seen);
631            }
632            RTk::IsectN(arms) => {
633                if structurally_empty(env, rt) {
634                    rep(
635                        "E4012",
636                        format!("structurally empty intersection in {name}"),
637                    );
638                }
639                arms.iter()
640                    .for_each(|a| check_resolved(env, rep, a, name, seen));
641            }
642            RTk::Map { key, val } => {
643                if !str_shaped(key) {
644                    rep("E4015", format!("map key type not string-shaped in {name}"));
645                }
646                check_resolved(env, rep, val, name, seen);
647            }
648            RTk::Union(arms) => {
649                let recs: Vec<&RT> = arms.iter().filter(|a| is_rec(a)).collect();
650                if recs.len() >= 2 {
651                    let lit_of = |r: &RT, n: &str| -> Option<Value> {
652                        rec_members(r)
653                            .iter()
654                            .find(|x| x.name == n)
655                            .and_then(|x| x.ty.as_ref())
656                            .and_then(|t| {
657                                if let RTk::Lit(v) = &t.k {
658                                    Some(v.clone())
659                                } else {
660                                    None
661                                }
662                            })
663                    };
664                    let disc: Vec<String> = rec_members(recs[0])
665                        .iter()
666                        .filter(|m| {
667                            matches!(m.ty.as_ref().map(|t| &t.k), Some(RTk::Lit(_)))
668                                && recs.iter().all(|r| lit_of(r, &m.name).is_some())
669                        })
670                        .map(|m| m.name.clone())
671                        .collect();
672                    let tuples: HashSet<String> = recs
673                        .iter()
674                        .map(|r| {
675                            format!(
676                                "[{}]",
677                                disc.iter()
678                                    .map(|d| json_str(&js_str(&lit_of(r, d).unwrap())))
679                                    .collect::<Vec<_>>()
680                                    .join(",")
681                            )
682                        })
683                        .collect();
684                    if disc.is_empty() || tuples.len() != recs.len() {
685                        rep(
686                            "E4013",
687                            format!("record union arms not discriminable in {name}"),
688                        );
689                    }
690                }
691                let non_rec_obj = arms
692                    .iter()
693                    .filter(|a| matches!(a.k, RTk::Map { .. } | RTk::Quantity(_)))
694                    .count();
695                if non_rec_obj > 1 {
696                    rep(
697                        "E4014",
698                        format!("more than one non-record object arm in {name}"),
699                    );
700                }
701                arms.iter()
702                    .for_each(|a| check_resolved(env, rep, a, name, seen));
703            }
704            RTk::Rec(_) => {
705                for m in rec_members(rt) {
706                    if let Some(t) = &m.ty {
707                        check_resolved(env, rep, t, name, seen);
708                    }
709                }
710            }
711            RTk::Pred { base, .. } => check_resolved(env, rep, base, name, seen),
712            RTk::Ref(target) => check_resolved(env, rep, target, name, seen),
713            _ => {}
714        }
715    }
716
717    let type_order = env.type_order.borrow().clone();
718    let type_entry = |n: &str| env.type_asts.borrow().get(n).cloned();
719    for name in &type_order {
720        let Some(decl) = type_entry(name) else {
721            continue;
722        };
723        if !decl.params.is_empty() {
724            continue; // generic declarations check at instantiation (§3.15)
725        }
726        match env.resolve(&named(name), None) {
727            Ok(rt) => check_resolved(&env, &rep, &rt, name, &mut HashSet::new()),
728            Err(msg) => map_resolve_err(&msg, name),
729        }
730    }
731
732    // AST walks over all declarations
733    for d in decls {
734        cur_decl.set(d.loc);
735        *tparams.borrow_mut() = match &d.body {
736            DeclBody::Type { params, .. } => params.iter().map(|p| p.name.clone()).collect(),
737            _ => HashSet::new(),
738        };
739        match &d.body {
740            DeclBody::Type { name, ty, .. } => walk.walk_type(Some(ty), 1, Some(name)),
741            DeclBody::Const { ty, expr, .. } => {
742                walk.walk_type(ty.as_ref(), 0, None);
743                walk_expr(expr);
744            }
745            DeclBody::Func {
746                params, ret, body, ..
747            } => {
748                params
749                    .iter()
750                    .for_each(|p| walk.walk_type(p.ty.as_ref(), 0, None));
751                walk.walk_type(ret.as_ref(), 0, None);
752                walk_expr(body);
753            }
754            DeclBody::Output { ty, expr, .. } => {
755                walk.walk_type(Some(ty), 0, None);
756                walk_expr(expr);
757            }
758            DeclBody::Input { ty, fallback, .. } => {
759                walk.walk_type(Some(ty), 0, None);
760                if let Some(f) = fallback {
761                    walk_expr(f);
762                }
763            }
764            _ => {}
765        }
766    }
767
768    // ---------- expression pass: inference, assignability, absence (§3.18, §4.10) ----------
769    let mut cx0 = make_ctx(env.clone(), report.clone());
770    cx0.pos = pos.clone();
771    if let Some(h) = hooks {
772        cx0.record = h.record.clone();
773        cx0.resolve_hook = h.resolve_hook.clone();
774    }
775    let cx0 = cx0;
776    let is_bool_ty = |t: &Ty| match &t.rt {
777        None => true,
778        Some(r) => {
779            matches!(&r.k, RTk::Prim(n) if n == "bool") || matches!(&r.k, RTk::Lit(Value::Bool(_)))
780        }
781    };
782    let rec_ctx = |cx: &Ctx, rt: &RT, ast: Option<&TypeAst>| -> Ctx {
783        let mut c = cx.child();
784        for m in rec_members(rt) {
785            c.vars.insert(
786                m.name.clone(),
787                Ty {
788                    rt: member_ty(&m),
789                    abs: m.kind == MKind::Opt,
790                },
791            );
792        }
793        c.vars.insert("$this".into(), tyv(Some(rt.clone())));
794        c.vars
795            .insert("$path".into(), tyv(Some(ty(RTk::Prim("string".into())))));
796        if let Some(TypeAst::Record { members, .. }) = ast {
797            for m in members {
798                if let MemberAst::Context { variable, ty, .. } = m {
799                    c.vars
800                        .insert(variable.clone(), tyv(try_resolve(&env, Some(ty))));
801                }
802            }
803        }
804        c
805    };
806    fn check_member_ast(
807        env: &Rc<Env>,
808        rep: &dyn Fn(&str, String),
809        is_bool_ty: &dyn Fn(&Ty) -> bool,
810        cx: &Ctx,
811        m: &MemberAst,
812    ) {
813        match m {
814            MemberAst::Value {
815                ty, dflt: Some(d), ..
816            } => {
817                check_expr(cx, d, try_resolve(env, Some(ty)).as_ref());
818            }
819            MemberAst::Derived { ty, expr, .. } => {
820                check_expr(cx, expr, try_resolve(env, ty.as_ref()).as_ref());
821            }
822            MemberAst::Assert { cond, tail, .. } => {
823                if !is_bool_ty(&require_val(
824                    cx,
825                    cond,
826                    infer(cx, cond),
827                    "as an assert condition",
828                )) {
829                    rep("E4001", "assert condition is not bool".into());
830                }
831                match tail {
832                    Some(Tail::Inline { template, .. }) => {
833                        for p in template {
834                            if let TPart::Expr(x) = p {
835                                infer(cx, x);
836                            }
837                        }
838                    }
839                    Some(Tail::Ref { args, .. }) => {
840                        for a in args {
841                            require_val(cx, a, infer(cx, a), "as a diagnostic argument");
842                        }
843                    }
844                    None => {}
845                }
846            }
847            MemberAst::When { cond, body, .. } => {
848                if !is_bool_ty(&require_val(
849                    cx,
850                    cond,
851                    infer(cx, cond),
852                    "as a when condition",
853                )) {
854                    rep("E4001", "when condition is not bool".into());
855                }
856                let c2 = apply_guards(cx, guards_of(cond, true));
857                for b in body {
858                    check_member_ast(env, rep, is_bool_ty, &c2, b);
859                }
860            }
861            _ => {}
862        }
863    }
864
865    let seen_recs: RefCell<HashSet<usize>> = RefCell::new(HashSet::new());
866    fn check_record_exprs(
867        env: &Rc<Env>,
868        rep: &dyn Fn(&str, String),
869        is_bool_ty: &dyn Fn(&Ty) -> bool,
870        rec_ctx: &dyn Fn(&Ctx, &RT, Option<&TypeAst>) -> Ctx,
871        seen: &RefCell<HashSet<usize>>,
872        rt: &RT,
873        cx: &Ctx,
874        ast: Option<&TypeAst>,
875    ) {
876        let RTk::Rec(rec) = &rt.k else { return };
877        let id = Rc::as_ptr(rt) as usize;
878        if seen.borrow().contains(&id) {
879            return;
880        }
881        seen.borrow_mut().insert(id);
882        let cx_r = rec_ctx(cx, rt, ast);
883        // member expressions and asserts check in their declaring module's
884        // scope (§8.3) — same rule the engine follows at evaluation
885        let cx_for = |menv: &Option<Rc<Env>>| -> Ctx {
886            match menv {
887                Some(m) if !Rc::ptr_eq(m, &cx_r.env) => cx_r.with_env(m.clone()),
888                _ => cx_r.clone(),
889            }
890        };
891        // D30/E4090: an embedded type's declared bounds must hold at this
892        // site — the container is the parent, the collection's key or index
893        // type is what $key ranges over (none for a direct member)
894        let check_embedding = |member_rt: &RT, member_name: &str, key_rt: Option<&RT>| {
895            let RTk::Rec(mrec) = &member_rt.k else { return };
896            let site = format!(
897                "{}.{member_name}",
898                rt.name.borrow().clone().unwrap_or_else(|| "record".into())
899            );
900            let who = member_rt
901                .name
902                .borrow()
903                .clone()
904                .unwrap_or_else(|| "the member type".into());
905            for (var, cty) in mrec.ctx_decls.borrow().iter() {
906                if var == "$parent" {
907                    let RTk::Ref(bound) = &cty.k else { continue };
908                    if !subsumes(env, rt, bound) {
909                        rep(
910                            "E4090",
911                            format!("embedding site {site} fails {who}'s $parent bound (§7.3)"),
912                        );
913                    }
914                } else if var == "$key" {
915                    match key_rt {
916                        None => rep("E4090", format!("embedding site {site} gives $key no meaning: {who} is a direct member, not a collection element (§7.3)")),
917                        Some(k) => {
918                            if !subsumes(env, k, cty) {
919                                rep("E4090", format!("embedding site {site} fails {who}'s $key bound (§7.3)"));
920                            }
921                        }
922                    }
923                }
924            }
925        };
926        let int_rt: RT = ty(RTk::Prim("int".into()));
927        let members = rec.members.borrow().clone();
928        for m in &members {
929            if m.kind == MKind::Der {
930                if let Some(x) = &m.expr {
931                    check_expr(&cx_for(&m.menv), x, m.ty.as_ref());
932                }
933            }
934            if m.kind == MKind::Dflt {
935                if let Some(d) = &m.dflt {
936                    check_expr(&cx_for(&m.menv), d, m.ty.as_ref());
937                }
938            }
939            let Some(mt) = &m.ty else { continue };
940            let nested: Option<(RT, Option<RT>)> = match &mt.k {
941                RTk::Rec(_) => Some((mt.clone(), None)),
942                RTk::Arr { elem, .. } if is_rec(elem) => Some((elem.clone(), Some(int_rt.clone()))),
943                RTk::Map { key, val } if is_rec(val) => Some((val.clone(), Some(key.clone()))),
944                _ => None,
945            };
946            if let Some((n, key_rt)) = nested {
947                check_embedding(&n, &m.name, key_rt.as_ref());
948                check_record_exprs(
949                    env,
950                    rep,
951                    is_bool_ty,
952                    rec_ctx,
953                    seen,
954                    &n,
955                    &cx_for(&m.menv),
956                    None,
957                );
958            }
959        }
960        let asserts = rec.asserts.borrow().clone();
961        for a in &asserts {
962            let m = if a.when {
963                MemberAst::When {
964                    cond: a.cond.clone(),
965                    body: a.body.clone(),
966                    loc: None,
967                }
968            } else {
969                MemberAst::Assert {
970                    name: a.name.clone(),
971                    cond: a.cond.clone(),
972                    tail: a.tail.clone(),
973                    loc: None,
974                }
975            };
976            check_member_ast(env, rep, is_bool_ty, &cx_for(&a.menv), &m);
977        }
978    }
979
980    for name in &type_order {
981        let Some(decl) = type_entry(name) else {
982            continue;
983        };
984        if !decl.params.is_empty() {
985            continue;
986        }
987        let Ok(rt) = env.resolve(&named(name), None) else {
988            continue;
989        };
990        check_record_exprs(
991            &env,
992            &rep,
993            &is_bool_ty,
994            &rec_ctx,
995            &seen_recs,
996            &rt,
997            &cx0,
998            Some(&decl.ast),
999        );
1000    }
1001    // D30/E4090 for $root: every record type owned (transitively) by an
1002    // evaluation root must have its declared $root bound met by the root's
1003    // own type — checked once per root declaration
1004    // §7.3: the root's own type gives $parent and $key no meaning — the root
1005    // has no owner and sits under no key — so a declaration of either on it
1006    // (directly, or on a union arm) is an error at the root
1007    fn check_root_type(rep: &dyn Fn(&str, String), root_name: &str, root_rt: &RT) {
1008        let arms: Vec<RT> = match &root_rt.k {
1009            RTk::Union(arms) => arms.clone(),
1010            _ => vec![root_rt.clone()],
1011        };
1012        for t in &arms {
1013            let RTk::Rec(r) = &t.k else { continue };
1014            let who = t.name.borrow().clone().unwrap_or_else(|| "its type".into());
1015            for (var, _) in r.ctx_decls.borrow().iter() {
1016                if var == "$parent" {
1017                    rep("E4090", format!("root {root_name} gives $parent no meaning: {who} is the evaluation root's own type (§7.3)"));
1018                } else if var == "$key" {
1019                    rep("E4090", format!("root {root_name} gives $key no meaning: {who} is the evaluation root's own type, not a collection element (§7.3)"));
1020                }
1021            }
1022        }
1023    }
1024    fn walk_root_bounds(
1025        env: &Rc<Env>,
1026        rep: &dyn Fn(&str, String),
1027        root_name: &str,
1028        root_rt: &RT,
1029        t: &RT,
1030        seen: &mut HashSet<usize>,
1031    ) {
1032        let id = Rc::as_ptr(t) as usize;
1033        if !seen.insert(id) {
1034            return;
1035        }
1036        match &t.k {
1037            RTk::Rec(r) => {
1038                for (var, cty) in r.ctx_decls.borrow().iter() {
1039                    if var != "$root" {
1040                        continue;
1041                    }
1042                    let RTk::Ref(bound) = &cty.k else { continue };
1043                    if !subsumes(env, root_rt, bound) {
1044                        rep(
1045                            "E4090",
1046                            format!(
1047                                "root {root_name} fails {}'s $root bound (§7.3)",
1048                                t.name
1049                                    .borrow()
1050                                    .clone()
1051                                    .unwrap_or_else(|| "a member type".into())
1052                            ),
1053                        );
1054                    }
1055                }
1056                for m in rec_members(t) {
1057                    if let Some(mt) = &m.ty {
1058                        walk_root_bounds(env, rep, root_name, root_rt, mt, seen);
1059                    }
1060                }
1061            }
1062            RTk::Arr { elem, .. } => walk_root_bounds(env, rep, root_name, root_rt, elem, seen),
1063            RTk::Map { val, .. } => walk_root_bounds(env, rep, root_name, root_rt, val, seen),
1064            RTk::Union(arms) | RTk::IsectN(arms) => arms
1065                .iter()
1066                .for_each(|a| walk_root_bounds(env, rep, root_name, root_rt, a, seen)),
1067            RTk::Pred { base, .. } => walk_root_bounds(env, rep, root_name, root_rt, base, seen),
1068            _ => {}
1069        }
1070    }
1071    for d in decls {
1072        cur_decl.set(d.loc);
1073        let (name, ty_ast) = match &d.body {
1074            DeclBody::Output { name, ty, .. } | DeclBody::Input { name, ty, .. } => (name, ty),
1075            _ => continue,
1076        };
1077        if let Some(rt) = try_resolve(&env, Some(ty_ast)) {
1078            check_root_type(&rep, name, &rt);
1079            walk_root_bounds(&env, &rep, name, &rt, &rt, &mut HashSet::new());
1080        }
1081    }
1082    for d in decls {
1083        cur_decl.set(d.loc);
1084        match &d.body {
1085            DeclBody::Const { name, ty, expr } => {
1086                let exp = ty
1087                    .as_ref()
1088                    .and_then(|t| resolve_or_report(Some(t), &format!("const {name}")));
1089                check_expr(&cx0, expr, exp.as_ref());
1090            }
1091            DeclBody::Func {
1092                name,
1093                params,
1094                ret,
1095                body,
1096            } => {
1097                let mut cx_f = cx0.child();
1098                for p in params {
1099                    cx_f.vars.insert(
1100                        p.name.clone(),
1101                        tyv(resolve_or_report(p.ty.as_ref(), &format!("func {name}"))),
1102                    );
1103                }
1104                let exp = ret
1105                    .as_ref()
1106                    .and_then(|t| resolve_or_report(Some(t), &format!("func {name}")));
1107                check_expr(&cx_f, body, exp.as_ref());
1108            }
1109            DeclBody::Output { name, ty, expr } => {
1110                let exp = resolve_or_report(Some(ty), &format!("output {name}"));
1111                check_expr(&cx0, expr, exp.as_ref());
1112            }
1113            DeclBody::Input {
1114                name,
1115                ty,
1116                fallback: Some(f),
1117            } => {
1118                let exp = resolve_or_report(Some(ty), &format!("input {name}"));
1119                check_expr(&cx0, f, exp.as_ref());
1120            }
1121            DeclBody::Input {
1122                name,
1123                ty,
1124                fallback: None,
1125            } => {
1126                resolve_or_report(Some(ty), &format!("input {name}"));
1127            }
1128            DeclBody::Diagnostic {
1129                params, template, ..
1130            } => {
1131                let mut cx_d = cx0.child();
1132                for p in params {
1133                    cx_d.vars
1134                        .insert(p.name.clone(), tyv(try_resolve(&env, p.ty.as_ref())));
1135                }
1136                for p in template {
1137                    if let TPart::Expr(x) = p {
1138                        infer(&cx_d, x);
1139                    }
1140                }
1141            }
1142            DeclBody::Unit {
1143                name,
1144                factor: Some(f),
1145                ..
1146            } => {
1147                if let Some(bad) = const_violation(f) {
1148                    rep(
1149                        "E4021",
1150                        format!("non-constant unit factor for {name}: {bad} (§3.16)"),
1151                    );
1152                }
1153            }
1154            _ => {}
1155        }
1156    }
1157    *env.const_diag_sink.borrow_mut() = None;
1158    let result = out.borrow().clone();
1159    result
1160}
1161
1162#[allow(dead_code)]
1163fn _unused(_: &HashMap<String, String>) {}