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, a display signature, and whether it
26/// mutates the field (a write the handler's implicit commit persists).
27#[derive(Debug, Clone, Copy)]
28pub struct StoreOp {
29    pub name: &'static str,
30    pub signature: &'static str,
31    /// #2194: `true` for a write (`put`, `append`, `update`, …), `false` for a
32    /// read or a lazy query root. The one source for "does this op write
33    /// state" — read by `bynk_lower::body_writes_state` (commit detection) and
34    /// by the IDE's sequence diagram (state-write notes).
35    pub mutates: bool,
36}
37
38/// A read-only operation.
39const fn op(name: &'static str, signature: &'static str) -> StoreOp {
40    StoreOp {
41        name,
42        signature,
43        mutates: false,
44    }
45}
46
47/// A mutating operation.
48const fn write(name: &'static str, signature: &'static str) -> StoreOp {
49    StoreOp {
50        name,
51        signature,
52        mutates: true,
53    }
54}
55
56/// `store Map[K, V]` (v0.82, ADR 0110) — entry-level and effectful.
57pub const MAP_STORE_OPS: &[StoreOp] = &[
58    write("put", "put(key: K, value: V) -> Effect[()]"),
59    op("get", "get(key: K) -> Effect[Option[V]]"),
60    write("remove", "remove(key: K) -> Effect[()]"),
61    op("contains", "contains(key: K) -> Effect[Bool]"),
62    op("size", "size() -> Effect[Int]"),
63    write("update", "update(key: K, f: (V) -> V) -> Effect[()]"),
64    write(
65        "upsert",
66        "upsert(key: K, initial: V, f: (V) -> V) -> Effect[()]",
67    ),
68];
69
70/// The `.entries`/`.keys`/`.values` lazy query accessors a `store Map[K, V]`
71/// field exposes (v0.158, ADR 0184) — **fields**, not method calls, so they're
72/// dispatched in `crate::checker::expressions::check_field_access` rather than
73/// a `check_store_*_op` function, and are a separate table from
74/// [`MAP_STORE_OPS`] though they share a receiver. Never offered on a held
75/// `Map[K, Connection]` (`bynk.held.query_accessor_on_held_map`) — this static
76/// table doesn't carry the value type, so callers gate that themselves.
77pub const MAP_QUERY_ACCESSORS: &[StoreOp] = &[
78    op("entries", "entries: Query[MapEntry[K, V]]"),
79    op("keys", "keys: Query[K]"),
80    op("values", "values: Query[V]"),
81];
82
83/// `store Cache[K, V]` (v0.87, ADR 0113) — the storage `Map`'s operation set,
84/// but its own table rather than an alias: every operation **but `remove`**
85/// applies TTL expiry, which reads the clock, so the handler must declare
86/// `given Clock`. That requirement is part of the operation's contract, so it is
87/// rendered in the signature (as a handler's own `given` clause is written,
88/// after the return type) — an alias to [`MAP_STORE_OPS`] would silently drop it.
89pub const CACHE_STORE_OPS: &[StoreOp] = &[
90    write("put", "put(key: K, value: V) -> Effect[()] given Clock"),
91    op("get", "get(key: K) -> Effect[Option[V]] given Clock"),
92    // The one op that does not apply expiry, and so does not read the clock.
93    write("remove", "remove(key: K) -> Effect[()]"),
94    op("contains", "contains(key: K) -> Effect[Bool] given Clock"),
95    op("size", "size() -> Effect[Int] given Clock"),
96    write(
97        "update",
98        "update(key: K, f: (V) -> V) -> Effect[()] given Clock",
99    ),
100    write(
101        "upsert",
102        "upsert(key: K, initial: V, f: (V) -> V) -> Effect[()] given Clock",
103    ),
104];
105
106/// `store Set[T]` (v0.83) — entry-level and effectful. Set algebra
107/// (`union`/`intersection`/`difference`) is deferred.
108pub const SET_STORE_OPS: &[StoreOp] = &[
109    write("add", "add(item: T) -> Effect[()]"),
110    write("remove", "remove(item: T) -> Effect[()]"),
111    op("contains", "contains(item: T) -> Effect[Bool]"),
112    op("size", "size() -> Effect[Int]"),
113];
114
115/// `store Cell[T]` (v0.98, ADR 0125) — `update` is the only method-shaped
116/// operation; a cell is read by its bare name and written with `:=`.
117pub const CELL_STORE_OPS: &[StoreOp] = &[write("update", "update(f: (T) -> T) -> Effect[()]")];
118
119/// `store Log[T]` (v0.95, ADR 0121) — `append` is the one effectful write; the
120/// time-window roots are lazy `Query[T]` builders. The general query
121/// vocabulary the roots feed into is the kernel `Query` surface, not a `Log`
122/// operation, so it is not listed here.
123pub const LOG_STORE_OPS: &[StoreOp] = &[
124    write("append", "append(entry: T) -> Effect[()]"),
125    op("since", "since(start: Instant) -> Query[T]"),
126    op("before", "before(end: Instant) -> Query[T]"),
127    op(
128        "between",
129        "between(start: Instant, end: Instant) -> Query[T]",
130    ),
131    op("recent", "recent(count: Int) -> Query[T]"),
132    op("reversed", "reversed() -> Query[T]"),
133];
134
135/// The operations of the storage kind named `head` (a `StoreKind`'s head
136/// identifier — `"Map"`, `"Cell"`, …). `Queue` is in the storage-kind
137/// catalogue but has no dispatched operations yet, so it — like an unknown
138/// head — yields none.
139pub fn ops_for(head: &str) -> &'static [StoreOp] {
140    match head {
141        "Map" => MAP_STORE_OPS,
142        "Cache" => CACHE_STORE_OPS,
143        "Set" => SET_STORE_OPS,
144        "Cell" => CELL_STORE_OPS,
145        "Log" => LOG_STORE_OPS,
146        _ => &[],
147    }
148}
149
150/// The storage kinds with dispatched operations — every head [`ops_for`]
151/// maps to a non-empty table.
152const KINDS: &[&str] = &["Map", "Cache", "Set", "Cell", "Log"];
153
154/// #2194: does `op` write a `store` field of kind `head`? `false` for a read,
155/// a query root, an unknown op or an unknown head.
156pub fn is_mutating(head: &str, op: &str) -> bool {
157    ops_for(head).iter().any(|o| o.name == op && o.mutates)
158}
159
160/// Does `op` write a `store` field of *some* kind? For a caller that already
161/// holds the op resolved against its own field's kind (a `Callee::Store`), so
162/// the op name alone is unambiguous.
163pub fn is_mutating_any(op: &str) -> bool {
164    KINDS.iter().any(|head| is_mutating(head, op))
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    /// The mutating subset per kind — pinned so a flag flipped by mistake in a
172    /// table shows up here, not as a missed commit (`body_writes_state`) or a
173    /// missing state-write note.
174    #[test]
175    fn mutating_ops_are_exactly_the_writes() {
176        let writes = |head: &str| {
177            ops_for(head)
178                .iter()
179                .filter(|o| o.mutates)
180                .map(|o| o.name)
181                .collect::<Vec<_>>()
182        };
183        assert_eq!(writes("Map"), ["put", "remove", "update", "upsert"]);
184        assert_eq!(writes("Cache"), ["put", "remove", "update", "upsert"]);
185        assert_eq!(writes("Set"), ["add", "remove"]);
186        assert_eq!(writes("Cell"), ["update"]);
187        assert_eq!(writes("Log"), ["append"]);
188        assert!(MAP_QUERY_ACCESSORS.iter().all(|o| !o.mutates));
189        assert!(is_mutating("Set", "add"));
190        assert!(!is_mutating("Map", "add"));
191        assert!(!is_mutating("Map", "get"));
192        assert!(!is_mutating("Nope", "put"));
193        assert!(is_mutating_any("append"));
194        assert!(!is_mutating_any("since"));
195    }
196
197    #[test]
198    fn ops_for_maps_the_catalogue_and_ignores_unknown_heads() {
199        // Assert a *reachable op* per kind, not a table property — `ops_for`
200        // mapping a head to the wrong table is exactly what this must catch.
201        assert!(ops_for("Map").iter().any(|o| o.name == "put"));
202        assert!(ops_for("Cache").iter().any(|o| o.name == "put"));
203        assert!(ops_for("Set").iter().any(|o| o.name == "add"));
204        assert!(ops_for("Cell").iter().all(|o| o.name == "update"));
205        assert!(ops_for("Log").iter().any(|o| o.name == "append"));
206        // A `Set` has no `put` and a `Map` no `add` — the tables are distinct.
207        assert!(!ops_for("Set").iter().any(|o| o.name == "put"));
208        assert!(!ops_for("Map").iter().any(|o| o.name == "add"));
209        // `Queue` is a catalogue kind with no dispatched ops; `Nope` is unknown.
210        assert!(ops_for("Queue").is_empty());
211        assert!(ops_for("Nope").is_empty());
212    }
213
214    /// A `Cache` op reads the clock for TTL expiry — every one but `remove` — so
215    /// its signature says so. This is the table's reason for existing separately
216    /// from [`MAP_STORE_OPS`]; an alias would drop the requirement silently.
217    #[test]
218    fn cache_signatures_carry_the_clock_requirement_except_remove() {
219        for o in CACHE_STORE_OPS {
220            let wants_clock = o.name != "remove";
221            assert_eq!(
222                o.signature.contains("given Clock"),
223                wants_clock,
224                "`Cache.{}` clock requirement is misrendered: {:?}",
225                o.name,
226                o.signature
227            );
228        }
229        // The op *names* still mirror the storage `Map`'s set exactly.
230        let names = |ops: &[StoreOp]| ops.iter().map(|o| o.name).collect::<Vec<_>>();
231        assert_eq!(names(CACHE_STORE_OPS), names(MAP_STORE_OPS));
232        // …and the storage `Map` itself requires no clock.
233        assert!(!MAP_STORE_OPS.iter().any(|o| o.signature.contains("given")));
234    }
235
236    #[test]
237    fn signatures_lead_with_their_operation_name() {
238        for ops in [
239            MAP_STORE_OPS,
240            CACHE_STORE_OPS,
241            SET_STORE_OPS,
242            CELL_STORE_OPS,
243            LOG_STORE_OPS,
244            MAP_QUERY_ACCESSORS,
245        ] {
246            assert!(!ops.is_empty());
247            for o in ops {
248                assert!(!o.name.is_empty());
249                assert!(
250                    o.signature.starts_with(o.name),
251                    "signature {:?} should lead with {:?}",
252                    o.signature,
253                    o.name
254                );
255            }
256        }
257    }
258
259    /// #596: names the exact ADR 0184 accessor set — `entries`/`keys`/`values`
260    /// — so a rename/reorder in `check_field_access`'s `map_query` dispatch
261    /// surfaces here too.
262    #[test]
263    fn map_query_accessors_are_entries_keys_values() {
264        let names: Vec<&str> = MAP_QUERY_ACCESSORS.iter().map(|o| o.name).collect();
265        assert_eq!(names, vec!["entries", "keys", "values"]);
266    }
267}