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