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}
29
30impl TypeEnv {
31    pub fn new_with_builtins() -> Self {
32        let mut e = TypeEnv::default();
33        // Result[T, E] = Ok(T) | Err(E)
34        let mut r_variants = IndexMap::new();
35        r_variants.insert("Ok".into(), Some(Ty::Var(0))); // T
36        r_variants.insert("Err".into(), Some(Ty::Var(1))); // E
37        e.types.insert("Result".into(), TypeDef {
38            params: vec!["T".into(), "E".into()],
39            kind: TypeDefKind::Union(r_variants),
40        });
41        e.ctor_to_type.insert("Ok".into(), "Result".into());
42        e.ctor_to_type.insert("Err".into(), "Result".into());
43
44        // Option[T] = Some(T) | None
45        let mut o_variants = IndexMap::new();
46        o_variants.insert("Some".into(), Some(Ty::Var(0))); // T
47        o_variants.insert("None".into(), None);
48        e.types.insert("Option".into(), TypeDef {
49            params: vec!["T".into()],
50            kind: TypeDefKind::Union(o_variants),
51        });
52        e.ctor_to_type.insert("Some".into(), "Option".into());
53        e.ctor_to_type.insert("None".into(), "Option".into());
54
55        // Nil = Unit (alias)
56        e.types.insert("Nil".into(), TypeDef {
57            params: vec![],
58            kind: TypeDefKind::Alias(Ty::Unit),
59        });
60
61        // Map, Set: opaque-ish. We just register the names so they parse as Cons.
62        e.types.insert("Map".into(), TypeDef { params: vec!["K".into(), "V".into()], kind: TypeDefKind::Opaque });
63        e.types.insert("Set".into(), TypeDef { params: vec!["T".into()], kind: TypeDefKind::Opaque });
64
65        // SqlParam = PStr(Str) | PInt(Int) | PFloat(Float) | PBool(Bool) | PNull
66        // Typed parameter binding for std.sql (#362). Replaces the v1 List[Str]
67        // approach so callers don't have to stringify non-string values.
68        let mut sp_variants = IndexMap::new();
69        sp_variants.insert("PStr".into(),   Some(Ty::str()));
70        sp_variants.insert("PInt".into(),   Some(Ty::int()));
71        sp_variants.insert("PFloat".into(), Some(Ty::float()));
72        sp_variants.insert("PBool".into(),  Some(Ty::bool()));
73        sp_variants.insert("PNull".into(),  None);
74        e.types.insert("SqlParam".into(), TypeDef {
75            params: vec![],
76            kind: TypeDefKind::Union(sp_variants),
77        });
78        for ctor in &["PStr", "PInt", "PFloat", "PBool", "PNull"] {
79            e.ctor_to_type.insert((*ctor).into(), "SqlParam".into());
80        }
81
82        // SqlTx: opaque transaction handle (#362). Backed by the same
83        // Int registry key as Db; the type system enforces that commit/
84        // rollback can only be called on a value from sql.begin, not on
85        // a raw Db connection.
86        e.types.insert("SqlTx".into(), TypeDef { params: vec![], kind: TypeDefKind::Opaque });
87
88        // SqlError = { message :: Str, code :: Option[Str], detail :: Option[Str] }
89        // Structured error shape returned by every `std.sql` op (#380).
90        // `code` carries the SQLSTATE (Postgres) or the symbolic SQLite
91        // error name (`SQLITE_BUSY`, `SQLITE_CONSTRAINT_UNIQUE`, …) so
92        // dialect-aware retry / conflict-handling can avoid string
93        // parsing. `message` is always populated; `detail` carries a
94        // driver-side detail string when present.
95        let mut se_fields = IndexMap::new();
96        se_fields.insert("message".into(), Ty::str());
97        se_fields.insert("code".into(), Ty::Con("Option".into(), vec![Ty::str()]));
98        se_fields.insert("detail".into(), Ty::Con("Option".into(), vec![Ty::str()]));
99        e.types.insert("SqlError".into(), TypeDef {
100            params: vec![],
101            kind: TypeDefKind::Alias(Ty::Record(se_fields)),
102        });
103
104        // AeadResult = { ciphertext :: Bytes, tag :: Bytes } — return
105        // shape for every AEAD seal op in `std.crypto` (#382 AEAD slice).
106        // The auth tag is split out from the ciphertext so callers don't
107        // have to know each algorithm's tag length: AES-GCM and
108        // ChaCha20-Poly1305 both happen to be 16 bytes today, but the
109        // shape keeps that detail encapsulated.
110        let mut ar_fields = IndexMap::new();
111        ar_fields.insert("ciphertext".into(), Ty::bytes());
112        ar_fields.insert("tag".into(), Ty::bytes());
113        e.types.insert("AeadResult".into(), TypeDef {
114            params: vec![],
115            kind: TypeDefKind::Alias(Ty::Record(ar_fields)),
116        });
117
118        // UdpDatagram = { data :: Bytes, host :: Str, port :: Int } —
119        // what `net.udp_recv` hands back (#760).
120        //
121        // The sender's address is part of the value rather than something
122        // the caller has to ask for separately, because with UDP it is not
123        // optional detail: any host can send to an open socket, so a reply
124        // that does not carry who sent it cannot be safely acted on. A
125        // request/response caller must check it matches who they asked.
126        let mut dg_fields = IndexMap::new();
127        dg_fields.insert("data".into(), Ty::bytes());
128        dg_fields.insert("host".into(), Ty::str());
129        dg_fields.insert("port".into(), Ty::int());
130        e.types.insert("UdpDatagram".into(), TypeDef {
131            params: vec![],
132            kind: TypeDefKind::Alias(Ty::Record(dg_fields)),
133        });
134
135        // Iter[T]: lazy positional iterator (#364). Backed at runtime by a
136        // (List[T], Int) tuple; the Int is the current cursor index. All
137        // iter.* operations are compiler-inlined so no effect is needed.
138        e.types.insert("Iter".into(), TypeDef { params: vec!["T".into()], kind: TypeDefKind::Opaque });
139
140        // Stream[T]: opaque streaming iterator (#305 slice 3).
141        // Built and consumed exclusively through the `stream.*` and
142        // `agent.cloud_stream` effect builtins; the runtime
143        // represents a Stream value as an opaque variant carrying a
144        // handle id. Registered as Opaque so type-checking knows
145        // `Stream[Str]` parses but doesn't unwrap it structurally.
146        e.types.insert("Stream".into(), TypeDef { params: vec!["T".into()], kind: TypeDefKind::Opaque });
147
148        // Tz = Utc | Local | Offset(Int) | Iana(Str).
149        // Used by std.datetime; the variant-typed alternative to the
150        // pre-v1 stringly Tz ("UTC" / "Local" / "+05:30" / IANA name).
151        // Registered globally so users don't have to import a module
152        // to mention `Utc` / `Iana("America/New_York")` etc.
153        let mut tz_variants = IndexMap::new();
154        tz_variants.insert("Utc".into(), None);
155        tz_variants.insert("Local".into(), None);
156        tz_variants.insert("Offset".into(), Some(Ty::int()));
157        tz_variants.insert("Iana".into(), Some(Ty::str()));
158        e.types.insert("Tz".into(), TypeDef {
159            params: vec![],
160            kind: TypeDefKind::Union(tz_variants),
161        });
162        for ctor in &["Utc", "Local", "Offset", "Iana"] {
163            e.ctor_to_type.insert((*ctor).into(), "Tz".into());
164        }
165
166        // HttpError = NetworkError(Str) | TimeoutError | TlsError(Str)
167        //           | DecodeError(Str)
168        // Used by std.http; structured failure shape so callers can
169        // discriminate transport vs. timeout vs. TLS vs. body-decode
170        // errors without parsing strings.
171        let mut http_err_variants = IndexMap::new();
172        http_err_variants.insert("NetworkError".into(), Some(Ty::str()));
173        http_err_variants.insert("TimeoutError".into(), None);
174        http_err_variants.insert("TlsError".into(), Some(Ty::str()));
175        http_err_variants.insert("DecodeError".into(), Some(Ty::str()));
176        e.types.insert("HttpError".into(), TypeDef {
177            params: vec![],
178            kind: TypeDefKind::Union(http_err_variants),
179        });
180        for ctor in &["NetworkError", "TimeoutError", "TlsError", "DecodeError"] {
181            e.ctor_to_type.insert((*ctor).into(), "HttpError".into());
182        }
183
184        // HttpRequest = { method, url, headers, body, timeout_ms }.
185        // The std.http request shape. Anonymous record literals coerce
186        // to this nominal alias at every position (per the §3.13
187        // record-coercion rules), so users write
188        // `{ method: "GET", url: u, headers: map.new(), body: None,
189        // timeout_ms: None }` rather than a dedicated constructor —
190        // builders (`http.with_header` etc.) are pure transforms over
191        // the same shape.
192        let mut req_fields = IndexMap::new();
193        req_fields.insert("method".into(), Ty::str());
194        req_fields.insert("url".into(), Ty::str());
195        req_fields.insert("headers".into(), Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]));
196        req_fields.insert("body".into(), Ty::Con("Option".into(), vec![Ty::bytes()]));
197        req_fields.insert("timeout_ms".into(), Ty::Con("Option".into(), vec![Ty::int()]));
198        e.types.insert("HttpRequest".into(), TypeDef {
199            params: vec![],
200            kind: TypeDefKind::Alias(Ty::Record(req_fields)),
201        });
202
203        // HttpResponse = { status, headers, body }. Returned by every
204        // `http.{send,get,post}` happy path; also the input to
205        // `http.{json_body,text_body}`.
206        let mut resp_fields = IndexMap::new();
207        resp_fields.insert("status".into(), Ty::int());
208        resp_fields.insert("headers".into(), Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]));
209        resp_fields.insert("body".into(), Ty::bytes());
210        e.types.insert("HttpResponse".into(), TypeDef {
211            params: vec![],
212            kind: TypeDefKind::Alias(Ty::Record(resp_fields)),
213        });
214
215        // Matrix = { rows :: Int, cols :: Int, data :: List[Float] }.
216        // Used by std.math; runtime values are the F64Array fast lane,
217        // not a real record. The alias makes math.* signatures readable
218        // (`:: Matrix` instead of an inline record) and lets call sites
219        // unify nominally. Field access via `m.rows` would type-check
220        // but fail at runtime — use `math.rows / math.cols / math.get`.
221        let mut mat_fields = IndexMap::new();
222        mat_fields.insert("rows".into(), Ty::int());
223        mat_fields.insert("cols".into(), Ty::int());
224        mat_fields.insert("data".into(), Ty::List(Box::new(Ty::float())));
225        e.types.insert("Matrix".into(), TypeDef {
226            params: vec![],
227            kind: TypeDefKind::Alias(Ty::Record(mat_fields)),
228        });
229
230        // Request = { method :: Str, path :: Str, query :: Str, body :: Str,
231        //             headers :: Map[Str, Str], path_params :: Map[Str, Str] }
232        // Inbound request shape used by net.serve_fn handlers.
233        // `path_params` is populated by `net.serve_routed` from `:name`
234        // segments in the route pattern; empty under `net.serve_fn`.
235        let mut net_req_fields = IndexMap::new();
236        net_req_fields.insert("method".into(), Ty::str());
237        net_req_fields.insert("path".into(), Ty::str());
238        net_req_fields.insert("query".into(), Ty::str());
239        net_req_fields.insert("body".into(), Ty::str());
240        net_req_fields.insert("headers".into(), Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]));
241        net_req_fields.insert("path_params".into(), Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]));
242        e.types.insert("Request".into(), TypeDef {
243            params: vec![],
244            kind: TypeDefKind::Alias(Ty::Record(net_req_fields)),
245        });
246
247        // Response = { status :: Int, body :: ResponseBody, headers :: Map[Str, Str] }
248        // Outbound response shape returned by net.serve_fn handlers.
249        // #375: `body` is now an ADT instead of a bare Str. Streaming
250        // variants (BodyStream / BodyBytes) carry an `Iter[T]` that the
251        // server drains chunk-by-chunk under chunked transfer-encoding.
252        let mut rb_variants = IndexMap::new();
253        rb_variants.insert("BodyStr".into(),    Some(Ty::str()));
254        rb_variants.insert(
255            "BodyStream".into(),
256            Some(Ty::Con("Iter".into(), vec![Ty::str()])),
257        );
258        rb_variants.insert(
259            "BodyBytes".into(),
260            Some(Ty::Con("Iter".into(), vec![Ty::List(Box::new(Ty::int()))])),
261        );
262        e.types.insert("ResponseBody".into(), TypeDef {
263            params: vec![],
264            kind: TypeDefKind::Union(rb_variants),
265        });
266        for ctor in &["BodyStr", "BodyStream", "BodyBytes"] {
267            e.ctor_to_type.insert((*ctor).into(), "ResponseBody".into());
268        }
269
270        let mut net_resp_fields = IndexMap::new();
271        net_resp_fields.insert("status".into(), Ty::int());
272        net_resp_fields.insert("body".into(), Ty::Con("ResponseBody".into(), vec![]));
273        net_resp_fields.insert("headers".into(), Ty::Con("Map".into(), vec![Ty::str(), Ty::str()]));
274        e.types.insert("Response".into(), TypeDef {
275            params: vec![],
276            kind: TypeDefKind::Alias(Ty::Record(net_resp_fields)),
277        });
278
279        // WsConn = { id :: Str, path :: Str, subprotocol :: Str }
280        // Passed to every net.serve_ws_fn message handler.
281        let mut ws_conn_fields = IndexMap::new();
282        ws_conn_fields.insert("id".into(), Ty::str());
283        ws_conn_fields.insert("path".into(), Ty::str());
284        ws_conn_fields.insert("subprotocol".into(), Ty::str());
285        e.types.insert("WsConn".into(), TypeDef {
286            params: vec![],
287            kind: TypeDefKind::Alias(Ty::Record(ws_conn_fields)),
288        });
289
290        // WsMessage = WsText(Str) | WsBinary(List[Int]) | WsPing | WsClose
291        let mut ws_msg_variants = IndexMap::new();
292        ws_msg_variants.insert("WsText".into(), Some(Ty::str()));
293        ws_msg_variants.insert("WsBinary".into(), Some(Ty::List(Box::new(Ty::int()))));
294        ws_msg_variants.insert("WsPing".into(), None);
295        ws_msg_variants.insert("WsClose".into(), None);
296        e.types.insert("WsMessage".into(), TypeDef {
297            params: vec![],
298            kind: TypeDefKind::Union(ws_msg_variants),
299        });
300        for ctor in &["WsText", "WsBinary", "WsPing", "WsClose"] {
301            e.ctor_to_type.insert((*ctor).into(), "WsMessage".into());
302        }
303
304        // WsAction = WsSend(Str) | WsSendBinary(List[Int]) | WsNoOp
305        // Handlers return this to tell the runtime what to send back.
306        // Connection close is handled automatically when the runtime receives
307        // an incoming WsClose frame; handlers do not need to emit a close action.
308        let mut ws_act_variants = IndexMap::new();
309        ws_act_variants.insert("WsSend".into(), Some(Ty::str()));
310        ws_act_variants.insert("WsSendBinary".into(), Some(Ty::List(Box::new(Ty::int()))));
311        ws_act_variants.insert("WsNoOp".into(), None);
312        e.types.insert("WsAction".into(), TypeDef {
313            params: vec![],
314            kind: TypeDefKind::Union(ws_act_variants),
315        });
316        for ctor in &["WsSend", "WsSendBinary", "WsNoOp"] {
317            e.ctor_to_type.insert((*ctor).into(), "WsAction".into());
318        }
319
320        // ConcError = AlreadyRegistered(Str) | NotRegistered(Str)
321        // Returned by `conc.register` / `conc.unregister` (#444). A
322        // third `TypeMismatch` variant is reserved for when the
323        // SigId-tagged registry lands — see `conc_registry.rs` in
324        // lex-bytecode for the deferred-design note.
325        let mut ce_variants = IndexMap::new();
326        ce_variants.insert("AlreadyRegistered".into(), Some(Ty::str()));
327        ce_variants.insert("NotRegistered".into(), Some(Ty::str()));
328        e.types.insert("ConcError".into(), TypeDef {
329            params: vec![],
330            kind: TypeDefKind::Union(ce_variants),
331        });
332        for ctor in &["AlreadyRegistered", "NotRegistered"] {
333            e.ctor_to_type.insert((*ctor).into(), "ConcError".into());
334        }
335
336        // ConnRedis: opaque handle for std.redis connections (#533).
337        // Backed at runtime by an Int into a process-wide registry,
338        // same pattern as Db (std.sql) and Kv (std.kv).
339        e.types.insert("ConnRedis".into(), TypeDef { params: vec![], kind: TypeDefKind::Opaque });
340
341        e
342    }
343
344    pub fn add_user_type(&mut self, name: &str, decl: lex_ast::TypeDecl) -> Result<(), String> {
345        match &decl.definition {
346            lex_ast::TypeExpr::Union { variants } => {
347                let mut vmap = IndexMap::new();
348                for v in variants {
349                    let payload = v.payload.as_ref().map(|p| ty_from_canon(p, &decl.params));
350                    vmap.insert(v.name.clone(), payload);
351                    self.ctor_to_type.insert(v.name.clone(), name.to_string());
352                }
353                self.types.insert(name.to_string(), TypeDef {
354                    params: decl.params.clone(),
355                    kind: TypeDefKind::Union(vmap),
356                });
357            }
358            other => {
359                let ty = ty_from_canon_env(other, &decl.params, self);
360                self.types.insert(name.to_string(), TypeDef {
361                    params: decl.params.clone(),
362                    kind: TypeDefKind::Alias(ty),
363                });
364            }
365        }
366        Ok(())
367    }
368}
369
370/// Convert canonical TypeExpr to internal Ty, treating type params as
371/// fresh-numbered Vars (0..n in declaration order). When instantiating, we
372/// substitute these out.
373pub fn ty_from_canon(t: &lex_ast::TypeExpr, params: &[String]) -> Ty {
374    match t {
375        lex_ast::TypeExpr::Named { name, args } => {
376            // type param?
377            if let Some(idx) = params.iter().position(|p| p == name) {
378                if !args.is_empty() {
379                    // Type params don't take args.
380                    return Ty::Con(name.clone(), args.iter().map(|a| ty_from_canon(a, params)).collect());
381                }
382                return Ty::Var(idx as u32);
383            }
384            // Primitives.
385            match name.as_str() {
386                "Int" => return Ty::int(),
387                "Float" => return Ty::float(),
388                "Bool" => return Ty::bool(),
389                "Str" => return Ty::str(),
390                "Bytes" => return Ty::bytes(),
391                "Unit" | "Nil" => return Ty::Unit,
392                "Never" => return Ty::Never,
393                "List" if args.len() == 1 => return Ty::List(Box::new(ty_from_canon(&args[0], params))),
394                // `Tuple[T0, T1, ...]` is the constructor surface for
395                // tuples; canonicalize to the structural Ty::Tuple so
396                // it unifies with `(T0, T1)` literal-tuple syntax and
397                // with std.tuple's signatures.
398                "Tuple" => return Ty::Tuple(args.iter().map(|a| ty_from_canon(a, params)).collect()),
399                _ => {}
400            }
401            Ty::Con(name.clone(), args.iter().map(|a| ty_from_canon(a, params)).collect())
402        }
403        lex_ast::TypeExpr::Record { fields } => {
404            let mut m = IndexMap::new();
405            for f in fields { m.insert(f.name.clone(), ty_from_canon(&f.ty, params)); }
406            Ty::Record(m)
407        }
408        lex_ast::TypeExpr::Tuple { items } => Ty::Tuple(items.iter().map(|t| ty_from_canon(t, params)).collect()),
409        lex_ast::TypeExpr::Function { params: ps, effects, effect_row_var, ret } => {
410            // Plumb effect args (#207).
411            let effs = EffectSet {
412                concrete: {
413                    let mut s = std::collections::BTreeSet::new();
414                    for e in effects {
415                        let arg = e.arg.as_ref().map(|a| match a {
416                            lex_ast::EffectArg::Str { value } => crate::types::EffectArg::Str(value.clone()),
417                            lex_ast::EffectArg::Int { value } => crate::types::EffectArg::Int(*value),
418                            lex_ast::EffectArg::Ident { value } => crate::types::EffectArg::Ident(value.clone()),
419                        });
420                        s.insert(crate::types::EffectKind { name: e.name.clone(), arg });
421                    }
422                    s
423                },
424                // Open-row tail: `[io | E]` where `E` is one of the enclosing
425                // type/fn's `params`. Resolve it to that param's index — the
426                // same id space as `Ty::Var(idx)`, but read back through the
427                // separate effect-substitution map at instantiation, so a
428                // type param and an effect-row param never collide.
429                var: effect_row_var
430                    .as_ref()
431                    .and_then(|name| params.iter().position(|p| p == name))
432                    .map(|i| i as u32),
433            };
434            Ty::Function {
435                params: ps.iter().map(|t| ty_from_canon(t, params)).collect(),
436                effects: effs,
437                ret: Box::new(ty_from_canon(ret, params)),
438            }
439        }
440        lex_ast::TypeExpr::Union { .. } => {
441            // Unions on the RHS of type-decls; not in arbitrary positions.
442            Ty::Unit
443        }
444        lex_ast::TypeExpr::Refined { base, .. } => {
445            // #209 slice 1: refinement types unify structurally as
446            // their base type. The predicate is parsed and stored in
447            // the AST (so `lex-vcs` content-addressing picks up
448            // refinement edits), but static discharge and runtime
449            // residual checks land in slices 2 and 3 of #209. The
450            // unification behavior here means a function declaring
451            // `Int{x | x > 0}` interoperates with plain `Int` callers
452            // — the predicate is informational until discharge is
453            // wired up.
454            ty_from_canon(base, params)
455        }
456        lex_ast::TypeExpr::RecordWithSpreads { .. } => {
457            // Caller should use ty_from_canon_env for spread resolution.
458            Ty::Unit
459        }
460    }
461}
462
463/// Like `ty_from_canon` but resolves `RecordWithSpreads` by looking up base
464/// type names in `env`. Called from `add_user_type` and `function_scheme` so
465/// that `{ ...Post, extra :: Int }` expands to a flat `Ty::Record`.
466pub fn ty_from_canon_env(t: &lex_ast::TypeExpr, params: &[String], env: &TypeEnv) -> Ty {
467    match t {
468        lex_ast::TypeExpr::RecordWithSpreads { spreads, fields } => {
469            let mut m = IndexMap::new();
470            for spread_name in spreads {
471                if let Some(td) = env.types.get(spread_name.as_str()) {
472                    if let TypeDefKind::Alias(Ty::Record(spread_fields)) = &td.kind {
473                        for (k, v) in spread_fields {
474                            m.insert(k.clone(), v.clone());
475                        }
476                    }
477                }
478            }
479            for f in fields {
480                m.insert(f.name.clone(), ty_from_canon_env(&f.ty, params, env));
481            }
482            Ty::Record(m)
483        }
484        other => ty_from_canon(other, params),
485    }
486}