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}