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/// A single table operation, independent of any transport.
22#[derive(Clone, Debug, PartialEq)]
23pub enum TableOp {
24    /// Read the value at `key`.
25    Get(Symbol),
26    /// Store `value` at `key`.
27    Set(Symbol, Expr),
28    /// Whether `key` is present.
29    Has(Symbol),
30    /// Remove `key`, returning the prior value. Wire op: `del`.
31    Delete(Symbol),
32    /// All keys in this table.
33    Keys,
34    /// All entries in this table.
35    Entries,
36    /// The number of entries.
37    Len,
38    /// Remove every entry.
39    Clear,
40    /// Create a subdirectory named `name`.
41    Mkdir(Symbol),
42    /// Open the subdirectory named `name`.
43    Opendir(Symbol),
44    /// Remove the subdirectory named `name`.
45    Rmdir(Symbol),
46    /// Whether `name` is a subdirectory. Wire op: `dir?`.
47    IsDir(Symbol),
48}
49
50/// Why decoding an `Expr` into a [`TableOp`] failed.
51#[derive(Clone, Debug, PartialEq)]
52pub enum TableOpError {
53    /// The `Expr` was not a `table/<op>` call at all.
54    NotATableCall,
55    /// The operator was in the `table` namespace but is not a known op.
56    UnknownOp(String),
57    /// The op was known but had the wrong number of arguments.
58    BadArity(String),
59    /// An argument had the wrong kind for the op.
60    BadArg(String),
61}
62
63/// The wire op name for `op` (the unqualified `name` of the `table/<name>`
64/// operator).
65fn wire_name(op: &TableOp) -> &'static str {
66    match op {
67        TableOp::Get(_) => "get",
68        TableOp::Set(_, _) => "set",
69        TableOp::Has(_) => "has",
70        TableOp::Delete(_) => "del",
71        TableOp::Keys => "keys",
72        TableOp::Entries => "entries",
73        TableOp::Len => "len",
74        TableOp::Clear => "clear",
75        TableOp::Mkdir(_) => "mkdir",
76        TableOp::Opendir(_) => "opendir",
77        TableOp::Rmdir(_) => "rmdir",
78        TableOp::IsDir(_) => "dir?",
79    }
80}
81
82/// Encode a [`TableOp`] as a `table/<op>` call `Expr`.
83pub fn encode_table_op(op: &TableOp) -> Expr {
84    let args = match op {
85        TableOp::Get(key)
86        | TableOp::Has(key)
87        | TableOp::Delete(key)
88        | TableOp::Mkdir(key)
89        | TableOp::Opendir(key)
90        | TableOp::Rmdir(key)
91        | TableOp::IsDir(key) => vec![Expr::Symbol(key.clone())],
92        TableOp::Set(key, value) => vec![Expr::Symbol(key.clone()), value.clone()],
93        TableOp::Keys | TableOp::Entries | TableOp::Len | TableOp::Clear => Vec::new(),
94    };
95    Expr::Call {
96        operator: Box::new(qsym("table", wire_name(op))),
97        args,
98    }
99}
100
101/// Pull the sole [`Symbol`] argument from `args` for an op named `op`.
102fn one_key(op: &str, args: &[Expr]) -> Result<Symbol, TableOpError> {
103    match args {
104        [Expr::Symbol(key)] => Ok(key.clone()),
105        [_] => Err(TableOpError::BadArg(op.to_owned())),
106        _ => Err(TableOpError::BadArity(op.to_owned())),
107    }
108}
109
110/// Pull the sole [`Symbol`] argument from `args` and require that it names one
111/// safe, unqualified directory segment.
112fn one_dir_segment(op: &str, args: &[Expr]) -> Result<Symbol, TableOpError> {
113    let key = one_key(op, args)?;
114    if key.namespace.is_none() && is_legal_table_segment(key.name.as_ref()) {
115        Ok(key)
116    } else {
117        Err(TableOpError::BadArg(op.to_owned()))
118    }
119}
120
121/// Require that `args` is empty for a nullary op named `op`.
122fn no_args(op: &str, args: &[Expr]) -> Result<(), TableOpError> {
123    if args.is_empty() {
124        Ok(())
125    } else {
126        Err(TableOpError::BadArity(op.to_owned()))
127    }
128}
129
130/// Decode a `table/<op>` call `Expr` back into a [`TableOp`].
131pub fn decode_table_op(expr: &Expr) -> Result<TableOp, TableOpError> {
132    let Expr::Call { operator, args } = expr else {
133        return Err(TableOpError::NotATableCall);
134    };
135    let Expr::Symbol(symbol) = operator.as_ref() else {
136        return Err(TableOpError::NotATableCall);
137    };
138    if symbol.namespace.as_deref() != Some("table") {
139        return Err(TableOpError::NotATableCall);
140    }
141    let name = symbol.name.as_ref();
142    let op = match name {
143        "get" => TableOp::Get(one_key(name, args)?),
144        "set" => match args.as_slice() {
145            [Expr::Symbol(key), value] => TableOp::Set(key.clone(), value.clone()),
146            [_, _] => return Err(TableOpError::BadArg(name.to_owned())),
147            _ => return Err(TableOpError::BadArity(name.to_owned())),
148        },
149        "has" => TableOp::Has(one_key(name, args)?),
150        "del" => TableOp::Delete(one_key(name, args)?),
151        "keys" => {
152            no_args(name, args)?;
153            TableOp::Keys
154        }
155        "entries" => {
156            no_args(name, args)?;
157            TableOp::Entries
158        }
159        "len" => {
160            no_args(name, args)?;
161            TableOp::Len
162        }
163        "clear" => {
164            no_args(name, args)?;
165            TableOp::Clear
166        }
167        "mkdir" => TableOp::Mkdir(one_dir_segment(name, args)?),
168        "opendir" => TableOp::Opendir(one_dir_segment(name, args)?),
169        "rmdir" => TableOp::Rmdir(one_dir_segment(name, args)?),
170        "dir?" => TableOp::IsDir(one_dir_segment(name, args)?),
171        other => return Err(TableOpError::UnknownOp(other.to_owned())),
172    };
173    Ok(op)
174}