Skip to main content

lex_types/
env.rs

1//! Type environment: type-decl info and value-binding scopes.
2
3use crate::types::*;
4use indexmap::IndexMap;
5
6#[derive(Debug, Clone)]
7pub struct TypeDef {
8    pub params: Vec<String>,
9    pub kind: TypeDefKind,
10}
11
12#[derive(Debug, Clone)]
13pub enum TypeDefKind {
14    /// A union: variant name → optional payload.
15    Union(IndexMap<String, Option<Ty>>),
16    /// A record alias: `type Foo = { x :: Int }` etc.
17    Alias(Ty),
18    /// Built-in opaque (Map, Set, ...).
19    Opaque,
20}
21
22#[derive(Debug, Clone, Default)]
23pub struct TypeEnv {
24    /// Type-name → definition.
25    pub types: IndexMap<String, TypeDef>,
26    /// Constructor name → owning type-name.
27    pub ctor_to_type: IndexMap<String, String>,
28    /// Import alias → dependency module mangle prefix (#963). When a dependency
29    /// is resolved as a whole package, its types are registered under canonical
30    /// prefix names (`error_<hash>.DbErr`); this maps an import alias `e` to
31    /// that prefix so an alias-qualified annotation `e.DbErr` is normalized to
32    /// the same canonical `Con` in [`ty_from_canon_env`] — making a
33    /// directly-imported module and the copies inlined into its sibling modules
34    /// one type. Empty in the ordinary (inlined / no-dependency) case.
35    pub dep_alias_prefixes: IndexMap<String, String>,
36}
37
38impl TypeEnv {
39    pub fn new_with_builtins() -> Self {
40        let mut e = TypeEnv::default();
41        // Result[T, E] = Ok(T) | Err(E)
42        let mut r_variants = IndexMap::new();
43        r_variants.insert("Ok".into(), Some(Ty::Var(0))); // T
44        r_variants.insert("Err".into(), Some(Ty::Var(1))); // E
45        e.types.insert("Result".into(), TypeDef {
46            params: vec!["T".into(), "E".into()],
47            kind: TypeDefKind::Union(r_variants),
48        });
49        e.ctor_to_type.insert("Ok".into(), "Result".into());
50        e.ctor_to_type.insert("Err".into(), "Result".into());
51
52        // Option[T] = Some(T) | None
53        let mut o_variants = IndexMap::new();
54        o_variants.insert("Some".into(), Some(Ty::Var(0))); // T
55        o_variants.insert("None".into(), None);
56        e.types.insert("Option".into(), TypeDef {
57            params: vec!["T".into()],
58            kind: TypeDefKind::Union(o_variants),
59        });
60        e.ctor_to_type.insert("Some".into(), "Option".into());
61        e.ctor_to_type.insert("None".into(), "Option".into());
62
63        // Nil = Unit (alias)
64        e.types.insert("Nil".into(), TypeDef {
65            params: vec![],
66            kind: TypeDefKind::Alias(Ty::Unit),
67        });
68
69        // Map, Set: opaque-ish. We just register the names so they parse as Cons.
70        e.types.insert("Map".into(), TypeDef { params: vec!["K".into(), "V".into()], kind: TypeDefKind::Opaque });
71        e.types.insert("Set".into(), TypeDef { params: vec!["T".into()], kind: TypeDefKind::Opaque });
72
73        // SqlParam = PStr(Str) | PInt(Int) | PFloat(Float) | PBool(Bool) | PNull
74        // Typed parameter binding for std.sql (#362). Replaces the v1 List[Str]
75        // approach so callers don't have to stringify non-string values.
76        let mut sp_variants = IndexMap::new();
77        sp_variants.insert("PStr".into(),   Some(Ty::str()));
78        sp_variants.insert("PInt".into(),   Some(Ty::int()));
79        sp_variants.insert("PFloat".into(), Some(Ty::float()));
80        sp_variants.insert("PBool".into(),  Some(Ty::bool()));
81        sp_variants.insert("PNull".into(),  None);
82        e.types.insert("SqlParam".into(), TypeDef {
83            params: vec![],
84            kind: TypeDefKind::Union(sp_variants),
85        });
86        for ctor in &["PStr", "PInt", "PFloat", "PBool", "PNull"] {
87            e.ctor_to_type.insert((*ctor).into(), "SqlParam".into());
88        }
89
90        // SqlTx: opaque transaction handle (#362). Backed by the same
91        // Int registry key as Db; the type system enforces that commit/
92        // rollback can only be called on a value from sql.begin, not on
93        // a raw Db connection.
94        e.types.insert("SqlTx".into(), TypeDef { params: vec![], kind: TypeDefKind::Opaque });
95
96        // SqlError = { message :: Str, code :: Option[Str], detail :: Option[Str] }
97        // Structured error shape returned by every `std.sql` op (#380).
98        // `code` carries the SQLSTATE (Postgres) or the symbolic SQLite
99        // error name (`SQLITE_BUSY`, `SQLITE_CONSTRAINT_UNIQUE`, …) so
100        // dialect-aware retry / conflict-handling can avoid string
101        // parsing. `message` is always populated; `detail` carries a
102        // driver-side detail string when present.
103        let mut se_fields = IndexMap::new();
104        se_fields.insert("message".into(), Ty::str());
105        se_fields.insert("code".into(), Ty::Con("Option".into(), vec![Ty::str()]));
106        se_fields.insert("detail".into(), Ty::Con("Option".into(), vec![Ty::str()]));
107        e.types.insert("SqlError".into(), TypeDef {
108            params: vec![],
109            kind: TypeDefKind::Alias(Ty::Record(se_fields)),
110        });
111
112        // AeadResult = { ciphertext :: Bytes, tag :: Bytes } — return
113        // shape for every AEAD seal op in `std.crypto` (#382 AEAD slice).
114        // The auth tag is split out from the ciphertext so callers don't
115        // have to know each algorithm's tag length: AES-GCM and
116        // ChaCha20-Poly1305 both happen to be 16 bytes today, but the
117        // shape keeps that detail encapsulated.
118        let mut ar_fields = IndexMap::new();
119        ar_fields.insert("ciphertext".into(), Ty::bytes());
120        ar_fields.insert("tag".into(), Ty::bytes());
121        e.types.insert("AeadResult".into(), TypeDef {
122            params: vec![],
123            kind: TypeDefKind::Alias(Ty::Record(ar_fields)),
124        });
125
126        // UdpDatagram = { data :: Bytes, host :: Str, port :: Int } —
127        // what `net.udp_recv` hands back (#760).
128        //
129        // The sender's address is part of the value rather than something
130        // the caller has to ask for separately, because with UDP it is not
131        // optional detail: any host can send to an open socket, so a reply
132        // that does not carry who sent it cannot be safely acted on. A
133        // request/response caller must check it matches who they asked.
134        let mut dg_fields = IndexMap::new();
135        dg_fields.insert("data".into(), Ty::bytes());
136        dg_fields.insert("host".into(), Ty::str());
137        dg_fields.insert("port".into(), Ty::int());
138        e.types.insert("UdpDatagram".into(), TypeDef {
139            params: vec![],
140            kind: TypeDefKind::Alias(Ty::Record(dg_fields)),
141        });
142
143        // Iter[T]: lazy positional iterator (#364). Backed at runtime by a
144        // (List[T], Int) tuple; the Int is the current cursor index. All
145        // iter.* operations are compiler-inlined so no effect is needed.
146        e.types.insert("Iter".into(), TypeDef { params: vec!["T".into()], kind: TypeDefKind::Opaque });
147
148        // Stream[T]: opaque streaming iterator (#305 slice 3).
149        // Built and consumed exclusively through the `stream.*` and
150        // `agent.cloud_stream` effect builtins; the runtime
151        // represents a Stream value as an opaque variant carrying a
152        // handle id. Registered as Opaque so type-checking knows
153        // `Stream[Str]` parses but doesn't unwrap it structurally.
154        e.types.insert("Stream".into(), TypeDef { params: vec!["T".into()], kind: TypeDefKind::Opaque });
155
156        // Tz = Utc | Local | Offset(Int) | Iana(Str).
157        // Used by std.datetime; the variant-typed alternative to the
158        // pre-v1 stringly Tz ("UTC" / "Local" / "+05:30" / IANA name).
159        // Registered globally so users don't have to import a module
160        // to mention `Utc` / `Iana("America/New_York")` etc.
161        let mut tz_variants = IndexMap::new();
162        tz_variants.insert("Utc".into(), None);
163        tz_variants.insert("Local".into(), None);
164        tz_variants.insert("Offset".into(), Some(Ty::int()));
165        tz_variants.insert("Iana".into(), Some(Ty::str()));
166        e.types.insert("Tz".into(), TypeDef {
167            params: vec![],
168            kind: TypeDefKind::Union(tz_variants),
169        });
170        for ctor in &["Utc", "Local", "Offset", "Iana"] {
171            e.ctor_to_type.insert((*ctor).into(), "Tz".into());
172        }
173
174        // HttpError = NetworkError(Str) | TimeoutError | TlsError(Str)
175        //           | DecodeError(Str)
176        // Used by std.http; structured failure shape so callers can
177        // discriminate transport vs. timeout vs. TLS vs. body-decode
178        // errors without parsing strings.
179        let mut http_err_variants = IndexMap::new();
180        http_err_variants.insert("NetworkError".into(), Some(Ty::str()));
181        http_err_variants.insert("TimeoutError".into(), None);
182        http_err_variants.insert("TlsError".into(), Some(Ty::str()));
183        http_err_variants.insert("DecodeError".into(), Some(Ty::str()));
184        e.types.insert("HttpError".into(), TypeDef {
185            params: vec![],
186            kind: TypeDefKind::Union(http_err_variants),
187        });
188        for ctor in &["NetworkError", "TimeoutError", "TlsError", "DecodeError"] {
189            e.ctor_to_type.insert((*ctor).into(), "HttpError".into());
190        }
191
192        // Json = JNull | JBool(Bool) | JInt(Int) | JFloat(Float)
193        //      | JStr(Str) | JList(List[Json]) | JObj(List[(Str, Json)])
194        // The generic JSON value ADT produced by `std.json.decode` and
195        // consumed by `std.json.encode`. Structurally identical to
196        // lex-schema's `json_value.Json`, so the native builtins are a
197        // drop-in for that interpreted parser. Registered globally (like
198        // Tz/HttpError) so a program can pattern-match `Json` without an
199        // extra type import. Recursive payloads reference the type by
200        // name via `Ty::Con("Json", [])`.
201        let json_ty = || Ty::Con("Json".into(), vec![]);
202        let mut json_variants = IndexMap::new();
203        json_variants.insert("JNull".into(), None);
204        json_variants.insert("JBool".into(), Some(Ty::bool()));
205        json_variants.insert("JInt".into(), Some(Ty::int()));
206        json_variants.insert("JFloat".into(), Some(Ty::float()));
207        json_variants.insert("JStr".into(), Some(Ty::str()));
208        json_variants.insert("JList".into(), Some(Ty::List(Box::new(json_ty()))));
209        json_variants.insert(
210            "JObj".into(),
211            Some(Ty::List(Box::new(Ty::Tuple(vec![Ty::str(), json_ty()])))),
212        );
213        e.types.insert("Json".into(), TypeDef {
214            params: vec![],
215            kind: TypeDefKind::Union(json_variants),
216        });
217        for ctor in &["JNull", "JBool", "JInt", "JFloat", "JStr", "JList", "JObj"] {
218            e.ctor_to_type.insert((*ctor).into(), "Json".into());
219        }
220
221        // HttpRequest = { method, url, headers, body, timeout_ms }.
222        // The std.http request shape. Anonymous record literals coerce
223        // to this nominal alias at every position (per the §3.13
224        // record-coercion rules), so users write
225        // `{ method: "GET", url: u, headers: map.new(), body: None,
226        // timeout_ms: None }` rather than a dedicated constructor —
227        // builders (`http.with_header` etc.) are pure transforms over
228        // the same shape.
229        let mut req_fields = IndexMap::new();
230        req_fields.insert("method".into(), Ty::str());
231        req_fields.insert("url".into(), Ty::str());
232        req_fields.insert("headers".into(), Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]));
233        req_fields.insert("body".into(), Ty::Con("Option".into(), vec![Ty::bytes()]));
234        req_fields.insert("timeout_ms".into(), Ty::Con("Option".into(), vec![Ty::int()]));
235        e.types.insert("HttpRequest".into(), TypeDef {
236            params: vec![],
237            kind: TypeDefKind::Alias(Ty::Record(req_fields)),
238        });
239
240        // HttpResponse = { status, headers, body }. Returned by every
241        // `http.{send,get,post}` happy path; also the input to
242        // `http.{json_body,text_body}`.
243        let mut resp_fields = IndexMap::new();
244        resp_fields.insert("status".into(), Ty::int());
245        resp_fields.insert("headers".into(), Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]));
246        resp_fields.insert("body".into(), Ty::bytes());
247        e.types.insert("HttpResponse".into(), TypeDef {
248            params: vec![],
249            kind: TypeDefKind::Alias(Ty::Record(resp_fields)),
250        });
251
252        // Matrix = { rows :: Int, cols :: Int, data :: List[Float] }.
253        // Used by std.math; runtime values are the F64Array fast lane,
254        // not a real record. The alias makes math.* signatures readable
255        // (`:: Matrix` instead of an inline record) and lets call sites
256        // unify nominally. Field access via `m.rows` would type-check
257        // but fail at runtime — use `math.rows / math.cols / math.get`.
258        let mut mat_fields = IndexMap::new();
259        mat_fields.insert("rows".into(), Ty::int());
260        mat_fields.insert("cols".into(), Ty::int());
261        mat_fields.insert("data".into(), Ty::List(Box::new(Ty::float())));
262        e.types.insert("Matrix".into(), TypeDef {
263            params: vec![],
264            kind: TypeDefKind::Alias(Ty::Record(mat_fields)),
265        });
266
267        // Request = { method :: Str, path :: Str, query :: Str, body :: Str,
268        //             headers :: Map[Str, Str], path_params :: Map[Str, Str] }
269        // Inbound request shape used by net.serve_fn handlers.
270        // `path_params` is populated by `net.serve_routed` from `:name`
271        // segments in the route pattern; empty under `net.serve_fn`.
272        let mut net_req_fields = IndexMap::new();
273        net_req_fields.insert("method".into(), Ty::str());
274        net_req_fields.insert("path".into(), Ty::str());
275        net_req_fields.insert("query".into(), Ty::str());
276        net_req_fields.insert("body".into(), Ty::str());
277        net_req_fields.insert("headers".into(), Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]));
278        net_req_fields.insert("path_params".into(), Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]));
279        e.types.insert("Request".into(), TypeDef {
280            params: vec![],
281            kind: TypeDefKind::Alias(Ty::Record(net_req_fields)),
282        });
283
284        // Response = { status :: Int, body :: ResponseBody, headers :: Map[Str, Str] }
285        // Outbound response shape returned by net.serve_fn handlers.
286        // #375: `body` is now an ADT instead of a bare Str. Streaming
287        // variants (BodyStream / BodyBytes) carry an `Iter[T]` that the
288        // server drains chunk-by-chunk under chunked transfer-encoding.
289        let mut rb_variants = IndexMap::new();
290        rb_variants.insert("BodyStr".into(),    Some(Ty::str()));
291        rb_variants.insert(
292            "BodyStream".into(),
293            Some(Ty::Con("Iter".into(), vec![Ty::str()])),
294        );
295        rb_variants.insert(
296            "BodyBytes".into(),
297            Some(Ty::Con("Iter".into(), vec![Ty::List(Box::new(Ty::int()))])),
298        );
299        e.types.insert("ResponseBody".into(), TypeDef {
300            params: vec![],
301            kind: TypeDefKind::Union(rb_variants),
302        });
303        for ctor in &["BodyStr", "BodyStream", "BodyBytes"] {
304            e.ctor_to_type.insert((*ctor).into(), "ResponseBody".into());
305        }
306
307        let mut net_resp_fields = IndexMap::new();
308        net_resp_fields.insert("status".into(), Ty::int());
309        net_resp_fields.insert("body".into(), Ty::Con("ResponseBody".into(), vec![]));
310        net_resp_fields.insert("headers".into(), Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]));
311        e.types.insert("Response".into(), TypeDef {
312            params: vec![],
313            kind: TypeDefKind::Alias(Ty::Record(net_resp_fields)),
314        });
315
316        // WsConn = { id :: Str, path :: Str, subprotocol :: Str }
317        // Passed to every net.serve_ws_fn message handler.
318        let mut ws_conn_fields = IndexMap::new();
319        ws_conn_fields.insert("id".into(), Ty::str());
320        ws_conn_fields.insert("path".into(), Ty::str());
321        ws_conn_fields.insert("subprotocol".into(), Ty::str());
322        e.types.insert("WsConn".into(), TypeDef {
323            params: vec![],
324            kind: TypeDefKind::Alias(Ty::Record(ws_conn_fields)),
325        });
326
327        // WsMessage = WsText(Str) | WsBinary(List[Int]) | WsPing | WsClose
328        let mut ws_msg_variants = IndexMap::new();
329        ws_msg_variants.insert("WsText".into(), Some(Ty::str()));
330        ws_msg_variants.insert("WsBinary".into(), Some(Ty::List(Box::new(Ty::int()))));
331        ws_msg_variants.insert("WsPing".into(), None);
332        ws_msg_variants.insert("WsClose".into(), None);
333        e.types.insert("WsMessage".into(), TypeDef {
334            params: vec![],
335            kind: TypeDefKind::Union(ws_msg_variants),
336        });
337        for ctor in &["WsText", "WsBinary", "WsPing", "WsClose"] {
338            e.ctor_to_type.insert((*ctor).into(), "WsMessage".into());
339        }
340
341        // WsAction = WsSend(Str) | WsSendBinary(List[Int]) | WsNoOp
342        // Handlers return this to tell the runtime what to send back.
343        // Connection close is handled automatically when the runtime receives
344        // an incoming WsClose frame; handlers do not need to emit a close action.
345        let mut ws_act_variants = IndexMap::new();
346        ws_act_variants.insert("WsSend".into(), Some(Ty::str()));
347        ws_act_variants.insert("WsSendBinary".into(), Some(Ty::List(Box::new(Ty::int()))));
348        ws_act_variants.insert("WsNoOp".into(), None);
349        e.types.insert("WsAction".into(), TypeDef {
350            params: vec![],
351            kind: TypeDefKind::Union(ws_act_variants),
352        });
353        for ctor in &["WsSend", "WsSendBinary", "WsNoOp"] {
354            e.ctor_to_type.insert((*ctor).into(), "WsAction".into());
355        }
356
357        // ConcError = AlreadyRegistered(Str) | NotRegistered(Str)
358        // Returned by `conc.register` / `conc.unregister` (#444). A
359        // third `TypeMismatch` variant is reserved for when the
360        // SigId-tagged registry lands — see `conc_registry.rs` in
361        // lex-bytecode for the deferred-design note.
362        let mut ce_variants = IndexMap::new();
363        ce_variants.insert("AlreadyRegistered".into(), Some(Ty::str()));
364        ce_variants.insert("NotRegistered".into(), Some(Ty::str()));
365        e.types.insert("ConcError".into(), TypeDef {
366            params: vec![],
367            kind: TypeDefKind::Union(ce_variants),
368        });
369        for ctor in &["AlreadyRegistered", "NotRegistered"] {
370            e.ctor_to_type.insert((*ctor).into(), "ConcError".into());
371        }
372
373        // ConnRedis: opaque handle for std.redis connections (#533).
374        // Backed at runtime by an Int into a process-wide registry,
375        // same pattern as Db (std.sql) and Kv (std.kv).
376        e.types.insert("ConnRedis".into(), TypeDef { params: vec![], kind: TypeDefKind::Opaque });
377
378        e
379    }
380
381    pub fn add_user_type(&mut self, name: &str, decl: lex_ast::TypeDecl) -> Result<(), String> {
382        match &decl.definition {
383            lex_ast::TypeExpr::Union { variants } => {
384                // Resolve payloads env-aware (#963: normalizes alias-qualified
385                // dependency types in a variant payload, e.g. `AddColumn(s.Field)`)
386                // — compute the whole map under an immutable borrow first, then
387                // record constructors and insert the type.
388                let mut vmap = IndexMap::new();
389                for v in variants {
390                    let payload = v.payload.as_ref().map(|p| ty_from_canon_env(p, &decl.params, self));
391                    vmap.insert(v.name.clone(), payload);
392                }
393                for v in variants {
394                    self.ctor_to_type.insert(v.name.clone(), name.to_string());
395                }
396                self.types.insert(name.to_string(), TypeDef {
397                    params: decl.params.clone(),
398                    kind: TypeDefKind::Union(vmap),
399                });
400            }
401            other => {
402                let ty = ty_from_canon_env(other, &decl.params, self);
403                self.types.insert(name.to_string(), TypeDef {
404                    params: decl.params.clone(),
405                    kind: TypeDefKind::Alias(ty),
406                });
407            }
408        }
409        Ok(())
410    }
411}
412
413/// Convert canonical TypeExpr to internal Ty, treating type params as
414/// fresh-numbered Vars (0..n in declaration order). When instantiating, we
415/// substitute these out.
416pub fn ty_from_canon(t: &lex_ast::TypeExpr, params: &[String]) -> Ty {
417    match t {
418        lex_ast::TypeExpr::Named { name, args } => {
419            // type param?
420            if let Some(idx) = params.iter().position(|p| p == name) {
421                if !args.is_empty() {
422                    // Type params don't take args.
423                    return Ty::Con(name.clone(), args.iter().map(|a| ty_from_canon(a, params)).collect());
424                }
425                return Ty::Var(idx as u32);
426            }
427            // Primitives.
428            match name.as_str() {
429                "Int" => return Ty::int(),
430                "Float" => return Ty::float(),
431                "Bool" => return Ty::bool(),
432                "Str" => return Ty::str(),
433                "Bytes" => return Ty::bytes(),
434                "Unit" | "Nil" => return Ty::Unit,
435                "Never" => return Ty::Never,
436                "List" if args.len() == 1 => return Ty::List(Box::new(ty_from_canon(&args[0], params))),
437                // `Tuple[T0, T1, ...]` is the constructor surface for
438                // tuples; canonicalize to the structural Ty::Tuple so
439                // it unifies with `(T0, T1)` literal-tuple syntax and
440                // with std.tuple's signatures.
441                "Tuple" => return Ty::Tuple(args.iter().map(|a| ty_from_canon(a, params)).collect()),
442                _ => {}
443            }
444            Ty::Con(name.clone(), args.iter().map(|a| ty_from_canon(a, params)).collect())
445        }
446        lex_ast::TypeExpr::Record { fields } => {
447            let mut m = IndexMap::new();
448            for f in fields { m.insert(f.name.clone(), ty_from_canon(&f.ty, params)); }
449            Ty::Record(m)
450        }
451        lex_ast::TypeExpr::Tuple { items } => Ty::Tuple(items.iter().map(|t| ty_from_canon(t, params)).collect()),
452        lex_ast::TypeExpr::Function { params: ps, effects, effect_row_var, ret } => {
453            // Plumb effect args (#207).
454            let effs = EffectSet {
455                concrete: {
456                    let mut s = std::collections::BTreeSet::new();
457                    for e in effects {
458                        let arg = e.arg.as_ref().map(|a| match a {
459                            lex_ast::EffectArg::Str { value } => crate::types::EffectArg::Str(value.clone()),
460                            lex_ast::EffectArg::Int { value } => crate::types::EffectArg::Int(*value),
461                            lex_ast::EffectArg::Ident { value } => crate::types::EffectArg::Ident(value.clone()),
462                        });
463                        s.insert(crate::types::EffectKind { name: e.name.clone(), arg });
464                    }
465                    s
466                },
467                // Open-row tail: `[io | E]` where `E` is one of the enclosing
468                // type/fn's `params`. Resolve it to that param's index — the
469                // same id space as `Ty::Var(idx)`, but read back through the
470                // separate effect-substitution map at instantiation, so a
471                // type param and an effect-row param never collide.
472                var: effect_row_var
473                    .as_ref()
474                    .and_then(|name| params.iter().position(|p| p == name))
475                    .map(|i| i as u32),
476            };
477            Ty::Function {
478                params: ps.iter().map(|t| ty_from_canon(t, params)).collect(),
479                effects: effs,
480                ret: Box::new(ty_from_canon(ret, params)),
481            }
482        }
483        lex_ast::TypeExpr::Union { .. } => {
484            // Unions on the RHS of type-decls; not in arbitrary positions.
485            Ty::Unit
486        }
487        lex_ast::TypeExpr::Refined { base, .. } => {
488            // #209 slice 1: refinement types unify structurally as
489            // their base type. The predicate is parsed and stored in
490            // the AST (so `lex-vcs` content-addressing picks up
491            // refinement edits), but static discharge and runtime
492            // residual checks land in slices 2 and 3 of #209. The
493            // unification behavior here means a function declaring
494            // `Int{x | x > 0}` interoperates with plain `Int` callers
495            // — the predicate is informational until discharge is
496            // wired up.
497            ty_from_canon(base, params)
498        }
499        lex_ast::TypeExpr::RecordWithSpreads { .. } => {
500            // Caller should use ty_from_canon_env for spread resolution.
501            Ty::Unit
502        }
503    }
504}
505
506/// Like `ty_from_canon` but resolves `RecordWithSpreads` by looking up base
507/// type names in `env`. Called from `add_user_type` and `function_scheme` so
508/// that `{ ...Post, extra :: Int }` expands to a flat `Ty::Record`.
509pub fn ty_from_canon_env(t: &lex_ast::TypeExpr, params: &[String], env: &TypeEnv) -> Ty {
510    // #963: normalize alias-qualified dependency type references to the
511    // dependency module's canonical prefix (`e.DbErr` → `error_<hash>.DbErr`)
512    // so they name the same type the dependency's own signatures (and the
513    // copies inlined into its sibling modules) do. Only kicks in when a
514    // dependency was resolved as a whole package.
515    if !env.dep_alias_prefixes.is_empty() {
516        if let Some(normalized) = normalize_dep_alias(t, env) {
517            return ty_from_canon_env(&normalized, params, env);
518        }
519    }
520    match t {
521        lex_ast::TypeExpr::RecordWithSpreads { spreads, fields } => {
522            let mut m = IndexMap::new();
523            for spread_name in spreads {
524                if let Some(td) = env.types.get(spread_name.as_str()) {
525                    if let TypeDefKind::Alias(Ty::Record(spread_fields)) = &td.kind {
526                        for (k, v) in spread_fields {
527                            m.insert(k.clone(), v.clone());
528                        }
529                    }
530                }
531            }
532            for f in fields {
533                m.insert(f.name.clone(), ty_from_canon_env(&f.ty, params, env));
534            }
535            Ty::Record(m)
536        }
537        other => ty_from_canon(other, params),
538    }
539}
540
541/// Deep-rewrite alias-qualified dependency type names (`e.DbErr`) to their
542/// module's canonical prefix (`error_<hash>.DbErr`) using
543/// [`TypeEnv::dep_alias_prefixes`]. Returns `Some(rewritten)` only when a name
544/// actually changed, so the caller can proceed on the rewritten form without
545/// re-entering (the rewritten form has no alias names left). See #963.
546fn normalize_dep_alias(t: &lex_ast::TypeExpr, env: &TypeEnv) -> Option<lex_ast::TypeExpr> {
547    use lex_ast::TypeExpr as T;
548    match t {
549        T::Named { name, args } => {
550            let renamed = name
551                .split_once('.')
552                .and_then(|(alias, rest)| {
553                    env.dep_alias_prefixes.get(alias).map(|p| format!("{p}.{rest}"))
554                });
555            let new_args: Vec<Option<T>> = args.iter().map(|a| normalize_dep_alias(a, env)).collect();
556            if renamed.is_none() && new_args.iter().all(|a| a.is_none()) {
557                return None;
558            }
559            let args = args
560                .iter()
561                .zip(new_args)
562                .map(|(orig, changed)| changed.unwrap_or_else(|| orig.clone()))
563                .collect();
564            Some(T::Named { name: renamed.unwrap_or_else(|| name.clone()), args })
565        }
566        T::Record { fields } => rewrite_fields(fields, env).map(|fields| T::Record { fields }),
567        T::Tuple { items } => rewrite_items(items, env).map(|items| T::Tuple { items }),
568        T::Function { params, effects, effect_row_var, ret } => {
569            let new_params: Vec<Option<T>> = params.iter().map(|p| normalize_dep_alias(p, env)).collect();
570            let new_ret = normalize_dep_alias(ret, env);
571            if new_ret.is_none() && new_params.iter().all(|p| p.is_none()) {
572                return None;
573            }
574            let params = params
575                .iter()
576                .zip(new_params)
577                .map(|(orig, changed)| changed.unwrap_or_else(|| orig.clone()))
578                .collect();
579            Some(T::Function {
580                params,
581                effects: effects.clone(),
582                effect_row_var: effect_row_var.clone(),
583                ret: Box::new(new_ret.unwrap_or_else(|| (**ret).clone())),
584            })
585        }
586        T::Union { variants } => {
587            let rewritten: Vec<Option<T>> = variants
588                .iter()
589                .map(|v| v.payload.as_ref().and_then(|p| normalize_dep_alias(p, env)))
590                .collect();
591            if rewritten.iter().all(|r| r.is_none()) {
592                return None;
593            }
594            let variants = variants
595                .iter()
596                .zip(rewritten)
597                .map(|(v, changed)| lex_ast::UnionVariant {
598                    name: v.name.clone(),
599                    payload: changed.or_else(|| v.payload.clone()),
600                })
601                .collect();
602            Some(T::Union { variants })
603        }
604        T::RecordWithSpreads { spreads, fields } => {
605            // Spread base names could be alias-qualified too.
606            let new_spreads: Vec<String> = spreads
607                .iter()
608                .map(|s| {
609                    s.split_once('.')
610                        .and_then(|(a, rest)| env.dep_alias_prefixes.get(a).map(|p| format!("{p}.{rest}")))
611                        .unwrap_or_else(|| s.clone())
612                })
613                .collect();
614            let spreads_changed = new_spreads != *spreads;
615            let new_fields = rewrite_fields(fields, env);
616            if !spreads_changed && new_fields.is_none() {
617                return None;
618            }
619            Some(T::RecordWithSpreads {
620                spreads: new_spreads,
621                fields: new_fields.unwrap_or_else(|| fields.clone()),
622            })
623        }
624        T::Refined { base, binding, predicate } => normalize_dep_alias(base, env).map(|b| T::Refined {
625            base: Box::new(b),
626            binding: binding.clone(),
627            predicate: predicate.clone(),
628        }),
629    }
630}
631
632fn rewrite_fields(fields: &[lex_ast::TypeField], env: &TypeEnv) -> Option<Vec<lex_ast::TypeField>> {
633    let rewritten: Vec<Option<lex_ast::TypeExpr>> =
634        fields.iter().map(|f| normalize_dep_alias(&f.ty, env)).collect();
635    if rewritten.iter().all(|r| r.is_none()) {
636        return None;
637    }
638    Some(
639        fields
640            .iter()
641            .zip(rewritten)
642            .map(|(f, changed)| lex_ast::TypeField {
643                name: f.name.clone(),
644                ty: changed.unwrap_or_else(|| f.ty.clone()),
645            })
646            .collect(),
647    )
648}
649
650fn rewrite_items(items: &[lex_ast::TypeExpr], env: &TypeEnv) -> Option<Vec<lex_ast::TypeExpr>> {
651    let rewritten: Vec<Option<lex_ast::TypeExpr>> =
652        items.iter().map(|it| normalize_dep_alias(it, env)).collect();
653    if rewritten.iter().all(|r| r.is_none()) {
654        return None;
655    }
656    Some(
657        items
658            .iter()
659            .zip(rewritten)
660            .map(|(it, changed)| changed.unwrap_or_else(|| it.clone()))
661            .collect(),
662    )
663}