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/// Build a [`Error::TypeMismatch`] whose `found` label names the actual `Expr`
99/// variant via [`expr_kind`](crate::kind::expr_kind). The one home for the
100/// `Err(Error::TypeMismatch { expected, found: expr_kind(other) })` tail that
101/// every typed slice reader across the constellation re-grew.
102fn type_mismatch(expected: &'static str, found: &Expr) -> Error {
103    Error::TypeMismatch {
104        expected,
105        found: crate::kind::expr_kind(found),
106    }
107}
108
109/// Look up a required *bare-symbol*-keyed field in an entry slice, with a
110/// context-labeled error when missing. The bare-key analog of [`entry_required`]
111/// (which also accepts `Expr::String` keys): the typed `entry_required_*`
112/// readers build on this so they are drop-in replacements for the bare-symbol
113/// `string_field`/`symbol_field`/`bool_field`/`list_field` forks that stream,
114/// music, fabric, and view crates each re-grew without loosening key matching.
115fn entry_required_bare<'a>(
116    entries: &'a [(Expr, Expr)],
117    name: &str,
118    context: &str,
119) -> Result<&'a Expr> {
120    entry_field(entries, name)
121        .ok_or_else(|| Error::Eval(format!("{context} is missing field {name}")))
122}
123
124/// Read a required string-valued field from an entry slice by *bare-symbol* key.
125/// Returns a [`Error::TypeMismatch`] naming the found variant when the field is
126/// present but not an `Expr::String`. The typed, slice-level counterpart of
127/// [`required_str`] and the one home for the bare-symbol `string_field` readers.
128/// Use [`entry_required_str_any`] when string keys must also match.
129pub fn entry_required_str<'a>(
130    entries: &'a [(Expr, Expr)],
131    name: &str,
132    expected: &'static str,
133) -> Result<&'a str> {
134    match entry_required_bare(entries, name, expected)? {
135        Expr::String(value) => Ok(value),
136        other => Err(type_mismatch(expected, other)),
137    }
138}
139
140/// Read a required symbol-valued field from an entry slice by *bare-symbol* key,
141/// borrowing the [`Symbol`]. Bare-symbol counterpart of the `symbol_field` forks.
142pub fn entry_required_sym<'a>(
143    entries: &'a [(Expr, Expr)],
144    name: &str,
145    expected: &'static str,
146) -> Result<&'a Symbol> {
147    match entry_required_bare(entries, name, expected)? {
148        Expr::Symbol(value) => Ok(value),
149        other => Err(type_mismatch(expected, other)),
150    }
151}
152
153/// Read a required bool-valued field (`Expr::Bool`) from an entry slice by
154/// *bare-symbol* key. Bare-symbol counterpart of the `bool_field` forks.
155pub fn entry_required_bool(
156    entries: &[(Expr, Expr)],
157    name: &str,
158    expected: &'static str,
159) -> Result<bool> {
160    match entry_required_bare(entries, name, expected)? {
161        Expr::Bool(value) => Ok(*value),
162        other => Err(type_mismatch(expected, other)),
163    }
164}
165
166/// Borrow a required list-valued field's items (`Expr::List`) from an entry
167/// slice by *bare-symbol* key. Bare-symbol counterpart of the `list_field` forks.
168pub fn entry_required_list<'a>(
169    entries: &'a [(Expr, Expr)],
170    name: &str,
171    expected: &'static str,
172) -> Result<&'a [Expr]> {
173    match entry_required_bare(entries, name, expected)? {
174        Expr::List(items) => Ok(items),
175        other => Err(type_mismatch(expected, other)),
176    }
177}
178
179/// Namespace-agnostic sibling of [`entry_required_str`]: matches a bare-symbol
180/// OR `Expr::String` key (via [`entry_required`]/[`entry_field_any`]). Use this
181/// for provider records (OpenAI, Ollama, MCP) that mix symbol and string keys.
182pub fn entry_required_str_any<'a>(
183    entries: &'a [(Expr, Expr)],
184    name: &str,
185    expected: &'static str,
186) -> Result<&'a str> {
187    match entry_required(entries, name, expected)? {
188        Expr::String(value) => Ok(value),
189        other => Err(type_mismatch(expected, other)),
190    }
191}
192
193/// Namespace-agnostic sibling of [`entry_required_sym`] (bare-symbol OR string
194/// key), borrowing the [`Symbol`].
195pub fn entry_required_sym_any<'a>(
196    entries: &'a [(Expr, Expr)],
197    name: &str,
198    expected: &'static str,
199) -> Result<&'a Symbol> {
200    match entry_required(entries, name, expected)? {
201        Expr::Symbol(value) => Ok(value),
202        other => Err(type_mismatch(expected, other)),
203    }
204}
205
206/// Namespace-agnostic sibling of [`entry_required_bool`] (bare-symbol OR string
207/// key).
208pub fn entry_required_bool_any(
209    entries: &[(Expr, Expr)],
210    name: &str,
211    expected: &'static str,
212) -> Result<bool> {
213    match entry_required(entries, name, expected)? {
214        Expr::Bool(value) => Ok(*value),
215        other => Err(type_mismatch(expected, other)),
216    }
217}
218
219/// Namespace-agnostic sibling of [`entry_required_list`] (bare-symbol OR string
220/// key), borrowing the list items.
221pub fn entry_required_list_any<'a>(
222    entries: &'a [(Expr, Expr)],
223    name: &str,
224    expected: &'static str,
225) -> Result<&'a [Expr]> {
226    match entry_required(entries, name, expected)? {
227        Expr::List(items) => Ok(items),
228        other => Err(type_mismatch(expected, other)),
229    }
230}
231
232/// Read a required string-valued field, with a context label for diagnostics.
233/// This is the one home for the `string_field`/`required_field`-style readers
234/// that coerce to `&str`; callers wanting a domain-specific error keep a thin
235/// local wrapper. Accepts either key form, matching [`field_any`].
236pub fn required_str<'a>(map: &'a Expr, name: &str, context: &str) -> Result<&'a str> {
237    as_str(required(map, name, context)?)
238        .ok_or_else(|| Error::Eval(format!("{context} field {name} is not a string")))
239}
240
241/// Read a required symbol-valued field, with a context label for diagnostics.
242pub fn required_sym(map: &Expr, name: &str, context: &str) -> Result<Symbol> {
243    match required(map, name, context)? {
244        Expr::Symbol(symbol) => Ok(symbol.clone()),
245        _ => Err(Error::Eval(format!(
246            "{context} field {name} is not a symbol"
247        ))),
248    }
249}
250
251/// Read a required bool-valued field (`Expr::Bool`), with a context label.
252pub fn required_bool(map: &Expr, name: &str, context: &str) -> Result<bool> {
253    match required(map, name, context)? {
254        Expr::Bool(value) => Ok(*value),
255        _ => Err(Error::Eval(format!("{context} field {name} is not a bool"))),
256    }
257}
258
259/// Borrow a required map-valued field's entries, with a context label. This is
260/// the context-carrying counterpart of [`map_entries`] for a named field.
261pub fn required_map<'a>(map: &'a Expr, name: &str, context: &str) -> Result<&'a [(Expr, Expr)]> {
262    match required(map, name, context)? {
263        Expr::Map(entries) => Ok(entries),
264        _ => Err(Error::Eval(format!("{context} field {name} is not a map"))),
265    }
266}
267
268/// Borrow a map value's entries, or return a `TypeMismatch` error labelled with
269/// `expected`. This is the one home for the `map_fields(expr, "...")` helper
270/// that MCP, skill, and codec crates each re-grew.
271pub fn map_entries<'a>(map: &'a Expr, expected: &'static str) -> Result<&'a [(Expr, Expr)]> {
272    match map {
273        Expr::Map(entries) => Ok(entries),
274        _ => Err(Error::TypeMismatch {
275            expected,
276            found: "non-map",
277        }),
278    }
279}
280
281/// List the field names present in `map` that are not in `known`. Keys that are
282/// neither bare symbols nor strings are ignored. Use this for open-record
283/// validation (reject or warn on unexpected fields).
284pub fn extra_fields<'a>(map: &'a Expr, known: &[&str]) -> Vec<&'a str> {
285    let Expr::Map(entries) = map else {
286        return Vec::new();
287    };
288    entries
289        .iter()
290        .filter_map(|(key, _)| key_name(key))
291        .filter(|name| !known.contains(name))
292        .collect()
293}
294
295/// Read a symbol-valued field.
296pub fn field_sym(map: &Expr, name: &str) -> Option<Symbol> {
297    match field(map, name) {
298        Some(Expr::Symbol(symbol)) => Some(symbol.clone()),
299        _ => None,
300    }
301}
302
303/// Read a string-valued field.
304pub fn field_str<'a>(map: &'a Expr, name: &str) -> Option<&'a str> {
305    field(map, name).and_then(as_str)
306}
307
308/// Read an integer-valued field.
309pub fn field_i64(map: &Expr, name: &str) -> Option<i64> {
310    field(map, name).and_then(as_i64)
311}
312
313/// Read a float-valued field.
314pub fn field_f64(map: &Expr, name: &str) -> Option<f64> {
315    field(map, name).and_then(as_f64)
316}
317
318/// Read a bool-valued field (`Expr::Bool`). Returns `None` when absent or not a
319/// bool. This is the optional counterpart of [`required_bool`].
320pub fn field_bool(map: &Expr, name: &str) -> Option<bool> {
321    match field_any(map, name) {
322        Some(Expr::Bool(value)) => Some(*value),
323        _ => None,
324    }
325}
326
327/// Read a number value's canonical literal as `i64`.
328pub fn as_i64(value: &Expr) -> Option<i64> {
329    match value {
330        Expr::Number(number) => number.canonical.parse::<i64>().ok(),
331        _ => None,
332    }
333}
334
335/// Read a number value's canonical literal as `f64`.
336pub fn as_f64(value: &Expr) -> Option<f64> {
337    match value {
338        Expr::Number(number) => number.canonical.parse::<f64>().ok(),
339        _ => None,
340    }
341}
342
343/// Borrow a string value's contents.
344pub fn as_str(value: &Expr) -> Option<&str> {
345    match value {
346        Expr::String(text) => Some(text),
347        _ => None,
348    }
349}
350
351/// Set (or insert) an unqualified-keyed field, preserving sibling keys, in a
352/// new map value.
353pub fn set(map: &Expr, name: &str, value: Expr) -> Expr {
354    let mut entries = match map {
355        Expr::Map(entries) => entries.clone(),
356        _ => Vec::new(),
357    };
358    if let Some(slot) = entries.iter_mut().find(|(key, _)| key_is(key, name)) {
359        slot.1 = value;
360    } else {
361        entries.push((sym(name), value));
362    }
363    Expr::Map(entries)
364}
365
366/// Remove an unqualified-keyed field, returning a new map value.
367pub fn remove(map: &Expr, name: &str) -> Expr {
368    let entries = match map {
369        Expr::Map(entries) => entries.clone(),
370        _ => Vec::new(),
371    };
372    Expr::Map(
373        entries
374            .into_iter()
375            .filter(|(key, _)| !key_is(key, name))
376            .collect(),
377    )
378}