Skip to main content

decl_lang/
infer.rs

1//! Expression-level static analysis — a port of the reference
2//! implementation's infer.ts: type inference, assignability (§3.18,
3//! strict S ⊑ T), the absence discipline (§4.10) with its two narrowing
4//! rules, and the `match` static checks (§4.7). Inference is
5//! conservative: a form whose type cannot be determined yields `unknown`
6//! (rt None) and suppresses downstream judgments rather than guessing.
7use crate::ast::*;
8use crate::semantics::*;
9use crate::subsume::subsumes;
10use num_bigint::BigInt;
11use std::cell::RefCell;
12use std::collections::{HashMap, HashSet};
13use std::rc::Rc;
14
15#[derive(Clone)]
16pub struct Ty {
17    pub rt: Option<RT>,
18    pub abs: bool,
19}
20pub fn unk() -> Ty {
21    Ty {
22        rt: None,
23        abs: false,
24    }
25}
26pub fn tyv(rt: Option<RT>) -> Ty {
27    Ty { rt, abs: false }
28}
29pub fn prim(name: &str) -> RT {
30    ty(RTk::Prim(name.to_string()))
31}
32fn bool_ty() -> Ty {
33    tyv(Some(prim("bool")))
34}
35
36pub type Report = Rc<dyn Fn(&str, String)>;
37
38#[derive(Clone)]
39pub struct Ctx {
40    pub env: Rc<Env>,
41    pub report: Report,
42    pub vars: HashMap<String, Ty>,
43    pub present: HashSet<String>,
44    pub nonnull: HashSet<String>,
45    pub const_memo: Rc<RefCell<HashMap<String, Ty>>>,
46    /// the expression under inference (shared by child contexts): what a report is anchored to
47    pub pos: Rc<RefCell<Option<Rc<Expr>>>>,
48    /// Phase 6 foundations: every inferred node's type, and every name's resolution (the language server's tables)
49    pub record: Option<Rc<dyn Fn(&Rc<Expr>, &Ty)>>,
50    pub resolve_hook: Option<Rc<dyn Fn(&Rc<Expr>, Option<Target>)>>,
51}
52
53/// what a name denotes: the declaration behind it, imports followed to their module
54#[derive(Clone)]
55pub struct Target {
56    pub kind: &'static str, // var | const | func | output | input | namespace | type | diagnostic | export
57    pub env: Option<Rc<Env>>,
58    pub name: String,
59}
60pub fn resolve_name(cx: &Ctx, name: &str) -> Option<Target> {
61    if cx.vars.contains_key(name) {
62        return Some(Target {
63            kind: "var",
64            env: None,
65            name: name.to_string(),
66        });
67    }
68    resolve_in(&cx.env, name)
69}
70pub fn resolve_in(env: &Rc<Env>, name: &str) -> Option<Target> {
71    let t = |kind: &'static str| {
72        Some(Target {
73            kind,
74            env: Some(env.clone()),
75            name: name.to_string(),
76        })
77    };
78    if env.consts.borrow().contains_key(name) {
79        return t("const");
80    }
81    if env.funcs.borrow().contains_key(name) {
82        return t("func");
83    }
84    if env.outputs.borrow().iter().any(|(o, _, _)| o == name) {
85        return t("output");
86    }
87    if env.inputs.borrow().contains_key(name) {
88        return t("input");
89    }
90    let im = env.imports.borrow().get(name).cloned();
91    if let Some(im) = im {
92        return resolve_in(&im.env, &im.name).or(Some(Target {
93            kind: "export",
94            env: Some(im.env.clone()),
95            name: im.name.clone(),
96        }));
97    }
98    if env.namespaces.borrow().contains_key(name) {
99        return t("namespace");
100    }
101    if env.type_asts.borrow().contains_key(name) {
102        return t("type");
103    }
104    if env.diags.borrow().contains_key(name) {
105        return t("diagnostic");
106    }
107    None
108}
109impl Ctx {
110    pub fn report(&self, code: &str, msg: String) {
111        (self.report)(code, msg)
112    }
113    pub fn child(&self) -> Ctx {
114        self.clone()
115    }
116    pub fn with_env(&self, env: Rc<Env>) -> Ctx {
117        let mut c = self.clone();
118        c.env = env;
119        c
120    }
121}
122pub fn make_ctx(env: Rc<Env>, report: Report) -> Ctx {
123    Ctx {
124        env,
125        report,
126        vars: HashMap::new(),
127        present: HashSet::new(),
128        nonnull: HashSet::new(),
129        const_memo: Rc::new(RefCell::new(HashMap::new())),
130        pos: Rc::new(RefCell::new(None)),
131        record: None,
132        resolve_hook: None,
133    }
134}
135
136// ---------------- JS-faithful helpers ----------------
137pub fn js_typeof(v: &Value) -> &'static str {
138    match v {
139        Value::Bool(_) => "boolean",
140        Value::Int(_) => "bigint",
141        Value::Float(_) => "number",
142        Value::Str(_) => "string",
143        _ => "object",
144    }
145}
146pub fn js_str(v: &Value) -> String {
147    match v {
148        Value::Null => "null".into(),
149        Value::Bool(b) => {
150            if *b {
151                "true".into()
152            } else {
153                "false".into()
154            }
155        }
156        Value::Int(i) => i.to_string(),
157        Value::Float(f) => js_num_str(*f),
158        Value::Str(s) => s.clone(),
159        other => format!("{other:?}"),
160    }
161}
162pub fn tag(rt: &RT) -> &'static str {
163    match &rt.k {
164        RTk::Prim(_) => "prim",
165        RTk::Lit(_) => "lit",
166        RTk::Range { .. } => "range",
167        RTk::Pattern { .. } => "pattern",
168        RTk::Arr { .. } => "arr",
169        RTk::Map { .. } => "map",
170        RTk::Union(_) => "union",
171        RTk::IsectN(_) => "isectN",
172        RTk::Rec(_) => "rec",
173        RTk::Pred { .. } => "pred",
174        RTk::Ref(_) => "ref",
175        RTk::Quantity(_) => "quantity",
176        RTk::Func { .. } => "func",
177        RTk::Any => "any",
178    }
179}
180/// a function's unknown result is carried as `any`
181fn ret_of(rt: &RT) -> Option<RT> {
182    if matches!(rt.k, RTk::Any) {
183        None
184    } else {
185        Some(rt.clone())
186    }
187}
188pub fn member_ty(m: &Member) -> Option<RT> {
189    match &m.conj {
190        Some(c) => Some(ty(RTk::IsectN(c.clone()))),
191        None => m.ty.clone(),
192    }
193}
194fn find_member<'a>(members: &'a [Member], name: &str) -> Option<&'a Member> {
195    members.iter().find(|m| m.name == name)
196}
197
198// ---------------- type utilities ----------------
199fn is_null_lit(t: &RT) -> bool {
200    matches!(&t.k, RTk::Lit(Value::Null)) || matches!(&t.k, RTk::Prim(n) if n == "null")
201}
202pub fn has_null(rt: Option<&RT>) -> bool {
203    match rt {
204        None => false,
205        Some(t) => {
206            is_null_lit(t)
207                || matches!(&t.k, RTk::Union(arms) if arms.iter().any(|a| has_null(Some(a))))
208        }
209    }
210}
211fn strip_null(rt: &RT) -> RT {
212    if let RTk::Union(arms) = &rt.k {
213        let kept: Vec<RT> = arms.iter().filter(|a| !is_null_lit(a)).cloned().collect();
214        return if kept.len() == 1 {
215            kept[0].clone()
216        } else {
217            ty(RTk::Union(kept))
218        };
219    }
220    rt.clone()
221}
222fn same_rt(a: &RT, b: &RT) -> bool {
223    if Rc::ptr_eq(a, b) {
224        return true;
225    }
226    match (&a.k, &b.k) {
227        (RTk::Prim(x), RTk::Prim(y)) => x == y,
228        (RTk::Lit(x), RTk::Lit(y)) => js_typeof(x) == js_typeof(y) && value_eq(x, y),
229        _ => false,
230    }
231}
232pub fn mk_union(arms: Vec<Option<RT>>) -> Option<RT> {
233    if arms.iter().any(|a| a.is_none()) {
234        return None;
235    }
236    let mut flat: Vec<RT> = vec![];
237    for a in arms.into_iter().flatten() {
238        match &a.k {
239            RTk::Union(xs) => flat.extend(xs.iter().cloned()),
240            _ => flat.push(a),
241        }
242    }
243    let mut uniq: Vec<RT> = vec![];
244    for a in flat {
245        if !uniq.iter().any(|b| same_rt(&a, b)) {
246            uniq.push(a);
247        }
248    }
249    Some(if uniq.len() == 1 {
250        uniq[0].clone()
251    } else {
252        ty(RTk::Union(uniq))
253    })
254}
255pub fn num_kind(rt: Option<&RT>) -> Option<String> {
256    let rt = rt?;
257    match &rt.k {
258        RTk::Prim(n) => {
259            if ["int", "float", "string", "bool"].contains(&n.as_str()) {
260                Some(n.clone())
261            } else {
262                None
263            }
264        }
265        RTk::Lit(v) => match v {
266            Value::Bool(_) => Some("bool".into()),
267            Value::Int(_) => Some("int".into()),
268            Value::Float(_) => Some("float".into()),
269            Value::Str(_) => Some("string".into()),
270            _ => None,
271        },
272        RTk::Range { base, .. } => Some(base.clone()),
273        RTk::Pattern { .. } => Some("string".into()),
274        RTk::Pred { base, .. } => num_kind(Some(base)),
275        RTk::Quantity(_) => Some("quantity".into()),
276        RTk::Union(arms) => {
277            let ks: Vec<Option<String>> = arms.iter().map(|a| num_kind(Some(a))).collect();
278            match ks.first() {
279                Some(Some(k0)) if ks.iter().all(|k| k.as_ref() == Some(k0)) => Some(k0.clone()),
280                _ => None,
281            }
282        }
283        _ => None,
284    }
285}
286fn is_boolish(rt: Option<&RT>) -> bool {
287    rt.is_none() || num_kind(rt).as_deref() == Some("bool")
288}
289/// structural view: unwrap ref/pred and select the arm of an intersection
290/// that has the wanted shape (merged `&` members carry conj arms)
291fn arm_of(rt: Option<&RT>, t: &str) -> Option<RT> {
292    let rt = rt?;
293    match &rt.k {
294        RTk::Ref(target) if t != "ref" => return arm_of(Some(target), t),
295        RTk::Pred { base, .. } => return arm_of(Some(base), t),
296        _ => {}
297    }
298    if tag(rt) == t {
299        return Some(rt.clone());
300    }
301    if let RTk::IsectN(arms) = &rt.k {
302        for x in arms {
303            if let Some(v) = arm_of(Some(x), t) {
304                return Some(v);
305            }
306        }
307    }
308    if let RTk::Union(arms) = &rt.k {
309        let sub: Vec<RT> = arms.iter().filter_map(|x| arm_of(Some(x), t)).collect();
310        if sub.len() == arms.len() && !sub.is_empty() {
311            if t == "arr" {
312                let elems: Vec<RT> = sub
313                    .iter()
314                    .filter_map(|a| {
315                        if let RTk::Arr { elem, .. } = &a.k {
316                            Some(elem.clone())
317                        } else {
318                            None
319                        }
320                    })
321                    .collect();
322                return Some(ty(RTk::Arr {
323                    elem: ty(RTk::Union(elems)),
324                    lo: None,
325                    hi: None,
326                }));
327            }
328            return Some(sub[0].clone());
329        }
330    }
331    None
332}
333
334// ---------------- navigation paths & narrowing ----------------
335pub fn path_key(e: &Expr) -> Option<String> {
336    match e {
337        Expr::Name(n) => Some(n.clone()),
338        Expr::Ctx(n) => Some(n.clone()),
339        Expr::Paren(x) => path_key(x),
340        Expr::Member { x, name, safe } => {
341            if *safe {
342                return None;
343            }
344            path_key(x).map(|b| format!("{b}.{name}"))
345        }
346        Expr::Index { x, i } => {
347            let b = path_key(x)?;
348            match &**i {
349                Expr::Lit(v) => Some(format!("{b}[{}]", js_str(v))),
350                Expr::Name(n) => Some(format!("{b}[{n}]")),
351                _ => None,
352            }
353        }
354        _ => None,
355    }
356}
357#[derive(Default)]
358pub struct Guards {
359    pub present: Vec<String>,
360    pub nonnull: Vec<String>,
361}
362fn merge(mut a: Guards, b: Guards) -> Guards {
363    a.present.extend(b.present);
364    a.nonnull.extend(b.nonnull);
365    a
366}
367pub fn guards_of(e: &Expr, polarity: bool) -> Guards {
368    match e {
369        Expr::Paren(x) => guards_of(x, polarity),
370        Expr::Un { op, x } => {
371            if op == "!" {
372                guards_of(x, !polarity)
373            } else {
374                Guards::default()
375            }
376        }
377        Expr::Bin { op, l, r } => {
378            if op == "&&" && polarity {
379                return merge(guards_of(l, true), guards_of(r, true));
380            }
381            if op == "||" && !polarity {
382                return merge(guards_of(l, false), guards_of(r, false));
383            }
384            if op == "in" && polarity {
385                let Some(b) = path_key(r) else {
386                    return Guards::default();
387                };
388                return match &**l {
389                    Expr::Lit(Value::Str(s)) => Guards {
390                        present: vec![format!("{b}.{s}"), format!("{b}[{s}]")],
391                        nonnull: vec![],
392                    },
393                    Expr::Name(n) => Guards {
394                        present: vec![format!("{b}[{n}]")],
395                        nonnull: vec![],
396                    },
397                    _ => Guards::default(),
398                };
399            }
400            let null_side = if matches!(&**l, Expr::Lit(Value::Null)) {
401                Some(r)
402            } else if matches!(&**r, Expr::Lit(Value::Null)) {
403                Some(l)
404            } else {
405                None
406            };
407            if let Some(side) = null_side {
408                if let Some(p) = path_key(side) {
409                    if (op == "!=" && polarity) || (op == "==" && !polarity) {
410                        return Guards {
411                            present: vec![],
412                            nonnull: vec![p],
413                        };
414                    }
415                }
416            }
417            Guards::default()
418        }
419        _ => Guards::default(),
420    }
421}
422/// is a name already taken here? (locals or the module namespace — the
423/// no-shadowing rule E3019 spans both)
424fn name_bound(cx: &Ctx, n: &str) -> bool {
425    cx.vars.contains_key(n)
426        || cx.env.consts.borrow().contains_key(n)
427        || cx.env.funcs.borrow().contains_key(n)
428        || cx.env.type_asts.borrow().contains_key(n)
429        || cx.env.inputs.borrow().contains_key(n)
430        || cx.env.outputs.borrow().iter().any(|(o, _, _)| o == n)
431}
432pub fn apply_guards(cx: &Ctx, g: Guards) -> Ctx {
433    let mut c2 = cx.child();
434    c2.present.extend(g.present);
435    c2.nonnull.extend(g.nonnull);
436    c2
437}
438
439// ---------------- stdlib signatures (arity + result) ----------------
440/// the return type of a std function, by shape
441#[derive(Clone, Copy)]
442pub enum StdRet {
443    Unknown,
444    Int,
445    Bool,
446    Str,
447    Float,
448    ArrStr,
449    PredFn,
450}
451/// the std functions (§13.1: names not listed do not exist): name, arity, return
452pub const STD: &[(&str, usize, StdRet)] = &[
453    ("array.count", 1, StdRet::Int),
454    ("array.all", 2, StdRet::Bool),
455    ("array.any", 2, StdRet::Bool),
456    ("array.filter", 2, StdRet::Unknown),
457    ("array.all_distinct", 1, StdRet::Bool),
458    ("array.sum", 1, StdRet::Unknown),
459    ("array.fold", 3, StdRet::Unknown),
460    ("map.keys", 1, StdRet::ArrStr),
461    ("map.values", 1, StdRet::Unknown),
462    ("string.length", 1, StdRet::Int),
463    ("string.of", 1, StdRet::Str),
464    ("string.join", 2, StdRet::Str),
465    ("string.starts_with", 2, StdRet::Bool),
466    ("string.ends_with", 2, StdRet::Bool),
467    ("string.contains", 2, StdRet::Bool),
468    ("string.split", 2, StdRet::ArrStr),
469    ("map.entries", 1, StdRet::Unknown),
470    ("ref.path", 1, StdRet::Str),
471    ("math.abs", 1, StdRet::Unknown),
472    ("math.min", 2, StdRet::Unknown),
473    ("math.max", 2, StdRet::Unknown),
474    ("math.clog2", 1, StdRet::Int),
475    ("math.floor", 1, StdRet::Int),
476    ("math.ceil", 1, StdRet::Int),
477    ("math.round", 1, StdRet::Int),
478    ("int.of", 1, StdRet::Int),
479    ("int.at_least", 1, StdRet::PredFn),
480    ("int.at_most", 1, StdRet::PredFn),
481    ("float.of", 1, StdRet::Float),
482    ("object.merge", 2, StdRet::Unknown),
483];
484/// the std function names, in table order (completion)
485pub fn std_names() -> impl Iterator<Item = &'static str> {
486    STD.iter().map(|e| e.0)
487}
488fn std_sig(name: &str) -> Option<(usize, Option<RT>)> {
489    let e = STD.iter().find(|e| e.0 == name)?;
490    let ret = match e.2 {
491        StdRet::Unknown => None,
492        StdRet::Int => Some(prim("int")),
493        StdRet::Bool => Some(prim("bool")),
494        StdRet::Str => Some(prim("string")),
495        StdRet::Float => Some(prim("float")),
496        StdRet::ArrStr => Some(ty(RTk::Arr {
497            elem: prim("string"),
498            lo: None,
499            hi: None,
500        })),
501        StdRet::PredFn => Some(ty(RTk::Func {
502            params: vec![prim("int")],
503            ret: prim("bool"),
504        })),
505    };
506    Some((e.1, ret))
507}
508
509// ---------------- type text ----------------
510// the static type as inference sees it, spelled in the language's own
511// type syntax where it has one (`:type` in the REPL, hover in the editor)
512pub fn type_text(rt: Option<&RT>) -> String {
513    let Some(rt) = rt else {
514        return "unknown".into();
515    };
516    let lit = |v: &Value| match v {
517        Value::Str(s) => crate::semantics::json_str(s),
518        other => js_str(other),
519    };
520    let is_null_arm = |a: &RT| {
521        matches!(&a.k, RTk::Prim(n) if n == "null") || matches!(&a.k, RTk::Lit(Value::Null))
522    };
523    match &rt.k {
524        RTk::Any => "any".into(),
525        RTk::Prim(n) => n.clone(),
526        RTk::Lit(v) => lit(v),
527        RTk::Range { lo, hi, excl, .. } => {
528            format!("{}..{}{}", lit(lo), if *excl { "<" } else { "" }, lit(hi))
529        }
530        RTk::Pattern { src, .. } => format!("/{src}/"),
531        RTk::Quantity(dim) => format!("quantity<{dim}>"),
532        RTk::Ref(t) => format!("ref<{}>", type_text(Some(t))),
533        RTk::Map { key, val } => format!("map<{}, {}>", type_text(Some(key)), type_text(Some(val))),
534        RTk::Arr { elem, lo, hi } => {
535            let b = if lo.is_some() || hi.is_some() {
536                format!(
537                    "[{}..{}]",
538                    lo.map(|x| x.to_string()).unwrap_or_default(),
539                    hi.map(|x| x.to_string()).unwrap_or_default()
540                )
541            } else {
542                "[]".into()
543            };
544            let e = type_text(Some(elem));
545            // a compound element type is parenthesized (`(1 | 2)[]`, `(1..8)[]`), as the grammar's paren_type spells it
546            let wrap = matches!(
547                &elem.k,
548                RTk::Union(_) | RTk::Func { .. } | RTk::Pred { .. } | RTk::Range { .. }
549            );
550            format!("{}{b}", if wrap { format!("({e})") } else { e })
551        }
552        RTk::Union(arms) => {
553            let nn: Vec<&RT> = arms.iter().filter(|a| !is_null_arm(a)).collect();
554            if nn.len() + 1 == arms.len() && nn.len() == 1 {
555                return format!("{}?", type_text(Some(nn[0])));
556            }
557            arms.iter()
558                .map(|a| type_text(Some(a)))
559                .collect::<Vec<_>>()
560                .join(" | ")
561        }
562        RTk::Pred { base, .. } => format!("{} where …", type_text(Some(base))),
563        RTk::Func { params, ret } => format!(
564            "({}) => {}",
565            params
566                .iter()
567                .map(|p| type_text(Some(p)))
568                .collect::<Vec<_>>()
569                .join(", "),
570            type_text(Some(ret))
571        ),
572        RTk::Rec(r) => {
573            if let Some(n) = rt.name.borrow().as_ref() {
574                if !n.starts_with('{') {
575                    return n.clone();
576                }
577            }
578            let ms: Vec<String> = r
579                .members
580                .borrow()
581                .iter()
582                .map(|m| {
583                    format!(
584                        "{}{}{}{}",
585                        m.name,
586                        if m.kind == MKind::Opt { "?" } else { "" },
587                        m.ty.as_ref()
588                            .map(|t| format!(": {}", type_text(Some(t))))
589                            .unwrap_or_default(),
590                        if matches!(m.kind, MKind::Der | MKind::Dflt) {
591                            " = …"
592                        } else {
593                            ""
594                        }
595                    )
596                })
597                .collect();
598            let open = if r.open.get() {
599                if ms.is_empty() {
600                    "..."
601                } else {
602                    ", ..."
603                }
604            } else {
605                ""
606            };
607            format!("{{ {}{open} }}", ms.join(", "))
608        }
609        RTk::IsectN(_) => "?".into(),
610    }
611}
612pub fn std_path(e: &Expr) -> Option<String> {
613    match e {
614        Expr::Member {
615            x,
616            name,
617            safe: false,
618        } => {
619            let b = std_path(x)?;
620            Some(if b.is_empty() {
621                name.clone()
622            } else {
623                format!("{b}.{name}")
624            })
625        }
626        Expr::Name(n) if n == "std" => Some(String::new()),
627        _ => None,
628    }
629}
630
631// ---------------- the judgment ----------------
632pub fn try_resolve(env: &Rc<Env>, ast: Option<&TypeAst>) -> Option<RT> {
633    env.resolve(ast?, None).ok()
634}
635pub fn named(name: &str) -> TypeAst {
636    TypeAst::Named {
637        name: name.to_string(),
638        args: vec![],
639        preds: None,
640        ext: None,
641        loc: None,
642    }
643}
644
645pub fn require_val(cx: &Ctx, e: &Expr, ty: Ty, what: &str) -> Ty {
646    if ty.abs {
647        let k = path_key(e);
648        if !k.map(|k| cx.present.contains(&k)).unwrap_or(false) {
649            cx.report(
650                "E4050",
651                format!("maybe-absent expression consumed {what} (use ?. / ?? or an `in` guard)"),
652            );
653        }
654    }
655    ty
656}
657
658pub fn infer(cx: &Ctx, e: &Rc<Expr>) -> Ty {
659    let prev = cx.pos.replace(Some(e.clone()));
660    let t = infer0(cx, e);
661    if let Some(r) = &cx.record {
662        r(e, &t);
663    }
664    if let Some(h) = &cx.resolve_hook {
665        if let Expr::Name(n) = &**e {
666            h(e, resolve_name(cx, n));
667        }
668    }
669    *cx.pos.borrow_mut() = prev;
670    t
671}
672fn infer0(cx: &Ctx, e: &Rc<Expr>) -> Ty {
673    match &**e {
674        Expr::Lit(v) => tyv(Some(ty(RTk::Lit(v.clone())))),
675        Expr::Pattern(src) => match pattern_error(src) {
676            Some(bad) => {
677                cx.report("E4119", format!("malformed pattern /{src}/: {bad}"));
678                unk()
679            }
680            None => match compile_pattern(src) {
681                Ok(re) => tyv(Some(ty(RTk::Pattern {
682                    src: src.clone(),
683                    re,
684                }))),
685                Err(_) => unk(),
686            },
687        },
688        Expr::UnitLit { unit, .. } => match cx.env.unit_info(unit) {
689            Ok((key, _)) => tyv(Some(ty(RTk::Quantity(key)))),
690            Err(msg) => {
691                cx.report("E4073", msg);
692                unk()
693            }
694        },
695        Expr::Template(parts) => {
696            for p in parts {
697                if let TPart::Expr(x) = p {
698                    require_val(cx, x, infer(cx, x), "in a template");
699                }
700            }
701            tyv(Some(prim("string")))
702        }
703        Expr::Name(name) => {
704            if let Some(t) = cx.vars.get(name) {
705                return t.clone();
706            }
707            let env = &cx.env;
708            if env.consts.borrow().contains_key(name) {
709                return const_ty(cx, name);
710            }
711            if env.funcs.borrow().contains_key(name) {
712                return tyv(Some(func_rt(cx, name)));
713            }
714            if name == "std" {
715                return unk();
716            }
717            let out = env
718                .outputs
719                .borrow()
720                .iter()
721                .find(|(o, _, _)| o == name)
722                .map(|(_, t, _)| t.clone());
723            if let Some(t) = out {
724                return tyv(try_resolve(env, Some(&t)));
725            }
726            let inp = env.inputs.borrow().get(name).map(|(t, _)| t.clone());
727            if let Some(t) = inp {
728                return tyv(try_resolve(env, Some(&t)));
729            }
730            let im = env.imports.borrow().get(name).cloned();
731            if let Some(im) = im {
732                return imported_ty(cx, &im);
733            }
734            if env.namespaces.borrow().contains_key(name) {
735                cx.report("E3008", format!("namespace name {name} used as a value"));
736                return unk();
737            }
738            if env.type_asts.borrow().contains_key(name) {
739                cx.report(
740                    "E3008",
741                    format!("type/namespace name {name} used as a value"),
742                );
743                return unk();
744            }
745            cx.report("E3003", format!("unknown name {name}"));
746            unk()
747        }
748        Expr::Ctx(n) => cx.vars.get(n).cloned().unwrap_or_else(unk),
749        Expr::Referrers { ty: tn, .. } => {
750            let rt = try_resolve(&cx.env, Some(&named(tn)));
751            match &rt {
752                None => cx.report("E4091", format!("$referrers: unknown record type {tn}")),
753                Some(r) if !is_rec(r) => {
754                    cx.report("E4091", format!("$referrers: {tn} is not a record type"))
755                }
756                _ => {}
757            }
758            tyv(rt.filter(is_rec).map(|r| {
759                ty(RTk::Arr {
760                    elem: ty(RTk::Ref(r)),
761                    lo: None,
762                    hi: None,
763                })
764            }))
765        }
766        Expr::Obj(entries) => {
767            for (_, v) in entries {
768                require_val(cx, v, infer(cx, v), "as a construction member");
769            }
770            unk() // literals are typed by their checked position (§3.18)
771        }
772        Expr::Arr(items) => {
773            let ts: Vec<Option<RT>> = items
774                .iter()
775                .map(|(spread, x)| {
776                    let t = require_val(cx, x, infer(cx, x), "as an array element");
777                    if *spread {
778                        t.rt.and_then(|r| {
779                            if let RTk::Arr { elem, .. } = &r.k {
780                                Some(elem.clone())
781                            } else {
782                                None
783                            }
784                        })
785                    } else {
786                        t.rt
787                    }
788                })
789                .collect();
790            tyv(mk_union(ts).map(|elem| {
791                ty(RTk::Arr {
792                    elem,
793                    lo: None,
794                    hi: None,
795                })
796            }))
797        }
798        Expr::Comp { head, clauses } => {
799            let c2 = bind_clauses(cx, clauses);
800            let h = require_val(&c2, head, infer(&c2, head), "as a comprehension element");
801            tyv(h.rt.map(|elem| {
802                ty(RTk::Arr {
803                    elem,
804                    lo: None,
805                    hi: None,
806                })
807            }))
808        }
809        Expr::MapComp { key, val, clauses } => {
810            let c2 = bind_clauses(cx, clauses);
811            let k = require_val(&c2, key, infer(&c2, key), "as a map key");
812            if k.rt.is_some() && num_kind(k.rt.as_ref()).as_deref() != Some("string") {
813                cx.report("E4001", "map-comprehension key is not a string".into());
814            }
815            let v = require_val(&c2, val, infer(&c2, val), "as a map value");
816            tyv(v.rt.map(|val| {
817                ty(RTk::Map {
818                    key: prim("string"),
819                    val,
820                })
821            }))
822        }
823        Expr::Bin { .. } => infer_bin(cx, e),
824        Expr::Un { op, x } => {
825            let t = require_val(cx, x, infer(cx, x), &format!("as `{op}` operand"));
826            if op == "!" {
827                if t.rt.is_some() && !is_boolish(t.rt.as_ref()) {
828                    cx.report("E4071", "`!` on a non-bool operand".into());
829                }
830                return bool_ty();
831            }
832            if op == "~" {
833                if t.rt.is_some() && num_kind(t.rt.as_ref()).as_deref() != Some("int") {
834                    cx.report("E4071", "`~` on a non-int operand".into());
835                }
836                return tyv(Some(prim("int")));
837            }
838            let k = num_kind(t.rt.as_ref());
839            if t.rt.is_some()
840                && !matches!(k.as_deref(), Some("int") | Some("float") | Some("quantity"))
841            {
842                cx.report("E4071", "unary `-` on a non-numeric operand".into());
843            }
844            tyv(match k.as_deref() {
845                Some("int") => Some(prim("int")),
846                Some("float") => Some(prim("float")),
847                _ => None,
848            })
849        }
850        Expr::Paren(x) => infer(cx, x),
851        Expr::If { c, t, f } => {
852            let ct = require_val(cx, c, infer(cx, c), "as a condition");
853            if ct.rt.is_some() && !is_boolish(ct.rt.as_ref()) {
854                cx.report("E4001", "`if` condition is not bool".into());
855            }
856            let tt = infer(&apply_guards(cx, guards_of(c, true)), t);
857            let ft = infer(&apply_guards(cx, guards_of(c, false)), f);
858            Ty {
859                rt: mk_union(vec![tt.rt, ft.rt]),
860                abs: tt.abs || ft.abs,
861            }
862        }
863        Expr::Lambda { params, body } => {
864            let mut c2 = cx.child();
865            for p in params {
866                if name_bound(&c2, p) {
867                    cx.report(
868                        "E3019",
869                        format!("lambda parameter {p} shadows an enclosing name"),
870                    );
871                }
872                c2.vars.insert(p.clone(), unk());
873            }
874            infer(&c2, body);
875            unk()
876        }
877        Expr::Call { .. } => infer_call(cx, e),
878        Expr::Member { .. } => infer_member(cx, e),
879        Expr::Index { x, .. } => {
880            let b = require_val(cx, x, infer(cx, x), "for indexing");
881            index_core(cx, b, e)
882        }
883        Expr::With { base, patch } => {
884            let b = require_val(cx, base, infer(cx, base), "as `with` base");
885            let brt = match &b.rt {
886                Some(r) => match &r.k {
887                    RTk::Ref(t) => Some(t.clone()),
888                    _ => Some(r.clone()),
889                },
890                None => None,
891            };
892            if let Some(r) = &brt {
893                if !is_rec(r) {
894                    cx.report("E4080", "`with` on a non-record base".into());
895                    return unk();
896                }
897            }
898            if let (Expr::Obj(entries), Some(r)) = (&**patch, &brt) {
899                let RTk::Rec(rec) = &r.k else { unreachable!() };
900                let members = rec.members.borrow();
901                for (k, _) in entries {
902                    match find_member(&members, k) {
903                        None if !rec.open.get() => {
904                            cx.report("E4080", format!("`with` updates unknown member {k}"))
905                        }
906                        Some(m) if m.kind == MKind::Der => {
907                            cx.report("E4080", format!("`with` updates derived member {k}"))
908                        }
909                        _ => {}
910                    }
911                }
912            }
913            if let Expr::Obj(entries) = &**patch {
914                for (_, v) in entries {
915                    require_val(cx, v, infer(cx, v), "as a `with` update");
916                }
917            } else {
918                infer(cx, patch);
919            }
920            tyv(brt)
921        }
922        Expr::Match { .. } => infer_match(cx, e, None),
923    }
924}
925
926fn bind_clauses(cx: &Ctx, clauses: &[ForClause]) -> Ctx {
927    let mut c2 = cx.child();
928    for cl in clauses {
929        let vt = iter_var_ty(&c2, &cl.iter);
930        if name_bound(&c2, &cl.v) {
931            cx.report(
932                "E3019",
933                format!("comprehension variable {} shadows an enclosing name", cl.v),
934            );
935        }
936        c2.vars.insert(cl.v.clone(), vt);
937        for f in &cl.filters {
938            require_val(&c2, f, infer(&c2, f), "as a filter");
939            c2 = apply_guards(&c2, guards_of(f, true));
940        }
941    }
942    c2
943}
944
945fn iter_var_ty(cx: &Ctx, it: &Rc<Expr>) -> Ty {
946    let t = require_val(cx, it, infer(cx, it), "as an iterable");
947    if let Expr::Bin { op, l, r } = &**it {
948        if op == ".." || op == "..<" {
949            let lo = if let Expr::Lit(v) = &**l {
950                Some(v.clone())
951            } else {
952                None
953            };
954            let hi = if let Expr::Lit(v) = &**r {
955                Some(v.clone())
956            } else {
957                None
958            };
959            if matches!(lo, Some(Value::Float(_))) || matches!(hi, Some(Value::Float(_))) {
960                cx.report("E4115", "comprehension over a float range".into());
961                return unk();
962            }
963            if let (Some(lo), Some(hi)) = (lo, hi) {
964                return tyv(Some(ty(RTk::Range {
965                    lo,
966                    hi,
967                    excl: op == "..<",
968                    base: "int".into(),
969                })));
970            }
971            return tyv(Some(prim("int")));
972        }
973    }
974    let Some(rt) = &t.rt else { return unk() };
975    if let Some(a) = arm_of(Some(rt), "arr") {
976        if let RTk::Arr { elem, .. } = &a.k {
977            return tyv(Some(elem.clone()));
978        }
979    }
980    let what = if arm_of(Some(rt), "map").is_some() {
981        "map (use std.map.keys/values)"
982    } else {
983        "value"
984    };
985    cx.report("E4115", format!("comprehension over a non-iterable {what}"));
986    unk()
987}
988
989fn q_dim(rt: Option<&RT>) -> Option<String> {
990    match rt.map(|r| &r.k) {
991        Some(RTk::Quantity(d)) => Some(d.clone()),
992        Some(RTk::Pred { base, .. }) => match &base.k {
993            RTk::Quantity(d) => Some(d.clone()),
994            _ => None,
995        },
996        _ => None,
997    }
998}
999
1000fn infer_bin(cx: &Ctx, e: &Rc<Expr>) -> Ty {
1001    let Expr::Bin { op, l, r } = &**e else {
1002        return unk();
1003    };
1004    let op = op.as_str();
1005    if op == "|>" {
1006        // first-argument insertion (§4.9)
1007        let call = match &**r {
1008            Expr::Call { fun, args } => {
1009                let mut a = vec![l.clone()];
1010                a.extend(args.iter().cloned());
1011                Expr::Call {
1012                    fun: fun.clone(),
1013                    args: a,
1014                }
1015            }
1016            _ => Expr::Call {
1017                fun: r.clone(),
1018                args: vec![l.clone()],
1019            },
1020        };
1021        return infer_call(cx, &Rc::new(call));
1022    }
1023    if op == "??" {
1024        let lt = infer(cx, l); // absence/null on the left is the point
1025        let rt = require_val(cx, r, infer(cx, r), "as `??` fallback");
1026        return tyv(match (lt.rt, rt.rt) {
1027            (Some(a), Some(b)) => mk_union(vec![Some(strip_null(&a)), Some(b)]),
1028            _ => None,
1029        });
1030    }
1031    if op == "&&" || op == "||" {
1032        let lt = require_val(cx, l, infer(cx, l), &format!("as `{op}` operand"));
1033        if lt.rt.is_some() && !is_boolish(lt.rt.as_ref()) {
1034            cx.report("E4071", format!("`{op}` on a non-bool operand"));
1035        }
1036        let c2 = apply_guards(cx, guards_of(l, op == "&&"));
1037        let rt = require_val(&c2, r, infer(&c2, r), &format!("as `{op}` operand"));
1038        if rt.rt.is_some() && !is_boolish(rt.rt.as_ref()) {
1039            cx.report("E4071", format!("`{op}` on a non-bool operand"));
1040        }
1041        return bool_ty();
1042    }
1043    if op == "in" {
1044        require_val(cx, l, infer(cx, l), "as `in` key");
1045        let rt = require_val(cx, r, infer(cx, r), "as `in` container");
1046        let rrt = rt.rt.map(|x| match &x.k {
1047            RTk::Ref(t) => t.clone(),
1048            _ => x,
1049        });
1050        if let (Some(rr), Expr::Lit(Value::Str(key))) = (&rrt, &**l) {
1051            if let RTk::Rec(rec) = &rr.k {
1052                let members = rec.members.borrow();
1053                match find_member(&members, key) {
1054                    Some(m) if m.kind != MKind::Opt => cx.report(
1055                        "E4054",
1056                        format!("`in` on member {key}, which is not optional"),
1057                    ),
1058                    None if !rec.open.get() => cx.report(
1059                        "E4054",
1060                        format!("`in` on undeclared member {key} of a closed record"),
1061                    ),
1062                    _ => {}
1063                }
1064            }
1065        }
1066        return bool_ty();
1067    }
1068    if op == ".." || op == "..<" {
1069        require_val(cx, l, infer(cx, l), "as a range endpoint");
1070        require_val(cx, r, infer(cx, r), "as a range endpoint");
1071        return unk(); // a range value: iterable / membership container only
1072    }
1073    let lt = require_val(cx, l, infer(cx, l), &format!("as `{op}` operand"));
1074    let rt = require_val(cx, r, infer(cx, r), &format!("as `{op}` operand"));
1075    if op == "matches" {
1076        if lt.rt.is_some() && num_kind(lt.rt.as_ref()).as_deref() != Some("string") {
1077            cx.report("E4071", "`matches` needs a string left operand".into());
1078        }
1079        return bool_ty();
1080    }
1081    if op == "==" || op == "!=" {
1082        return bool_ty();
1083    }
1084    let lk = num_kind(lt.rt.as_ref());
1085    let rk = num_kind(rt.rt.as_ref());
1086    let cmp = ["<", "<=", ">", ">="].contains(&op);
1087    let (lks, rks) = (lk.as_deref(), rk.as_deref());
1088    if lks == Some("quantity") || rks == Some("quantity") {
1089        // §3.16: +/-/compare need equal dimensions; * and / compose them;
1090        // a bare int/float scales; a cancelled vector is a plain number
1091        if op == "+" || op == "-" || cmp {
1092            if lt.rt.is_some() && rt.rt.is_some() {
1093                if lks != Some("quantity") || rks != Some("quantity") {
1094                    let other = if lks == Some("quantity") { rks } else { lks };
1095                    cx.report(
1096                        "E4071",
1097                        format!("`{op}` mixes quantity and {}", other.unwrap_or("null")),
1098                    );
1099                } else {
1100                    let (a, b) = (q_dim(lt.rt.as_ref()), q_dim(rt.rt.as_ref()));
1101                    if let (Some(a), Some(b)) = (&a, &b) {
1102                        if a != b {
1103                            let one = |s: &str| {
1104                                if s.is_empty() {
1105                                    "1".to_string()
1106                                } else {
1107                                    s.to_string()
1108                                }
1109                            };
1110                            cx.report(
1111                                "E4072",
1112                                format!(
1113                                    "`{op}` on quantities of different dimensions ({} vs {})",
1114                                    one(a),
1115                                    one(b)
1116                                ),
1117                            );
1118                        }
1119                    }
1120                }
1121            }
1122            return if cmp {
1123                bool_ty()
1124            } else {
1125                tyv(if lks == Some("quantity") {
1126                    lt.rt.clone()
1127                } else {
1128                    rt.rt.clone()
1129                })
1130            };
1131        }
1132        if op == "*" || op == "/" {
1133            let (Some(_), Some(_)) = (&lt.rt, &rt.rt) else {
1134                return unk();
1135            };
1136            let (lv, rv) = (q_dim(lt.rt.as_ref()), q_dim(rt.rt.as_ref()));
1137            let numeric = |k: Option<&str>| matches!(k, Some("int") | Some("float"));
1138            if (lv.is_none() && !numeric(lks)) || (rv.is_none() && !numeric(rks)) {
1139                cx.report("E4071", format!("`{op}` on a non-numeric operand"));
1140                return unk();
1141            }
1142            let key = key_of_vec(&vec_combine(
1143                &lv.as_deref().map(vec_of_key).unwrap_or_default(),
1144                &rv.as_deref().map(vec_of_key).unwrap_or_default(),
1145                if op == "*" { 1 } else { -1 },
1146            ));
1147            return tyv(Some(if key.is_empty() {
1148                prim("float")
1149            } else {
1150                ty(RTk::Quantity(key))
1151            }));
1152        }
1153        cx.report("E4071", format!("`{op}` on quantity operands"));
1154        return unk();
1155    }
1156    if let (Some(_), Some(_), Some(a), Some(b)) = (&lt.rt, &rt.rt, lks, rks) {
1157        if a != b {
1158            cx.report("E4071", format!("`{op}` mixes {a} and {b} operands"));
1159        }
1160    }
1161    if cmp {
1162        return bool_ty();
1163    }
1164    if ["&", "^", "<<", ">>"].contains(&op) {
1165        if (lt.rt.is_some() && lks != Some("int")) || (rt.rt.is_some() && rks != Some("int")) {
1166            cx.report("E4071", format!("`{op}` on non-int operands"));
1167        }
1168        return tyv(Some(prim("int")));
1169    }
1170    if op == "|" {
1171        // bitwise on ints (type-level | never reaches expressions)
1172        if (lt.rt.is_some() && lks != Some("int")) || (rt.rt.is_some() && rks != Some("int")) {
1173            cx.report("E4071", "`|` on non-int operands".into());
1174        }
1175        return tyv(Some(prim("int")));
1176    }
1177    // + - * / %
1178    if op == "+" && lks == Some("string") && rks == Some("string") {
1179        return tyv(Some(prim("string")));
1180    }
1181    if let (Some(lrt), Some(rrt), Some(a), Some(_)) = (&lt.rt, &rt.rt, lks, rks) {
1182        if !["int", "float", "quantity"].contains(&a) {
1183            cx.report("E4071", format!("`{op}` on {a} operands"));
1184        }
1185        if a == "int" && ["+", "-", "*"].contains(&op) {
1186            // interval arithmetic keeps range-typed operands range-typed, so
1187            // `9000 + i` with i: 0..<3 stays assignable where 1..65535 is expected
1188            if let (Some(x), Some(y)) = (as_ival(lrt), as_ival(rrt)) {
1189                let cands: Vec<BigInt> = match op {
1190                    "+" => vec![&x.0 + &y.0, &x.1 + &y.1],
1191                    "-" => vec![&x.0 - &y.1, &x.1 - &y.0],
1192                    _ => vec![&x.0 * &y.0, &x.0 * &y.1, &x.1 * &y.0, &x.1 * &y.1],
1193                };
1194                let lo = cands.iter().min().unwrap().clone();
1195                let hi = cands.iter().max().unwrap().clone();
1196                return tyv(Some(ty(RTk::Range {
1197                    lo: Value::Int(lo),
1198                    hi: Value::Int(hi),
1199                    excl: false,
1200                    base: "int".into(),
1201                })));
1202            }
1203        }
1204        return tyv(if a == "int" || a == "float" {
1205            Some(prim(a))
1206        } else {
1207            None
1208        });
1209    }
1210    unk()
1211}
1212
1213fn as_ival(rt: &RT) -> Option<(BigInt, BigInt)> {
1214    match &rt.k {
1215        RTk::Lit(Value::Int(i)) => Some((i.clone(), i.clone())),
1216        RTk::Range {
1217            lo: Value::Int(lo),
1218            hi: Value::Int(hi),
1219            excl,
1220            base,
1221        } if base == "int" => Some((lo.clone(), if *excl { hi - 1 } else { hi.clone() })),
1222        RTk::Union(arms) => {
1223            let ivs: Vec<Option<(BigInt, BigInt)>> = arms.iter().map(as_ival).collect();
1224            if !ivs.is_empty() && ivs.iter().all(|v| v.is_some()) {
1225                let ivs: Vec<(BigInt, BigInt)> = ivs.into_iter().flatten().collect();
1226                let lo = ivs.iter().map(|v| v.0.clone()).min().unwrap();
1227                let hi = ivs.iter().map(|v| v.1.clone()).max().unwrap();
1228                return Some((lo, hi));
1229            }
1230            None
1231        }
1232        RTk::Pred { base, .. } => as_ival(base),
1233        _ => None,
1234    }
1235}
1236
1237fn index_core(cx: &Ctx, b: Ty, e: &Rc<Expr>) -> Ty {
1238    let Expr::Index { i, .. } = &**e else {
1239        return unk();
1240    };
1241    let it = require_val(cx, i, infer(cx, i), "as an index");
1242    let Some(brt) = &b.rt else { return unk() };
1243    if let Some(a) = arm_of(Some(brt), "arr") {
1244        if it.rt.is_some() && num_kind(it.rt.as_ref()).as_deref() != Some("int") {
1245            cx.report("E4071", "array index is not an int".into());
1246        }
1247        if let RTk::Arr { elem, .. } = &a.k {
1248            return tyv(Some(elem.clone()));
1249        }
1250    }
1251    if let Some(m) = arm_of(Some(brt), "map") {
1252        let k = path_key(e);
1253        if let RTk::Map { val, .. } = &m.k {
1254            return Ty {
1255                rt: Some(val.clone()),
1256                abs: !k.map(|k| cx.present.contains(&k)).unwrap_or(false),
1257            };
1258        }
1259    }
1260    if arm_of(Some(brt), "rec").is_some() {
1261        return unk(); // dynamic member access
1262    }
1263    cx.report("E4071", "indexing a non-collection".into());
1264    unk()
1265}
1266
1267/// a name imported from another module, typed in that module's scope
1268fn imported_ty(cx: &Ctx, ex: &Export) -> Ty {
1269    let t = &ex.env;
1270    let name = &ex.name;
1271    let c = t.consts.borrow().get(name).cloned();
1272    if let Some(c) = c {
1273        // an imported constant is typed as a local one: its annotation, else
1274        // its expression inferred silently in the exporting module
1275        return match try_resolve(t, c.ty.as_ref()) {
1276            Some(a) => tyv(Some(a)),
1277            None => infer(&make_ctx(t.clone(), Rc::new(|_, _| {})), &c.expr),
1278        };
1279    }
1280    let f = t.funcs.borrow().get(name).cloned();
1281    if let Some(f) = f {
1282        return tyv(Some(func_rt_of(t, &f)));
1283    }
1284    let out = t
1285        .outputs
1286        .borrow()
1287        .iter()
1288        .find(|(o, _, _)| o == name)
1289        .map(|(_, ty, _)| ty.clone());
1290    if let Some(o) = out {
1291        return tyv(try_resolve(t, Some(&o)));
1292    }
1293    let inp = t.inputs.borrow().get(name).map(|(ty, _)| ty.clone());
1294    if let Some(i) = inp {
1295        return tyv(try_resolve(t, Some(&i)));
1296    }
1297    if t.type_asts.borrow().contains_key(name) {
1298        cx.report("E3008", format!("type name {name} used as a value"));
1299        return unk();
1300    }
1301    unk()
1302}
1303
1304fn infer_member(cx: &Ctx, e: &Rc<Expr>) -> Ty {
1305    let Expr::Member { x, name, safe } = &**e else {
1306        return unk();
1307    };
1308    if std_path(e).is_some() {
1309        return unk(); // std.* namespace path (typed at the call)
1310    }
1311    if let Expr::Name(xn) = &**x {
1312        if !cx.vars.contains_key(xn) {
1313            let ns = cx.env.namespaces.borrow().get(xn).map(|(_, ex)| ex.clone());
1314            if let Some(exports) = ns {
1315                let ex = exports.borrow().get(name).cloned();
1316                let Some(ex) = ex else {
1317                    cx.report("E3005", format!("namespace {xn} has no export {name}"));
1318                    return unk();
1319                };
1320                return imported_ty(cx, &ex);
1321            }
1322        }
1323    }
1324    let b = infer(cx, x);
1325    let key = path_key(x);
1326    if !*safe {
1327        let present = key
1328            .as_ref()
1329            .map(|k| cx.present.contains(k))
1330            .unwrap_or(false);
1331        let nonnull = key
1332            .as_ref()
1333            .map(|k| cx.nonnull.contains(k))
1334            .unwrap_or(false);
1335        if b.abs && !present {
1336            cx.report(
1337                "E4050",
1338                "member access on a maybe-absent expression (use ?. or an `in` guard)".into(),
1339            );
1340        }
1341        if has_null(b.rt.as_ref()) && !nonnull {
1342            cx.report(
1343                "E4051",
1344                format!("member .{name} on a possibly-null expression without ?."),
1345            );
1346        }
1347    }
1348    member_core(cx, b, e)
1349}
1350
1351fn member_core(cx: &Ctx, b: Ty, e: &Rc<Expr>) -> Ty {
1352    let Expr::Member { name, safe, .. } = &**e else {
1353        return unk();
1354    };
1355    let mut brt = b.rt.as_ref().map(strip_null);
1356    if let Some(RTk::Ref(t)) = brt.as_ref().map(|r| &r.k) {
1357        brt = Some(t.clone());
1358    }
1359    if let Some(RTk::Pred { base, .. }) = brt.as_ref().map(|r| &r.k) {
1360        brt = Some(base.clone());
1361    }
1362    if let Some(r) = &brt {
1363        if matches!(r.k, RTk::IsectN(_)) {
1364            brt = arm_of(Some(r), "rec")
1365                .or_else(|| arm_of(Some(r), "map"))
1366                .or_else(|| Some(r.clone()));
1367        }
1368    }
1369    let mk_abs = |t: Ty| {
1370        if *safe {
1371            Ty {
1372                rt: t.rt,
1373                abs: true,
1374            }
1375        } else {
1376            t
1377        }
1378    };
1379    let Some(brt) = brt else { return mk_abs(unk()) };
1380    match &brt.k {
1381        RTk::Rec(rec) => {
1382            let members = rec.members.borrow();
1383            let Some(m) = find_member(&members, name) else {
1384                if !rec.open.get() {
1385                    cx.report(
1386                        "E4003",
1387                        format!(
1388                            "member {name} is not declared on {}",
1389                            brt.name
1390                                .borrow()
1391                                .clone()
1392                                .unwrap_or_else(|| "this record".into())
1393                        ),
1394                    );
1395                }
1396                return mk_abs(unk());
1397            };
1398            let rt = member_ty(m);
1399            let present = path_key(e)
1400                .map(|k| cx.present.contains(&k))
1401                .unwrap_or(false);
1402            Ty {
1403                rt,
1404                abs: *safe || (m.kind == MKind::Opt && !present),
1405            }
1406        }
1407        RTk::Map { val, .. } => {
1408            let present = path_key(e)
1409                .map(|k| cx.present.contains(&k))
1410                .unwrap_or(false);
1411            Ty {
1412                rt: Some(val.clone()),
1413                abs: *safe || !present,
1414            }
1415        }
1416        RTk::Union(arms) => {
1417            let parts: Vec<Option<RT>> = arms
1418                .iter()
1419                .map(|a| match &a.k {
1420                    RTk::Rec(rec) => {
1421                        find_member(&rec.members.borrow(), name).and_then(|m| m.ty.clone())
1422                    }
1423                    _ => None,
1424                })
1425                .collect();
1426            mk_abs(tyv(mk_union(parts)))
1427        }
1428        RTk::Quantity(_) if name == "value" || name == "unit" => {
1429            mk_abs(tyv(Some(prim(if name == "value" {
1430                "float"
1431            } else {
1432                "string"
1433            }))))
1434        }
1435        _ => mk_abs(unk()),
1436    }
1437}
1438
1439fn func_rt_of(env: &Rc<Env>, f: &FuncEntry) -> RT {
1440    ty(RTk::Func {
1441        params: f
1442            .params
1443            .iter()
1444            .map(|p| try_resolve(env, p.ty.as_ref()).unwrap_or_else(|| ty(RTk::Any)))
1445            .collect(),
1446        ret: try_resolve(env, f.ret.as_ref()).unwrap_or_else(|| ty(RTk::Any)),
1447    })
1448}
1449fn func_rt(cx: &Ctx, name: &str) -> RT {
1450    let f = cx.env.funcs.borrow().get(name).cloned().unwrap();
1451    func_rt_of(&cx.env, &f)
1452}
1453fn const_ty(cx: &Ctx, name: &str) -> Ty {
1454    if let Some(t) = cx.const_memo.borrow().get(name) {
1455        return t.clone();
1456    }
1457    cx.const_memo.borrow_mut().insert(name.to_string(), unk()); // cycle guard
1458    let c = cx.env.consts.borrow().get(name).cloned().unwrap();
1459    let anno = try_resolve(&cx.env, c.ty.as_ref());
1460    let t = match anno {
1461        Some(a) => tyv(Some(a)),
1462        None => infer(&make_ctx(cx.env.clone(), Rc::new(|_, _| {})), &c.expr), // silent module-scope inference
1463    };
1464    cx.const_memo
1465        .borrow_mut()
1466        .insert(name.to_string(), t.clone());
1467    t
1468}
1469
1470fn infer_call(cx: &Ctx, e: &Rc<Expr>) -> Ty {
1471    let Expr::Call { fun, args } = &**e else {
1472        return unk();
1473    };
1474    if let Some(sp) = std_path(fun) {
1475        let sig = std_sig(&sp);
1476        if sig.is_none() {
1477            cx.report(
1478                "E3003",
1479                format!("std.{sp} does not exist (§13.1: names not listed do not exist)"),
1480            );
1481        }
1482        if let Some((arity, _)) = &sig {
1483            if args.len() != *arity {
1484                cx.report(
1485                    "E4062",
1486                    format!("std.{sp} expects {arity} argument(s), got {}", args.len()),
1487                );
1488            }
1489        }
1490        for a in args {
1491            if matches!(&**a, Expr::Lambda { .. }) {
1492                infer(cx, a);
1493                continue;
1494            }
1495            require_val(cx, a, infer(cx, a), "as an argument");
1496        }
1497        return tyv(sig.and_then(|(_, r)| r));
1498    }
1499    let f = infer(cx, fun);
1500    let frt = f.rt.filter(|r| matches!(r.k, RTk::Func { .. }));
1501    let (params, ret): (Vec<RT>, Option<RT>) = match frt.as_ref().map(|r| &r.k) {
1502        Some(RTk::Func { params, ret }) => (params.clone(), ret_of(ret)),
1503        _ => (vec![], None),
1504    };
1505    if frt.is_some() && args.len() != params.len() {
1506        cx.report(
1507            "E4062",
1508            format!(
1509                "call expects {} argument(s), got {}",
1510                params.len(),
1511                args.len()
1512            ),
1513        );
1514    }
1515    for (i, a) in args.iter().enumerate() {
1516        let expected: Option<RT> =
1517            if frt.is_some() && i < params.len() && !matches!(params[i].k, RTk::Any) {
1518                Some(params[i].clone())
1519            } else {
1520                None
1521            };
1522        if let (Expr::Lambda { .. }, Some(ex)) = (&**a, &expected) {
1523            if matches!(ex.k, RTk::Func { .. }) {
1524                check_lambda(cx, a, ex);
1525                continue;
1526            }
1527        }
1528        if matches!(&**a, Expr::Lambda { .. }) {
1529            infer(cx, a);
1530            continue;
1531        }
1532        let at = require_val(cx, a, infer(cx, a), "as an argument");
1533        if let (Some(art), Some(ex)) = (&at.rt, &expected) {
1534            if !subsumes(&cx.env, art, ex) && !deferrable(art, ex) {
1535                cx.report(
1536                    "E4001",
1537                    format!("argument {} is not assignable to its parameter", i + 1),
1538                );
1539            }
1540        }
1541    }
1542    tyv(if frt.is_some() { ret } else { None })
1543}
1544
1545fn check_lambda(cx: &Ctx, e: &Rc<Expr>, expected: &RT) {
1546    let (Expr::Lambda { params, body }, RTk::Func { params: eps, ret }) = (&**e, &expected.k)
1547    else {
1548        return;
1549    };
1550    if params.len() != eps.len() {
1551        cx.report(
1552            "E4062",
1553            "lambda arity differs from expected function type".into(),
1554        );
1555        return;
1556    }
1557    let mut c2 = cx.child();
1558    for (i, p) in params.iter().enumerate() {
1559        if name_bound(&c2, p) {
1560            cx.report(
1561                "E3019",
1562                format!("lambda parameter {p} shadows an enclosing name"),
1563            );
1564        }
1565        c2.vars.insert(
1566            p.clone(),
1567            tyv(if matches!(eps[i].k, RTk::Any) {
1568                None
1569            } else {
1570                Some(eps[i].clone())
1571            }),
1572        );
1573    }
1574    let b = require_val(&c2, body, infer(&c2, body), "as a lambda result");
1575    if let (Some(brt), Some(r)) = (&b.rt, ret_of(ret)) {
1576        if !subsumes(&cx.env, brt, &r) && !deferrable(brt, &r) {
1577            cx.report(
1578                "E4001",
1579                "lambda body is not assignable to the expected result type".into(),
1580            );
1581        }
1582    }
1583}
1584
1585// ---------------- match (§4.7) ----------------
1586fn infer_match(cx: &Ctx, e: &Rc<Expr>, expected: Option<&RT>) -> Ty {
1587    let Expr::Match { subject, arms } = &**e else {
1588        return unk();
1589    };
1590    let s = require_val(cx, subject, infer(cx, subject), "as a match subject");
1591    let mut variants: Option<Vec<RT>> = None;
1592    if let Some(srt0) = &s.rt {
1593        let srt = strip_null(srt0);
1594        if let RTk::Union(vs) = &srt.k {
1595            let mut v = vs.clone();
1596            if has_null(Some(srt0)) {
1597                v.push(ty(RTk::Lit(Value::Null)));
1598            }
1599            variants = Some(v);
1600        } else {
1601            cx.report(
1602                "E4103",
1603                "`match` subject is not a discriminable union".into(),
1604            );
1605        }
1606    }
1607    let mut covered: HashSet<usize> = HashSet::new();
1608    let mut catch_alls = 0;
1609    let mut results: Vec<Option<RT>> = vec![];
1610    for arm in arms {
1611        let mut c2 = cx.child();
1612        if name_bound(&c2, &arm.v) {
1613            cx.report(
1614                "E3019",
1615                format!("match binding {} shadows an enclosing name", arm.v),
1616            );
1617        }
1618        let mut arm_ty: Option<RT> = None;
1619        if let Some(t) = &arm.ty {
1620            arm_ty = try_resolve(&cx.env, Some(t));
1621            if arm_ty.is_none() {
1622                cx.report("E3003", "unknown type in match arm".into());
1623            }
1624            if let (Some(vs), Some(at)) = (&variants, &arm_ty) {
1625                for (i, v) in vs.iter().enumerate() {
1626                    if subsumes(&cx.env, v, at) {
1627                        if covered.contains(&i) {
1628                            cx.report("E4100", "match arms overlap on a variant".into());
1629                        }
1630                        covered.insert(i);
1631                    }
1632                }
1633            }
1634        } else {
1635            catch_alls += 1;
1636            if let Some(vs) = &variants {
1637                let rest: Vec<Option<RT>> = vs
1638                    .iter()
1639                    .enumerate()
1640                    .filter(|(i, _)| !covered.contains(i))
1641                    .map(|(_, v)| Some(v.clone()))
1642                    .collect();
1643                if rest.is_empty() {
1644                    cx.report(
1645                        "E4102",
1646                        "match catch-all is dead (typed arms are exhaustive)".into(),
1647                    );
1648                }
1649                arm_ty = mk_union(rest);
1650            }
1651        }
1652        c2.vars.insert(arm.v.clone(), tyv(arm_ty));
1653        let bt = match expected {
1654            Some(ex) => check_expr(&c2, &arm.body, Some(ex)),
1655            None => infer(&c2, &arm.body),
1656        };
1657        let b = require_val(&c2, &arm.body, bt, "as a match result");
1658        results.push(b.rt);
1659    }
1660    if catch_alls > 1 {
1661        cx.report("E4100", "more than one match catch-all arm".into());
1662    }
1663    if let Some(vs) = &variants {
1664        if catch_alls == 0 && covered.len() < vs.len() {
1665            cx.report(
1666                "E4101",
1667                "`match` is not exhaustive over the subject union".into(),
1668            );
1669        }
1670    }
1671    tyv(mk_union(results))
1672}
1673
1674// ---------------- bidirectional checking (§3.18) ----------------
1675/// a navigation expression in a ref<T> position denotes a place (§7.4):
1676/// the absence discipline does not apply along the spine — whether the
1677/// place holds a value is reference integrity (§7.5), checked at binding
1678fn place_ty(cx: &Ctx, e: &Rc<Expr>) -> Ty {
1679    match &**e {
1680        Expr::Paren(x) => place_ty(cx, x),
1681        Expr::Member { x, name, .. } => {
1682            let base = place_ty(cx, x);
1683            // a hidden member's value is not part of any document: no reference
1684            // can target it (§7.5, D34)
1685            let rec = base.rt.as_ref().map(|t| match &t.k {
1686                RTk::Ref(target) => target.clone(),
1687                _ => t.clone(),
1688            });
1689            if let Some(rec) = rec {
1690                if let RTk::Rec(r) = &rec.k {
1691                    if r.members
1692                        .borrow()
1693                        .iter()
1694                        .any(|m| m.name == *name && m.hidden)
1695                    {
1696                        cx.report("E4093", format!("`ref` position navigates hidden member {name} — not part of the value (§7.5)"));
1697                    }
1698                }
1699            }
1700            member_core(cx, base, e)
1701        }
1702        Expr::Index { x, .. } => index_core(cx, place_ty(cx, x), e),
1703        Expr::If { c, t, f } => {
1704            // a conditional between places is a place: each branch is read in
1705            // the ref position, the condition as an ordinary value (§7.4)
1706            let ct = require_val(cx, c, infer(cx, c), "as a condition");
1707            if ct.rt.is_some() && !is_boolish(ct.rt.as_ref()) {
1708                cx.report("E4001", "`if` condition is not bool".into());
1709            }
1710            let tt = place_ty(&apply_guards(cx, guards_of(c, true)), t);
1711            let ft = place_ty(&apply_guards(cx, guards_of(c, false)), f);
1712            Ty {
1713                rt: mk_union(vec![tt.rt, ft.rt]),
1714                abs: tt.abs || ft.abs,
1715            }
1716        }
1717        Expr::Name(n) if !cx.vars.contains_key(n) && cx.env.consts.borrow().contains_key(n) => {
1718            // the spine root must be a root-derived place, never a module const (§7.5, D32)
1719            cx.report(
1720                "E4093",
1721                format!(
1722                    "`ref` position navigates module const {n} — not a root-derived place (§7.5)"
1723                ),
1724            );
1725            infer(cx, e)
1726        }
1727        _ => infer(cx, e),
1728    }
1729}
1730
1731pub fn check_expr(cx: &Ctx, e: &Rc<Expr>, expected: Option<&RT>) -> Ty {
1732    let Some(expected) = expected else {
1733        return infer(cx, e);
1734    };
1735    if let RTk::Ref(_) = &expected.k {
1736        place_ty(cx, e);
1737        return tyv(Some(expected.clone())); // place, not value (§7.4)
1738    }
1739    if let RTk::Pred { base, .. } = &expected.k {
1740        return check_expr(cx, e, Some(base));
1741    }
1742    if let RTk::IsectN(arms) = &expected.k {
1743        if matches!(
1744            &**e,
1745            Expr::Obj(_) | Expr::Arr(_) | Expr::Comp { .. } | Expr::MapComp { .. }
1746        ) {
1747            for arm in arms {
1748                check_expr(cx, e, Some(arm)); // a literal must satisfy every arm
1749            }
1750            return tyv(Some(expected.clone()));
1751        }
1752    }
1753    match (&**e, &expected.k) {
1754        (Expr::Comp { head, clauses }, RTk::Arr { elem, .. }) => {
1755            let c2 = bind_clauses(cx, clauses);
1756            check_expr(&c2, head, Some(elem));
1757            return tyv(Some(expected.clone()));
1758        }
1759        (Expr::MapComp { key, val, clauses }, RTk::Map { val: ev, .. }) => {
1760            let c2 = bind_clauses(cx, clauses);
1761            let k = require_val(&c2, key, infer(&c2, key), "as a map key");
1762            if k.rt.is_some() && num_kind(k.rt.as_ref()).as_deref() != Some("string") {
1763                cx.report("E4001", "map-comprehension key is not a string".into());
1764            }
1765            check_expr(&c2, val, Some(ev));
1766            return tyv(Some(expected.clone()));
1767        }
1768        (Expr::Paren(x), _) => return check_expr(cx, x, Some(expected)),
1769        (Expr::If { c, t, f }, _) => {
1770            let ct = require_val(cx, c, infer(cx, c), "as a condition");
1771            if ct.rt.is_some() && !is_boolish(ct.rt.as_ref()) {
1772                cx.report("E4001", "`if` condition is not bool".into());
1773            }
1774            check_expr(&apply_guards(cx, guards_of(c, true)), t, Some(expected));
1775            check_expr(&apply_guards(cx, guards_of(c, false)), f, Some(expected));
1776            return tyv(Some(expected.clone()));
1777        }
1778        (Expr::Match { .. }, _) => return infer_match(cx, e, Some(expected)),
1779        (Expr::Obj(entries), RTk::Rec(rec)) => {
1780            // entries see the record's members (siblings + inherited scope chain)
1781            let members = rec.members.borrow().clone();
1782            let mut cx_r = cx.child();
1783            for m in &members {
1784                cx_r.vars.insert(
1785                    m.name.clone(),
1786                    Ty {
1787                        rt: member_ty(m),
1788                        abs: m.kind == MKind::Opt,
1789                    },
1790                );
1791            }
1792            for (k, v) in entries {
1793                let Some(m) = find_member(&members, k) else {
1794                    if !rec.open.get() {
1795                        cx.report(
1796                            "E4003",
1797                            format!(
1798                                "member {k} is not declared on {}",
1799                                expected
1800                                    .name
1801                                    .borrow()
1802                                    .clone()
1803                                    .unwrap_or_else(|| "the record".into())
1804                            ),
1805                        );
1806                    }
1807                    require_val(&cx_r, v, infer(&cx_r, v), "as a construction member");
1808                    continue;
1809                };
1810                let mt = member_ty(m);
1811                let t = check_expr(&cx_r, v, mt.as_ref());
1812                require_val(&cx_r, v, t, "as a construction member");
1813            }
1814            for m in &members {
1815                if m.kind == MKind::Req && !entries.iter().any(|(k, _)| *k == m.name) {
1816                    cx.report(
1817                        "E4002",
1818                        format!("required member {} missing in the construction", m.name),
1819                    );
1820                }
1821            }
1822            return tyv(Some(expected.clone()));
1823        }
1824        (Expr::Obj(entries), RTk::Map { val, .. }) => {
1825            for (_, v) in entries {
1826                let t = check_expr(cx, v, Some(val));
1827                require_val(cx, v, t, "as a map value");
1828            }
1829            return tyv(Some(expected.clone()));
1830        }
1831        (Expr::Obj(_), RTk::Union(_)) => {
1832            infer(cx, e);
1833            return tyv(Some(expected.clone())); // discriminated at binding
1834        }
1835        (Expr::Obj(_), _) => {
1836            infer(cx, e);
1837            cx.report(
1838                "E4001",
1839                format!("object literal where {} is expected", tag(expected)),
1840            );
1841            return tyv(Some(expected.clone()));
1842        }
1843        (Expr::Arr(items), RTk::Arr { elem, .. }) => {
1844            for (spread, x) in items {
1845                if *spread {
1846                    require_val(cx, x, infer(cx, x), "as a spread");
1847                    continue;
1848                }
1849                let t = check_expr(cx, x, Some(elem));
1850                require_val(cx, x, t, "as an array element");
1851            }
1852            return tyv(Some(expected.clone()));
1853        }
1854        (Expr::Lambda { .. }, RTk::Func { .. }) => {
1855            check_lambda(cx, e, expected);
1856            return tyv(Some(expected.clone()));
1857        }
1858        _ => {}
1859    }
1860    let t = require_val(cx, e, infer(cx, e), "as a value");
1861    if let Some(rt) = &t.rt {
1862        if !subsumes(&cx.env, rt, expected) && !deferrable(rt, expected) {
1863            cx.report(
1864                "E4001",
1865                "expression type does not satisfy the expected type".into(),
1866            );
1867        }
1868    }
1869    t
1870}
1871
1872/// a same-kind refinement target (pattern, range, literal set) whose
1873/// membership the static type cannot prove is validated at binding, not
1874/// rejected here — the corpus (guide, benchmarks) relies on this split;
1875/// kind-level mismatches still fail statically
1876fn deferrable(s: &RT, t: &RT) -> bool {
1877    let Some(k) = num_kind(Some(s)) else {
1878        return false;
1879    };
1880    match &t.k {
1881        RTk::Pattern { .. } => k == "string",
1882        RTk::Range { base, .. } => &k == base,
1883        RTk::Lit(_) => Some(k) == num_kind(Some(t)),
1884        RTk::Union(arms) => arms.iter().any(|a| deferrable(s, a)),
1885        RTk::Pred { base, .. } => deferrable(s, base),
1886        _ => false,
1887    }
1888}