Skip to main content

submilli_engine/stdlib/session/
declaration.rs

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