Skip to main content

submilli_engine/stdlib/session/
declaration.rs

1//! Type surface of `submilli:session`.
2//!
3//! `get` is declared generic in `T` and **runtime-generic**: like every
4//! generic function, a call passes one `$TypeDescriptor` per type parameter
5//! in a trailing `$types` environment, carrying the type's structural
6//! predicate (`codegen::runtime_descriptors`). The host tests the
7//! stored value against it and throws a catchable `TypeError` on a mismatch,
8//! so wrong-shaped stored data never arrives statically typed. The typechecker
9//! gates what `T` may be (`session_get_gate`, matched by mangled name through
10//! [`is_checked_get`]); the declaration and that gate are a matched pair.
11
12use std::collections::BTreeMap;
13
14use crate::{
15    Dispatch, PackageDeclaration, Param, PropertySig, Span, Type, TypeKind, TypeSymbol, ValueKind,
16    ValueSymbol,
17};
18
19use super::{MAX_LIST_LIMIT, MODULE_NAME};
20
21/// The export name of the checked read. Shared with the typechecker's
22/// gate (`session_get_gate`) so the declaration and the check that keeps it
23/// sound can't drift apart.
24const SESSION_GET: &str = "get";
25
26/// Whether `mangled` is this package's checked read. Matching the
27/// package-export mangled name rather than a receiver name is what makes the
28/// typechecker's gate independent of how the symbol was imported —
29/// `session.get`, `kv.get` under an aliased namespace import, and a
30/// named-import `get` all carry this name, while a user's own `session`
31/// binding never can.
32pub fn is_checked_get(mangled: &crate::MangledName) -> bool {
33    *mangled == crate::mangle::package_symbol(MODULE_NAME, SESSION_GET)
34}
35
36/// The type parameter of [`SESSION_GET`].
37const GET_TYPE_PARAM: &str = "T";
38
39pub fn package_declaration() -> PackageDeclaration {
40    let mut defs = PackageDeclaration::with_package(MODULE_NAME);
41    insert_entry_interface(&mut defs);
42    insert_page_interface(&mut defs);
43    insert_generic_fn(
44        &mut defs,
45        SESSION_GET,
46        vec![GET_TYPE_PARAM.to_string()],
47        vec![Param::new("key", Type::String)],
48        Type::union(vec![
49            Type::TypeVar(GET_TYPE_PARAM.to_string()),
50            Type::Undefined,
51        ]),
52        "/**\n * Read a session value, checked against `T`. The stored value is tested \
53         structurally — every field, element, and union arm — and a mismatch throws a \
54         catchable `TypeError` rather than handing back a wrongly-typed value, so \
55         `get<Progress>(\"progress\")` returns a real `Progress`, `undefined` for a missing key, or throws.\n *\n \
56         * Returns `T | undefined`: a missing key returns `undefined`; a stored `null` \
57         is preserved when `T` permits it and otherwise throws a `TypeError`.\n *\n * `T` must be a type the runtime can \
58         verify: object, array, tuple, union, and primitive shapes. A bare `unknown`, a \
59         class, or an interface with methods is rejected at compile time. A generic \
60         may pass its own type parameter, or a type built from it (`get<T>` or \
61         `get<T[]>` inside `function load<T>`): the stored value is checked against \
62         the type its caller names.\n * @param key \
63         Session key. Exact UTF-16 \
64         code units; no normalization, no path semantics.\n * @capability session.read \
65         { key: $key }\n */",
66    );
67    // `get` reads its `T` as a `$TypeDescriptor` from the generic `$types`
68    // environment and checks the stored value against it host-side.
69    defs.runtime_generics
70        .insert(crate::mangle::package_symbol(MODULE_NAME, SESSION_GET));
71    insert_fn(
72        &mut defs,
73        "has",
74        vec![Param::new("key", Type::String)],
75        Type::Boolean,
76        "/**\n * Whether the session holds an entry for `key`. A stored `null` is an \
77         entry and answers `true`.\n * @param key Session key.\n * @capability \
78         session.read { key: $key }\n */",
79    );
80    insert_fn(
81        &mut defs,
82        "set",
83        vec![
84            Param::new("key", Type::String),
85            Param::new("value", Type::Unknown),
86        ],
87        Type::Void,
88        "/**\n * Store `value` under `key`, replacing any previous entry. `value` must \
89         be JSON-compatible data — `null`, a boolean, a finite number, a string, an \
90         array, or a plain object of those. A function, a `Map`/`Set`, a host handle, \
91         or a value reachable from itself is rejected and the previous entry is left \
92         intact. Traps if the value or the session exceeds its configured size \
93         limits.\n * @param key Session key.\n * @param value The data to store.\n * \
94         @capability session.write { key: $key }\n */",
95    );
96    insert_fn(
97        &mut defs,
98        "remove",
99        vec![Param::new("key", Type::String)],
100        Type::Boolean,
101        "/**\n * Delete `key`. Returns `true` when an entry existed.\n * @param key \
102         Session key.\n * @capability session.remove { key: $key }\n */",
103    );
104    insert_fn(
105        &mut defs,
106        "list",
107        vec![
108            Param::new("prefix", Type::String),
109            Param::new("limit", Type::Number),
110            Param::with_default(
111                "cursor",
112                Type::union(vec![Type::String, Type::Undefined]),
113                crate::DefaultValue::Undefined,
114            ),
115        ],
116        page_type(),
117        &format!(
118            "/**\n * Enumerate session keys starting with `prefix`, in UTF-16 code-unit \
119             order. Returns metadata only — `key` and `sizeBytes` — never the stored \
120             values; read one with `get`. Pass `\"\"` to list every key.\n *\n * Each \
121             page holds at most `limit` entries. `nextCursor` is a `string` when the \
122             listing is unfinished and `undefined` only when no further matching key remains, \
123             so page until it is `undefined` rather than until a page is short: a page can be \
124             short because the scan bound was reached, not because the keys ran out. Pass \
125             the previous page's `nextCursor` back unchanged; a cursor minted for a \
126             different prefix, or one this runtime did not issue, throws.\n *\n * Each \
127             page reflects the store as it is when the page is read, not a snapshot: a \
128             key written behind a cursor is not seen until the listing restarts.\n *\n * \
129             Keys the policy does not permit reading are omitted, and neither the entry \
130             count nor the cursor reveals them.\n * @param prefix Key prefix, matched as \
131             exact UTF-16 code units — no normalization, no path semantics. `\"\"` \
132             matches every key.\n * @param limit Maximum entries in the page; 1 to \
133             {MAX_LIST_LIMIT}. Outside that range throws.\n * @param cursor The previous \
134             page's `nextCursor`; omit it to start at the first key.\n * @capability \
135             session.list {{ prefix: $prefix }}\n * @capability \
136             session.read {{ key: $key }} per candidate key\n */"
137        ),
138    );
139    insert_fn(
140        &mut defs,
141        "registerFinalizer",
142        vec![
143            Param::new("key", Type::String),
144            Param::new("value", Type::Unknown),
145        ],
146        Type::Void,
147        "/** Register durable cleanup using this package's finalizer(key, value). Packages only. Keys are automatically namespaced by the engine; duplicate live keys are rejected.\n * @capability session.finalize { key: $key }\n */",
148    );
149    insert_fn(
150        &mut defs,
151        "unregisterFinalizer",
152        vec![Param::new("key", Type::String)],
153        Type::Void,
154        "/** Forget cleanup after releasing the resource. Packages only; keys are local to the calling package.\n * @capability session.finalize { key: $key }\n */",
155    );
156    defs
157}
158
159fn entry_type() -> Type {
160    Type::InterfaceRef {
161        mangled: crate::mangle::package_symbol(MODULE_NAME, "Entry"),
162        package: crate::Package(MODULE_NAME.to_string()),
163        name: "Entry".to_string(),
164        args: Vec::new(),
165    }
166}
167
168fn page_type() -> Type {
169    Type::InterfaceRef {
170        mangled: crate::mangle::package_symbol(MODULE_NAME, "Page"),
171        package: crate::Package(MODULE_NAME.to_string()),
172        name: "Page".to_string(),
173        args: Vec::new(),
174    }
175}
176
177fn insert_entry_interface(defs: &mut PackageDeclaration) {
178    let mut properties = BTreeMap::new();
179    insert_property(
180        &mut properties,
181        "key",
182        Type::String,
183        "/** The session key, in the exact code units it was stored under. */",
184    );
185    insert_property(
186        &mut properties,
187        "sizeBytes",
188        Type::Number,
189        "/** Serialized size of the stored value in bytes — the value alone, not the key and not the store's per-entry overhead. */",
190    );
191    insert_interface(
192        defs,
193        "Entry",
194        properties,
195        "/** One key a `list` page discloses. Carries the key and the value's size; the value itself is never part of a listing — call `get(entry.key)` for it. */",
196    );
197}
198
199fn insert_page_interface(defs: &mut PackageDeclaration) {
200    let mut properties = BTreeMap::new();
201    insert_property(
202        &mut properties,
203        "entries",
204        Type::Array(Box::new(entry_type())),
205        "/** The keys this page discloses, in UTF-16 code-unit order. May be shorter than `limit`, or empty, while `nextCursor` is still defined. */",
206    );
207    insert_property(
208        &mut properties,
209        "nextCursor",
210        Type::union(vec![Type::String, Type::Undefined]),
211        "/** Opaque cursor for the next page, or `undefined` when no further matching key remains. Pass it back to `list` unchanged, with the same prefix. */",
212    );
213    insert_interface(
214        defs,
215        "Page",
216        properties,
217        "/** One page of a `list` call: the `entries` it discloses and the `nextCursor` that continues it. Constructed only by `list`. */",
218    );
219}
220
221fn insert_interface(
222    defs: &mut PackageDeclaration,
223    name: &str,
224    properties: BTreeMap<String, PropertySig>,
225    doc: &str,
226) {
227    defs.types.insert(
228        name.to_string(),
229        TypeSymbol {
230            name: name.to_string(),
231            mangled_name: crate::mangle::package_symbol(MODULE_NAME, name),
232            declaration_span: Span::at(crate::FileId::SESSION),
233            kind: TypeKind::Interface {
234                index: None,
235                generics: Vec::new(),
236                methods: BTreeMap::new(),
237                properties,
238                dispatch: Dispatch::Direct,
239                doc: crate::doc(crate::FileId::SESSION, doc),
240            },
241        },
242    );
243}
244
245fn insert_property(
246    properties: &mut BTreeMap<String, PropertySig>,
247    name: &str,
248    ty: Type,
249    doc: &str,
250) {
251    properties.insert(
252        name.to_string(),
253        PropertySig {
254            ty,
255            readonly: true,
256            optional: false,
257            intrinsic: false,
258            doc: crate::doc(crate::FileId::SESSION, doc),
259        },
260    );
261}
262
263fn insert_fn(defs: &mut PackageDeclaration, name: &str, params: Vec<Param>, ret: Type, doc: &str) {
264    insert_generic_fn(defs, name, Vec::new(), params, ret, doc);
265}
266
267fn insert_generic_fn(
268    defs: &mut PackageDeclaration,
269    name: &str,
270    generics: Vec<String>,
271    params: Vec<Param>,
272    ret: Type,
273    doc: &str,
274) {
275    defs.values.insert(
276        name.to_string(),
277        ValueSymbol {
278            name: name.to_string(),
279            mangled_name: crate::mangle::package_symbol(MODULE_NAME, name),
280            declaration_span: Span::at(crate::FileId::SESSION),
281            kind: ValueKind::Function {
282                generics,
283                params,
284                ret,
285                type_predicate: None,
286                doc: crate::doc(crate::FileId::SESSION, doc),
287            },
288        },
289    );
290}