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