Skip to main content

sim_value/
access.rs

1//! Reading and immutable updates for kernel `Expr` data.
2//!
3//! `field` matches an unqualified symbol key equal to `name` -- the existing
4//! majority behavior across the repo. `field_q` covers qualified keys. The
5//! split prevents a silent behavior change for callers that relied on either
6//! form. `set`/`remove` are immutable (clone, then modify) to match every
7//! current caller.
8
9use sim_kernel::{Error, Expr, Result, Symbol};
10
11use crate::build::sym;
12
13fn key_is(key: &Expr, name: &str) -> bool {
14    matches!(key, Expr::Symbol(symbol) if &*symbol.name == name && symbol.namespace.is_none())
15}
16
17/// True for a bare-symbol key OR an `Expr::String` key equal to `name`.
18fn key_is_any(key: &Expr, name: &str) -> bool {
19    key_is(key, name) || matches!(key, Expr::String(text) if text == name)
20}
21
22/// The unqualified field name spelled by a key, if it has one. Bare symbol keys
23/// report their name; string keys report their text; qualified symbol and other
24/// keys report `None`.
25fn key_name(key: &Expr) -> Option<&str> {
26    match key {
27        Expr::Symbol(symbol) if symbol.namespace.is_none() => Some(&symbol.name),
28        Expr::String(text) => Some(text),
29        _ => None,
30    }
31}
32
33/// Look up an unqualified-keyed field in a map's entry slice. The slice-level
34/// primitive [`field`] delegates to; use it when a caller already holds the
35/// `&[(Expr, Expr)]` entries (provider codecs, MCP) instead of rebuilding a map.
36pub fn entry_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Option<&'a Expr> {
37    entries
38        .iter()
39        .find_map(|(key, value)| key_is(key, name).then_some(value))
40}
41
42/// Look up a field in an entry slice, accepting a bare-symbol OR `Expr::String`
43/// key (the slice primitive behind [`field_any`]).
44pub fn entry_field_any<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Option<&'a Expr> {
45    entries
46        .iter()
47        .find_map(|(key, value)| key_is_any(key, name).then_some(value))
48}
49
50/// Look up an unqualified-keyed field by name.
51pub fn field<'a>(map: &'a Expr, name: &str) -> Option<&'a Expr> {
52    match map {
53        Expr::Map(entries) => entry_field(entries, name),
54        _ => None,
55    }
56}
57
58/// Look up a qualified-keyed field by namespace and name.
59pub fn field_q<'a>(map: &'a Expr, ns: &str, name: &str) -> Option<&'a Expr> {
60    let Expr::Map(entries) = map else {
61        return None;
62    };
63    entries.iter().find_map(|(key, value)| {
64        matches!(key, Expr::Symbol(symbol) if symbol.namespace.as_deref() == Some(ns) && &*symbol.name == name)
65            .then_some(value)
66    })
67}
68
69/// Look up a field by name, accepting either a bare-symbol key or an
70/// `Expr::String` key. Use this for provider-style records (OpenAI, Ollama,
71/// MCP) that mix symbol and string keys; use [`field`] when only the
72/// bare-symbol form is valid.
73pub fn field_any<'a>(map: &'a Expr, name: &str) -> Option<&'a Expr> {
74    match map {
75        Expr::Map(entries) => entry_field_any(entries, name),
76        _ => None,
77    }
78}
79
80/// Look up a required field, returning a context-labeled error when it is
81/// missing. Accepts either key form, matching [`field_any`].
82pub fn required<'a>(map: &'a Expr, name: &str, context: &str) -> Result<&'a Expr> {
83    field_any(map, name).ok_or_else(|| Error::Eval(format!("{context} is missing field {name}")))
84}
85
86/// Look up a required field in a map's entry slice, with a context-labeled error
87/// when missing. The slice analog of [`required`] and the one home for the
88/// `required_field(entries, name)` forks. Accepts either key form.
89pub fn entry_required<'a>(
90    entries: &'a [(Expr, Expr)],
91    name: &str,
92    context: &str,
93) -> Result<&'a Expr> {
94    entry_field_any(entries, name)
95        .ok_or_else(|| Error::Eval(format!("{context} is missing field {name}")))
96}
97
98/// Read a required string-valued field, with a context label for diagnostics.
99/// This is the one home for the `string_field`/`required_field`-style readers
100/// that coerce to `&str`; callers wanting a domain-specific error keep a thin
101/// local wrapper. Accepts either key form, matching [`field_any`].
102pub fn required_str<'a>(map: &'a Expr, name: &str, context: &str) -> Result<&'a str> {
103    as_str(required(map, name, context)?)
104        .ok_or_else(|| Error::Eval(format!("{context} field {name} is not a string")))
105}
106
107/// Read a required symbol-valued field, with a context label for diagnostics.
108pub fn required_sym(map: &Expr, name: &str, context: &str) -> Result<Symbol> {
109    match required(map, name, context)? {
110        Expr::Symbol(symbol) => Ok(symbol.clone()),
111        _ => Err(Error::Eval(format!(
112            "{context} field {name} is not a symbol"
113        ))),
114    }
115}
116
117/// Read a required bool-valued field (`Expr::Bool`), with a context label.
118pub fn required_bool(map: &Expr, name: &str, context: &str) -> Result<bool> {
119    match required(map, name, context)? {
120        Expr::Bool(value) => Ok(*value),
121        _ => Err(Error::Eval(format!("{context} field {name} is not a bool"))),
122    }
123}
124
125/// Borrow a required map-valued field's entries, with a context label. This is
126/// the context-carrying counterpart of [`map_entries`] for a named field.
127pub fn required_map<'a>(map: &'a Expr, name: &str, context: &str) -> Result<&'a [(Expr, Expr)]> {
128    match required(map, name, context)? {
129        Expr::Map(entries) => Ok(entries),
130        _ => Err(Error::Eval(format!("{context} field {name} is not a map"))),
131    }
132}
133
134/// Borrow a map value's entries, or return a `TypeMismatch` error labelled with
135/// `expected`. This is the one home for the `map_fields(expr, "...")` helper
136/// that MCP, skill, and codec crates each re-grew.
137pub fn map_entries<'a>(map: &'a Expr, expected: &'static str) -> Result<&'a [(Expr, Expr)]> {
138    match map {
139        Expr::Map(entries) => Ok(entries),
140        _ => Err(Error::TypeMismatch {
141            expected,
142            found: "non-map",
143        }),
144    }
145}
146
147/// List the field names present in `map` that are not in `known`. Keys that are
148/// neither bare symbols nor strings are ignored. Use this for open-record
149/// validation (reject or warn on unexpected fields).
150pub fn extra_fields<'a>(map: &'a Expr, known: &[&str]) -> Vec<&'a str> {
151    let Expr::Map(entries) = map else {
152        return Vec::new();
153    };
154    entries
155        .iter()
156        .filter_map(|(key, _)| key_name(key))
157        .filter(|name| !known.contains(name))
158        .collect()
159}
160
161/// Read a symbol-valued field.
162pub fn field_sym(map: &Expr, name: &str) -> Option<Symbol> {
163    match field(map, name) {
164        Some(Expr::Symbol(symbol)) => Some(symbol.clone()),
165        _ => None,
166    }
167}
168
169/// Read a string-valued field.
170pub fn field_str<'a>(map: &'a Expr, name: &str) -> Option<&'a str> {
171    field(map, name).and_then(as_str)
172}
173
174/// Read an integer-valued field.
175pub fn field_i64(map: &Expr, name: &str) -> Option<i64> {
176    field(map, name).and_then(as_i64)
177}
178
179/// Read a float-valued field.
180pub fn field_f64(map: &Expr, name: &str) -> Option<f64> {
181    field(map, name).and_then(as_f64)
182}
183
184/// Read a bool-valued field (`Expr::Bool`). Returns `None` when absent or not a
185/// bool. This is the optional counterpart of [`required_bool`].
186pub fn field_bool(map: &Expr, name: &str) -> Option<bool> {
187    match field_any(map, name) {
188        Some(Expr::Bool(value)) => Some(*value),
189        _ => None,
190    }
191}
192
193/// Read a number value's canonical literal as `i64`.
194pub fn as_i64(value: &Expr) -> Option<i64> {
195    match value {
196        Expr::Number(number) => number.canonical.parse::<i64>().ok(),
197        _ => None,
198    }
199}
200
201/// Read a number value's canonical literal as `f64`.
202pub fn as_f64(value: &Expr) -> Option<f64> {
203    match value {
204        Expr::Number(number) => number.canonical.parse::<f64>().ok(),
205        _ => None,
206    }
207}
208
209/// Borrow a string value's contents.
210pub fn as_str(value: &Expr) -> Option<&str> {
211    match value {
212        Expr::String(text) => Some(text),
213        _ => None,
214    }
215}
216
217/// Set (or insert) an unqualified-keyed field, preserving sibling keys, in a
218/// new map value.
219pub fn set(map: &Expr, name: &str, value: Expr) -> Expr {
220    let mut entries = match map {
221        Expr::Map(entries) => entries.clone(),
222        _ => Vec::new(),
223    };
224    if let Some(slot) = entries.iter_mut().find(|(key, _)| key_is(key, name)) {
225        slot.1 = value;
226    } else {
227        entries.push((sym(name), value));
228    }
229    Expr::Map(entries)
230}
231
232/// Remove an unqualified-keyed field, returning a new map value.
233pub fn remove(map: &Expr, name: &str) -> Expr {
234    let entries = match map {
235        Expr::Map(entries) => entries.clone(),
236        _ => Vec::new(),
237    };
238    Expr::Map(
239        entries
240            .into_iter()
241            .filter(|(key, _)| !key_is(key, name))
242            .collect(),
243    )
244}