Skip to main content

sim_table_core/
op.rs

1//! The `table/<op>` operation model and its `Expr` wire encoding.
2//!
3//! The wire spellings here match `sim-table-remote` exactly (see
4//! `remote_dir.rs` for the client `call(cx, "<op>", ...)` sites and `site.rs`
5//! for the `answer_table_request` matcher). Two spellings are NOT the obvious
6//! ones and are matched deliberately:
7//!
8//! - [`TableOp::Delete`] encodes to `table/del` (not `table/delete`);
9//! - [`TableOp::IsDir`] encodes to `table/dir?` (not `table/isdir`).
10//!
11//! Table entry ops (`get`, `set`, `has`, `del`) act on symbol-keyed table
12//! entries and therefore preserve the full [`Symbol`] surface. Dir ops
13//! (`mkdir`, `opendir`, `rmdir`, `dir?`) name child directories and therefore
14//! accept only one safe, unqualified table-path segment.
15
16use sim_kernel::{Expr, Symbol};
17use sim_value::build::qsym;
18
19use crate::path::is_legal_table_segment;
20
21/// Expected wire state for compare-exchange.
22#[derive(Clone, Debug, PartialEq)]
23pub enum CompareExpected {
24    /// The key must be absent.
25    Absent,
26    /// The key must contain this expression (including [`Expr::Nil`]).
27    Value(Expr),
28}
29
30/// Replacement wire state for compare-exchange.
31#[derive(Clone, Debug, PartialEq)]
32pub enum CompareReplacement {
33    /// Delete the key.
34    Delete,
35    /// Store this expression (including [`Expr::Nil`]).
36    Value(Expr),
37}
38
39/// A single table operation, independent of any transport.
40#[derive(Clone, Debug, PartialEq)]
41pub enum TableOp {
42    /// Read the value at `key`.
43    Get(Symbol),
44    /// Store `value` at `key`.
45    Set(Symbol, Expr),
46    /// Atomically mutate a key when its canonical expression matches.
47    CompareExchange(Symbol, CompareExpected, CompareReplacement),
48    /// Whether `key` is present.
49    Has(Symbol),
50    /// Remove `key`, returning the prior value. Wire op: `del`.
51    Delete(Symbol),
52    /// All keys in this table.
53    Keys,
54    /// All entries in this table.
55    Entries,
56    /// The number of entries.
57    Len,
58    /// Remove every entry.
59    Clear,
60    /// Create a subdirectory named `name`.
61    Mkdir(Symbol),
62    /// Open the subdirectory named `name`.
63    Opendir(Symbol),
64    /// Remove the subdirectory named `name`.
65    Rmdir(Symbol),
66    /// Whether `name` is a subdirectory. Wire op: `dir?`.
67    IsDir(Symbol),
68}
69
70/// Why decoding an `Expr` into a [`TableOp`] failed.
71#[derive(Clone, Debug, PartialEq)]
72pub enum TableOpError {
73    /// The `Expr` was not a `table/<op>` call at all.
74    NotATableCall,
75    /// The operator was in the `table` namespace but is not a known op.
76    UnknownOp(String),
77    /// The op was known but had the wrong number of arguments.
78    BadArity(String),
79    /// An argument had the wrong kind for the op.
80    BadArg(String),
81}
82
83/// The wire op name for `op` (the unqualified `name` of the `table/<name>`
84/// operator).
85fn wire_name(op: &TableOp) -> &'static str {
86    match op {
87        TableOp::Get(_) => "get",
88        TableOp::Set(_, _) => "set",
89        TableOp::CompareExchange(_, _, _) => "cas",
90        TableOp::Has(_) => "has",
91        TableOp::Delete(_) => "del",
92        TableOp::Keys => "keys",
93        TableOp::Entries => "entries",
94        TableOp::Len => "len",
95        TableOp::Clear => "clear",
96        TableOp::Mkdir(_) => "mkdir",
97        TableOp::Opendir(_) => "opendir",
98        TableOp::Rmdir(_) => "rmdir",
99        TableOp::IsDir(_) => "dir?",
100    }
101}
102
103/// Encode a [`TableOp`] as a `table/<op>` call `Expr`.
104pub fn encode_table_op(op: &TableOp) -> Expr {
105    let args = match op {
106        TableOp::Get(key)
107        | TableOp::Has(key)
108        | TableOp::Delete(key)
109        | TableOp::Mkdir(key)
110        | TableOp::Opendir(key)
111        | TableOp::Rmdir(key)
112        | TableOp::IsDir(key) => vec![Expr::Symbol(key.clone())],
113        TableOp::Set(key, value) => vec![Expr::Symbol(key.clone()), value.clone()],
114        TableOp::CompareExchange(key, expected, replacement) => vec![
115            Expr::Symbol(key.clone()),
116            encode_expected(expected),
117            encode_replacement(replacement),
118        ],
119        TableOp::Keys | TableOp::Entries | TableOp::Len | TableOp::Clear => Vec::new(),
120    };
121    Expr::Call {
122        operator: Box::new(qsym("table", wire_name(op))),
123        args,
124    }
125}
126
127fn tagged(name: &str, args: Vec<Expr>) -> Expr {
128    Expr::Call {
129        operator: Box::new(qsym("table", name)),
130        args,
131    }
132}
133
134fn encode_expected(expected: &CompareExpected) -> Expr {
135    match expected {
136        CompareExpected::Absent => tagged("absent", Vec::new()),
137        CompareExpected::Value(value) => tagged("value", vec![value.clone()]),
138    }
139}
140
141fn encode_replacement(replacement: &CompareReplacement) -> Expr {
142    match replacement {
143        CompareReplacement::Delete => tagged("delete", Vec::new()),
144        CompareReplacement::Value(value) => tagged("value", vec![value.clone()]),
145    }
146}
147
148fn decode_tag(expr: &Expr, expected: bool) -> Result<Option<Expr>, TableOpError> {
149    let Expr::Call { operator, args } = expr else {
150        return Err(TableOpError::BadArg("cas".into()));
151    };
152    let Expr::Symbol(symbol) = operator.as_ref() else {
153        return Err(TableOpError::BadArg("cas".into()));
154    };
155    if symbol.namespace.as_deref() != Some("table") {
156        return Err(TableOpError::BadArg("cas".into()));
157    }
158    match (expected, symbol.name.as_ref(), args.as_slice()) {
159        (true, "absent", []) | (false, "delete", []) => Ok(None),
160        (_, "value", [value]) => Ok(Some(value.clone())),
161        _ => Err(TableOpError::BadArg("cas".into())),
162    }
163}
164
165/// Pull the sole [`Symbol`] argument from `args` for an op named `op`.
166fn one_key(op: &str, args: &[Expr]) -> Result<Symbol, TableOpError> {
167    match args {
168        [Expr::Symbol(key)] => Ok(key.clone()),
169        [_] => Err(TableOpError::BadArg(op.to_owned())),
170        _ => Err(TableOpError::BadArity(op.to_owned())),
171    }
172}
173
174/// Pull the sole [`Symbol`] argument from `args` and require that it names one
175/// safe, unqualified directory segment.
176fn one_dir_segment(op: &str, args: &[Expr]) -> Result<Symbol, TableOpError> {
177    let key = one_key(op, args)?;
178    if key.namespace.is_none() && is_legal_table_segment(key.name.as_ref()) {
179        Ok(key)
180    } else {
181        Err(TableOpError::BadArg(op.to_owned()))
182    }
183}
184
185/// Require that `args` is empty for a nullary op named `op`.
186fn no_args(op: &str, args: &[Expr]) -> Result<(), TableOpError> {
187    if args.is_empty() {
188        Ok(())
189    } else {
190        Err(TableOpError::BadArity(op.to_owned()))
191    }
192}
193
194/// Decode a `table/<op>` call `Expr` back into a [`TableOp`].
195pub fn decode_table_op(expr: &Expr) -> Result<TableOp, TableOpError> {
196    let Expr::Call { operator, args } = expr else {
197        return Err(TableOpError::NotATableCall);
198    };
199    let Expr::Symbol(symbol) = operator.as_ref() else {
200        return Err(TableOpError::NotATableCall);
201    };
202    if symbol.namespace.as_deref() != Some("table") {
203        return Err(TableOpError::NotATableCall);
204    }
205    let name = symbol.name.as_ref();
206    let op = match name {
207        "get" => TableOp::Get(one_key(name, args)?),
208        "set" => match args.as_slice() {
209            [Expr::Symbol(key), value] => TableOp::Set(key.clone(), value.clone()),
210            [_, _] => return Err(TableOpError::BadArg(name.to_owned())),
211            _ => return Err(TableOpError::BadArity(name.to_owned())),
212        },
213        "cas" => match args.as_slice() {
214            [Expr::Symbol(key), expected, replacement] => {
215                let expected = match decode_tag(expected, true)? {
216                    None => CompareExpected::Absent,
217                    Some(value) => CompareExpected::Value(value),
218                };
219                let replacement = match decode_tag(replacement, false)? {
220                    None => CompareReplacement::Delete,
221                    Some(value) => CompareReplacement::Value(value),
222                };
223                TableOp::CompareExchange(key.clone(), expected, replacement)
224            }
225            [_, _, _] => return Err(TableOpError::BadArg(name.to_owned())),
226            _ => return Err(TableOpError::BadArity(name.to_owned())),
227        },
228        "has" => TableOp::Has(one_key(name, args)?),
229        "del" => TableOp::Delete(one_key(name, args)?),
230        "keys" => {
231            no_args(name, args)?;
232            TableOp::Keys
233        }
234        "entries" => {
235            no_args(name, args)?;
236            TableOp::Entries
237        }
238        "len" => {
239            no_args(name, args)?;
240            TableOp::Len
241        }
242        "clear" => {
243            no_args(name, args)?;
244            TableOp::Clear
245        }
246        "mkdir" => TableOp::Mkdir(one_dir_segment(name, args)?),
247        "opendir" => TableOp::Opendir(one_dir_segment(name, args)?),
248        "rmdir" => TableOp::Rmdir(one_dir_segment(name, args)?),
249        "dir?" => TableOp::IsDir(one_dir_segment(name, args)?),
250        other => return Err(TableOpError::UnknownOp(other.to_owned())),
251    };
252    Ok(op)
253}