Skip to main content

bynk_check/
store_ops.rs

1//! #611: the enumerable storage-operation registry.
2//!
3//! The entry operations of the `store` kinds (`Cell`/`Map`/`Set`/`Cache`/
4//! `Log`) are dispatched by the checker in [`crate::checker`]'s
5//! `check_store_*_op` functions, where the operation names live in `match`
6//! arms — authoritative for *typing*, but not enumerable. This module is the
7//! enumerable view the LSP reads for hover on a store operation, mirroring
8//! [`crate::kernel_methods`]'s relationship to the value-kernel dispatch.
9//!
10//! The signatures are human-readable Bynk-surface display strings, generic in
11//! the kind's element/key/value type (`K`/`V`/`T` — the store field's declared
12//! kind grounds them at the hover site).
13//!
14//! **What the drift test pins, and what it does not.**
15//! `store_op_registry_pins_dispatch` drives every listed operation through the
16//! real checker on a `store` field of the matching kind and asserts none is
17//! rejected as `unknown_op` — so the table cannot list a **phantom** operation.
18//! It does not bite the other way: an operation added to a `check_store_*_op`
19//! arm later fails nothing here, and this table will silently **under-list** it
20//! (hover then falls through — a missing hover, not a wrong one). Nor does it
21//! check the signature **strings**, which are display-only and unread by the
22//! checker; those are pinned by eye against the `check_store_*_op` arms.
23//! [`crate::kernel_methods`] has the same shape and the same two limits.
24
25/// One storage operation: its name and a display signature.
26#[derive(Debug, Clone, Copy)]
27pub struct StoreOp {
28    pub name: &'static str,
29    pub signature: &'static str,
30}
31
32const fn op(name: &'static str, signature: &'static str) -> StoreOp {
33    StoreOp { name, signature }
34}
35
36/// `store Map[K, V]` (v0.82, ADR 0110) — entry-level and effectful.
37pub const MAP_STORE_OPS: &[StoreOp] = &[
38    op("put", "put(key: K, value: V) -> Effect[()]"),
39    op("get", "get(key: K) -> Effect[Option[V]]"),
40    op("remove", "remove(key: K) -> Effect[()]"),
41    op("contains", "contains(key: K) -> Effect[Bool]"),
42    op("size", "size() -> Effect[Int]"),
43    op("update", "update(key: K, f: (V) -> V) -> Effect[()]"),
44    op(
45        "upsert",
46        "upsert(key: K, initial: V, f: (V) -> V) -> Effect[()]",
47    ),
48];
49
50/// The `.entries`/`.keys`/`.values` lazy query accessors a `store Map[K, V]`
51/// field exposes (v0.158, ADR 0184) — **fields**, not method calls, so they're
52/// dispatched in `crate::checker::expressions::check_field_access` rather than
53/// a `check_store_*_op` function, and are a separate table from
54/// [`MAP_STORE_OPS`] though they share a receiver. Never offered on a held
55/// `Map[K, Connection]` (`bynk.held.query_accessor_on_held_map`) — this static
56/// table doesn't carry the value type, so callers gate that themselves.
57pub const MAP_QUERY_ACCESSORS: &[StoreOp] = &[
58    op("entries", "entries: Query[MapEntry[K, V]]"),
59    op("keys", "keys: Query[K]"),
60    op("values", "values: Query[V]"),
61];
62
63/// `store Cache[K, V]` (v0.87, ADR 0113) — the storage `Map`'s operation set,
64/// but its own table rather than an alias: every operation **but `remove`**
65/// applies TTL expiry, which reads the clock, so the handler must declare
66/// `given Clock`. That requirement is part of the operation's contract, so it is
67/// rendered in the signature (as a handler's own `given` clause is written,
68/// after the return type) — an alias to [`MAP_STORE_OPS`] would silently drop it.
69pub const CACHE_STORE_OPS: &[StoreOp] = &[
70    op("put", "put(key: K, value: V) -> Effect[()] given Clock"),
71    op("get", "get(key: K) -> Effect[Option[V]] given Clock"),
72    // The one op that does not apply expiry, and so does not read the clock.
73    op("remove", "remove(key: K) -> Effect[()]"),
74    op("contains", "contains(key: K) -> Effect[Bool] given Clock"),
75    op("size", "size() -> Effect[Int] given Clock"),
76    op(
77        "update",
78        "update(key: K, f: (V) -> V) -> Effect[()] given Clock",
79    ),
80    op(
81        "upsert",
82        "upsert(key: K, initial: V, f: (V) -> V) -> Effect[()] given Clock",
83    ),
84];
85
86/// `store Set[T]` (v0.83) — entry-level and effectful. Set algebra
87/// (`union`/`intersection`/`difference`) is deferred.
88pub const SET_STORE_OPS: &[StoreOp] = &[
89    op("add", "add(item: T) -> Effect[()]"),
90    op("remove", "remove(item: T) -> Effect[()]"),
91    op("contains", "contains(item: T) -> Effect[Bool]"),
92    op("size", "size() -> Effect[Int]"),
93];
94
95/// `store Cell[T]` (v0.98, ADR 0125) — `update` is the only method-shaped
96/// operation; a cell is read by its bare name and written with `:=`.
97pub const CELL_STORE_OPS: &[StoreOp] = &[op("update", "update(f: (T) -> T) -> Effect[()]")];
98
99/// `store Log[T]` (v0.95, ADR 0121) — `append` is the one effectful write; the
100/// time-window roots are lazy `Query[T]` builders. The general query
101/// vocabulary the roots feed into is the kernel `Query` surface, not a `Log`
102/// operation, so it is not listed here.
103pub const LOG_STORE_OPS: &[StoreOp] = &[
104    op("append", "append(entry: T) -> Effect[()]"),
105    op("since", "since(start: Instant) -> Query[T]"),
106    op("before", "before(end: Instant) -> Query[T]"),
107    op(
108        "between",
109        "between(start: Instant, end: Instant) -> Query[T]",
110    ),
111    op("recent", "recent(count: Int) -> Query[T]"),
112    op("reversed", "reversed() -> Query[T]"),
113];
114
115/// The operations of the storage kind named `head` (a `StoreKind`'s head
116/// identifier — `"Map"`, `"Cell"`, …). `Queue` is in the storage-kind
117/// catalogue but has no dispatched operations yet, so it — like an unknown
118/// head — yields none.
119pub fn ops_for(head: &str) -> &'static [StoreOp] {
120    match head {
121        "Map" => MAP_STORE_OPS,
122        "Cache" => CACHE_STORE_OPS,
123        "Set" => SET_STORE_OPS,
124        "Cell" => CELL_STORE_OPS,
125        "Log" => LOG_STORE_OPS,
126        _ => &[],
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn ops_for_maps_the_catalogue_and_ignores_unknown_heads() {
136        // Assert a *reachable op* per kind, not a table property — `ops_for`
137        // mapping a head to the wrong table is exactly what this must catch.
138        assert!(ops_for("Map").iter().any(|o| o.name == "put"));
139        assert!(ops_for("Cache").iter().any(|o| o.name == "put"));
140        assert!(ops_for("Set").iter().any(|o| o.name == "add"));
141        assert!(ops_for("Cell").iter().all(|o| o.name == "update"));
142        assert!(ops_for("Log").iter().any(|o| o.name == "append"));
143        // A `Set` has no `put` and a `Map` no `add` — the tables are distinct.
144        assert!(!ops_for("Set").iter().any(|o| o.name == "put"));
145        assert!(!ops_for("Map").iter().any(|o| o.name == "add"));
146        // `Queue` is a catalogue kind with no dispatched ops; `Nope` is unknown.
147        assert!(ops_for("Queue").is_empty());
148        assert!(ops_for("Nope").is_empty());
149    }
150
151    /// A `Cache` op reads the clock for TTL expiry — every one but `remove` — so
152    /// its signature says so. This is the table's reason for existing separately
153    /// from [`MAP_STORE_OPS`]; an alias would drop the requirement silently.
154    #[test]
155    fn cache_signatures_carry_the_clock_requirement_except_remove() {
156        for o in CACHE_STORE_OPS {
157            let wants_clock = o.name != "remove";
158            assert_eq!(
159                o.signature.contains("given Clock"),
160                wants_clock,
161                "`Cache.{}` clock requirement is misrendered: {:?}",
162                o.name,
163                o.signature
164            );
165        }
166        // The op *names* still mirror the storage `Map`'s set exactly.
167        let names = |ops: &[StoreOp]| ops.iter().map(|o| o.name).collect::<Vec<_>>();
168        assert_eq!(names(CACHE_STORE_OPS), names(MAP_STORE_OPS));
169        // …and the storage `Map` itself requires no clock.
170        assert!(!MAP_STORE_OPS.iter().any(|o| o.signature.contains("given")));
171    }
172
173    #[test]
174    fn signatures_lead_with_their_operation_name() {
175        for ops in [
176            MAP_STORE_OPS,
177            CACHE_STORE_OPS,
178            SET_STORE_OPS,
179            CELL_STORE_OPS,
180            LOG_STORE_OPS,
181            MAP_QUERY_ACCESSORS,
182        ] {
183            assert!(!ops.is_empty());
184            for o in ops {
185                assert!(!o.name.is_empty());
186                assert!(
187                    o.signature.starts_with(o.name),
188                    "signature {:?} should lead with {:?}",
189                    o.signature,
190                    o.name
191                );
192            }
193        }
194    }
195
196    /// #596: names the exact ADR 0184 accessor set — `entries`/`keys`/`values`
197    /// — so a rename/reorder in `check_field_access`'s `map_query` dispatch
198    /// surfaces here too.
199    #[test]
200    fn map_query_accessors_are_entries_keys_values() {
201        let names: Vec<&str> = MAP_QUERY_ACCESSORS.iter().map(|o| o.name).collect();
202        assert_eq!(names, vec!["entries", "keys", "values"]);
203    }
204}