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}