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 authored
4//! SIM-record behavior. `field_q` covers qualified keys, and `field_any`
5//! accepts either a bare-symbol key or an `Expr::String` key for provider-style
6//! maps. The split prevents a silent behavior change for callers that relied on
7//! either form.
8//!
9//! Immutable update helpers follow the same distinction. [`set`] and
10//! [`remove`] operate on the visible field name across bare-symbol and string
11//! keys so provider/config maps do not retain a stale readable value beside a
12//! newly written one. [`set_strict`] and [`remove_strict`] are the authored
13//! SIM-record variants: they only touch bare-symbol keys and leave provider
14//! string keys alone.
15
16use sim_kernel::{Error, Expr, Result, Symbol};
17
18use crate::build::sym;
19
20fn key_is(key: &Expr, name: &str) -> bool {
21    matches!(key, Expr::Symbol(symbol) if &*symbol.name == name && symbol.namespace.is_none())
22}
23
24/// True for a bare-symbol key OR an `Expr::String` key equal to `name`.
25fn key_is_any(key: &Expr, name: &str) -> bool {
26    key_is(key, name) || matches!(key, Expr::String(text) if text == name)
27}
28
29/// The unqualified field name spelled by a key, if it has one. Bare symbol keys
30/// report their name; string keys report their text; qualified symbol and other
31/// keys report `None`.
32fn key_name(key: &Expr) -> Option<&str> {
33    match key {
34        Expr::Symbol(symbol) if symbol.namespace.is_none() => Some(&symbol.name),
35        Expr::String(text) => Some(text),
36        _ => None,
37    }
38}
39
40/// Look up an unqualified-keyed field in a map's entry slice. The slice-level
41/// primitive [`field`] delegates to; use it when a caller already holds the
42/// `&[(Expr, Expr)]` entries (provider codecs, MCP) instead of rebuilding a map.
43pub fn entry_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Option<&'a Expr> {
44    entries
45        .iter()
46        .find_map(|(key, value)| key_is(key, name).then_some(value))
47}
48
49/// Look up a field in an entry slice, accepting a bare-symbol OR `Expr::String`
50/// key (the slice primitive behind [`field_any`]).
51pub fn entry_field_any<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Option<&'a Expr> {
52    entries
53        .iter()
54        .find_map(|(key, value)| key_is_any(key, name).then_some(value))
55}
56
57/// Look up an unqualified-keyed field by name.
58pub fn field<'a>(map: &'a Expr, name: &str) -> Option<&'a Expr> {
59    match map {
60        Expr::Map(entries) => entry_field(entries, name),
61        _ => None,
62    }
63}
64
65/// Look up a qualified-keyed field by namespace and name.
66pub fn field_q<'a>(map: &'a Expr, ns: &str, name: &str) -> Option<&'a Expr> {
67    let Expr::Map(entries) = map else {
68        return None;
69    };
70    entries.iter().find_map(|(key, value)| {
71        matches!(key, Expr::Symbol(symbol) if symbol.namespace.as_deref() == Some(ns) && &*symbol.name == name)
72            .then_some(value)
73    })
74}
75
76/// Look up a field by name, accepting either a bare-symbol key or an
77/// `Expr::String` key. Use this for provider-style records (OpenAI, Ollama,
78/// MCP) that mix symbol and string keys; use [`field`] when only the
79/// bare-symbol form is valid.
80pub fn field_any<'a>(map: &'a Expr, name: &str) -> Option<&'a Expr> {
81    match map {
82        Expr::Map(entries) => entry_field_any(entries, name),
83        _ => None,
84    }
85}
86
87/// Look up a required field, returning a context-labeled error when it is
88/// missing. Accepts either key form, matching [`field_any`].
89pub fn required<'a>(map: &'a Expr, name: &str, context: &str) -> Result<&'a Expr> {
90    field_any(map, name).ok_or_else(|| Error::Eval(format!("{context} is missing field {name}")))
91}
92
93/// Look up a required field in a map's entry slice, with a context-labeled error
94/// when missing. The slice analog of [`required`] and the one home for the
95/// `required_field(entries, name)` forks. Accepts either key form.
96pub fn entry_required<'a>(
97    entries: &'a [(Expr, Expr)],
98    name: &str,
99    context: &str,
100) -> Result<&'a Expr> {
101    entry_field_any(entries, name)
102        .ok_or_else(|| Error::Eval(format!("{context} is missing field {name}")))
103}
104
105/// Build a [`Error::TypeMismatch`] whose `found` label names the actual `Expr`
106/// variant via [`expr_kind`](crate::kind::expr_kind). This is the shared helper
107/// for typed slice readers that report the actual expression kind.
108fn type_mismatch(expected: &'static str, found: &Expr) -> Error {
109    Error::TypeMismatch {
110        expected,
111        found: crate::kind::expr_kind(found),
112    }
113}
114
115/// Look up a required *bare-symbol*-keyed field in an entry slice, with a
116/// context-labeled error when missing. The bare-key analog of [`entry_required`]
117/// (which also accepts `Expr::String` keys): the typed `entry_required_*`
118/// readers build on this so they are drop-in replacements for bare-symbol
119/// `string_field`/`symbol_field`/`bool_field`/`list_field` readers in stream,
120/// music, fabric, and view crates without loosening key matching.
121fn entry_required_bare<'a>(
122    entries: &'a [(Expr, Expr)],
123    name: &str,
124    context: &str,
125) -> Result<&'a Expr> {
126    entry_field(entries, name)
127        .ok_or_else(|| Error::Eval(format!("{context} is missing field {name}")))
128}
129
130/// Read a required string-valued field from an entry slice by *bare-symbol* key.
131/// Returns a [`Error::TypeMismatch`] naming the found variant when the field is
132/// present but not an `Expr::String`. The typed, slice-level counterpart of
133/// [`required_str`] and the one home for the bare-symbol `string_field` readers.
134/// Use [`entry_required_str_any`] when string keys must also match.
135pub fn entry_required_str<'a>(
136    entries: &'a [(Expr, Expr)],
137    name: &str,
138    expected: &'static str,
139) -> Result<&'a str> {
140    match entry_required_bare(entries, name, expected)? {
141        Expr::String(value) => Ok(value),
142        other => Err(type_mismatch(expected, other)),
143    }
144}
145
146/// Read a required symbol-valued field from an entry slice by *bare-symbol* key,
147/// borrowing the [`Symbol`]. Bare-symbol counterpart of the `symbol_field` forks.
148pub fn entry_required_sym<'a>(
149    entries: &'a [(Expr, Expr)],
150    name: &str,
151    expected: &'static str,
152) -> Result<&'a Symbol> {
153    match entry_required_bare(entries, name, expected)? {
154        Expr::Symbol(value) => Ok(value),
155        other => Err(type_mismatch(expected, other)),
156    }
157}
158
159/// Read a required bool-valued field (`Expr::Bool`) from an entry slice by
160/// *bare-symbol* key. Bare-symbol counterpart of the `bool_field` forks.
161pub fn entry_required_bool(
162    entries: &[(Expr, Expr)],
163    name: &str,
164    expected: &'static str,
165) -> Result<bool> {
166    match entry_required_bare(entries, name, expected)? {
167        Expr::Bool(value) => Ok(*value),
168        other => Err(type_mismatch(expected, other)),
169    }
170}
171
172/// Borrow a required list-valued field's items (`Expr::List`) from an entry
173/// slice by *bare-symbol* key. Bare-symbol counterpart of the `list_field` forks.
174pub fn entry_required_list<'a>(
175    entries: &'a [(Expr, Expr)],
176    name: &str,
177    expected: &'static str,
178) -> Result<&'a [Expr]> {
179    match entry_required_bare(entries, name, expected)? {
180        Expr::List(items) => Ok(items),
181        other => Err(type_mismatch(expected, other)),
182    }
183}
184
185/// Namespace-agnostic sibling of [`entry_required_str`]: matches a bare-symbol
186/// OR `Expr::String` key (via [`entry_required`]/[`entry_field_any`]). Use this
187/// for provider records (OpenAI, Ollama, MCP) that mix symbol and string keys.
188pub fn entry_required_str_any<'a>(
189    entries: &'a [(Expr, Expr)],
190    name: &str,
191    expected: &'static str,
192) -> Result<&'a str> {
193    match entry_required(entries, name, expected)? {
194        Expr::String(value) => Ok(value),
195        other => Err(type_mismatch(expected, other)),
196    }
197}
198
199/// Namespace-agnostic sibling of [`entry_required_sym`] (bare-symbol OR string
200/// key), borrowing the [`Symbol`].
201pub fn entry_required_sym_any<'a>(
202    entries: &'a [(Expr, Expr)],
203    name: &str,
204    expected: &'static str,
205) -> Result<&'a Symbol> {
206    match entry_required(entries, name, expected)? {
207        Expr::Symbol(value) => Ok(value),
208        other => Err(type_mismatch(expected, other)),
209    }
210}
211
212/// Namespace-agnostic sibling of [`entry_required_bool`] (bare-symbol OR string
213/// key).
214pub fn entry_required_bool_any(
215    entries: &[(Expr, Expr)],
216    name: &str,
217    expected: &'static str,
218) -> Result<bool> {
219    match entry_required(entries, name, expected)? {
220        Expr::Bool(value) => Ok(*value),
221        other => Err(type_mismatch(expected, other)),
222    }
223}
224
225/// Namespace-agnostic sibling of [`entry_required_list`] (bare-symbol OR string
226/// key), borrowing the list items.
227pub fn entry_required_list_any<'a>(
228    entries: &'a [(Expr, Expr)],
229    name: &str,
230    expected: &'static str,
231) -> Result<&'a [Expr]> {
232    match entry_required(entries, name, expected)? {
233        Expr::List(items) => Ok(items),
234        other => Err(type_mismatch(expected, other)),
235    }
236}
237
238/// Read a required string-valued field, with a context label for diagnostics.
239/// This is the one home for the `string_field`/`required_field`-style readers
240/// that coerce to `&str`; callers wanting a domain-specific error keep a thin
241/// local wrapper. Accepts either key form, matching [`field_any`].
242pub fn required_str<'a>(map: &'a Expr, name: &str, context: &str) -> Result<&'a str> {
243    as_str(required(map, name, context)?)
244        .ok_or_else(|| Error::Eval(format!("{context} field {name} is not a string")))
245}
246
247/// Read a required symbol-valued field, with a context label for diagnostics.
248pub fn required_sym(map: &Expr, name: &str, context: &str) -> Result<Symbol> {
249    match required(map, name, context)? {
250        Expr::Symbol(symbol) => Ok(symbol.clone()),
251        _ => Err(Error::Eval(format!(
252            "{context} field {name} is not a symbol"
253        ))),
254    }
255}
256
257/// Read a required bool-valued field (`Expr::Bool`), with a context label.
258pub fn required_bool(map: &Expr, name: &str, context: &str) -> Result<bool> {
259    match required(map, name, context)? {
260        Expr::Bool(value) => Ok(*value),
261        _ => Err(Error::Eval(format!("{context} field {name} is not a bool"))),
262    }
263}
264
265/// Borrow a required map-valued field's entries, with a context label. This is
266/// the context-carrying counterpart of [`map_entries`] for a named field.
267pub fn required_map<'a>(map: &'a Expr, name: &str, context: &str) -> Result<&'a [(Expr, Expr)]> {
268    match required(map, name, context)? {
269        Expr::Map(entries) => Ok(entries),
270        _ => Err(Error::Eval(format!("{context} field {name} is not a map"))),
271    }
272}
273
274/// Borrow a map value's entries, or return a `TypeMismatch` error labelled with
275/// `expected`. This is the shared home for the `map_fields(expr, "...")` helper
276/// shape used by MCP, skill, and codec crates.
277pub fn map_entries<'a>(map: &'a Expr, expected: &'static str) -> Result<&'a [(Expr, Expr)]> {
278    match map {
279        Expr::Map(entries) => Ok(entries),
280        _ => Err(Error::TypeMismatch {
281            expected,
282            found: "non-map",
283        }),
284    }
285}
286
287/// List the field names present in `map` that are not in `known`. Keys that are
288/// neither bare symbols nor strings are ignored. Use this for open-record
289/// validation (reject or warn on unexpected fields).
290pub fn extra_fields<'a>(map: &'a Expr, known: &[&str]) -> Vec<&'a str> {
291    let Expr::Map(entries) = map else {
292        return Vec::new();
293    };
294    entries
295        .iter()
296        .filter_map(|(key, _)| key_name(key))
297        .filter(|name| !known.contains(name))
298        .collect()
299}
300
301/// Read a symbol-valued field.
302pub fn field_sym(map: &Expr, name: &str) -> Option<Symbol> {
303    match field(map, name) {
304        Some(Expr::Symbol(symbol)) => Some(symbol.clone()),
305        _ => None,
306    }
307}
308
309/// Read a string-valued field.
310pub fn field_str<'a>(map: &'a Expr, name: &str) -> Option<&'a str> {
311    field(map, name).and_then(as_str)
312}
313
314/// Read an integer-valued field.
315pub fn field_i64(map: &Expr, name: &str) -> Option<i64> {
316    field(map, name).and_then(as_i64)
317}
318
319/// Read a float-valued field.
320pub fn field_f64(map: &Expr, name: &str) -> Option<f64> {
321    field(map, name).and_then(as_f64)
322}
323
324/// Read a bool-valued field (`Expr::Bool`). Returns `None` when absent or not a
325/// bool. This is the optional counterpart of [`required_bool`].
326pub fn field_bool(map: &Expr, name: &str) -> Option<bool> {
327    match field_any(map, name) {
328        Some(Expr::Bool(value)) => Some(*value),
329        _ => None,
330    }
331}
332
333/// Read a number value's canonical literal as `i64`.
334pub fn as_i64(value: &Expr) -> Option<i64> {
335    match value {
336        Expr::Number(number) if number.domain.name.as_ref() == "i64" => {
337            number.canonical.parse::<i64>().ok()
338        }
339        _ => None,
340    }
341}
342
343/// Read a number value's canonical literal as `u64`.
344pub fn as_u64(value: &Expr) -> Option<u64> {
345    match value {
346        Expr::Number(number) if number.domain.name.as_ref() == "u64" => {
347            number.canonical.parse::<u64>().ok()
348        }
349        _ => None,
350    }
351}
352
353/// Read a number value's canonical literal as `f64`.
354pub fn as_f64(value: &Expr) -> Option<f64> {
355    match value {
356        Expr::Number(number) => number.canonical.parse::<f64>().ok(),
357        _ => None,
358    }
359}
360
361/// Borrow a string value's contents.
362pub fn as_str(value: &Expr) -> Option<&str> {
363    match value {
364        Expr::String(text) => Some(text),
365        _ => None,
366    }
367}
368
369fn set_matching<F>(map: &Expr, name: &str, value: Expr, matches: F) -> Expr
370where
371    F: Fn(&Expr, &str) -> bool,
372{
373    let entries: &[(Expr, Expr)] = match map {
374        Expr::Map(entries) => entries,
375        _ => &[],
376    };
377    let mut updated = Vec::with_capacity(entries.len().saturating_add(1));
378    let mut replacement = Some(value);
379    let mut matched = false;
380
381    for (key, existing) in entries {
382        if matches(key, name) {
383            if !matched {
384                updated.push((key.clone(), replacement.take().unwrap()));
385                matched = true;
386            }
387        } else {
388            updated.push((key.clone(), existing.clone()));
389        }
390    }
391
392    if !matched {
393        updated.push((sym(name), replacement.take().unwrap()));
394    }
395
396    Expr::Map(updated)
397}
398
399/// Set (or insert) a visible field by name, matching either a bare-symbol or
400/// string key, preserving sibling keys in a new map value.
401///
402/// When duplicates exist under the same visible name, the first matching entry
403/// keeps its original key spelling and later duplicates are dropped.
404pub fn set(map: &Expr, name: &str, value: Expr) -> Expr {
405    set_matching(map, name, value, key_is_any)
406}
407
408/// Set (or insert) a strict bare-symbol field, preserving sibling keys in a
409/// new map value.
410///
411/// This authored-SIM-record variant ignores provider-style string keys with the
412/// same visible name rather than creating an ambiguous overlay.
413pub fn set_strict(map: &Expr, name: &str, value: Expr) -> Expr {
414    if matches!(map, Expr::Map(entries) if entries.iter().any(|(key, _)| key_is_any(key, name)) && !entries.iter().any(|(key, _)| key_is(key, name)))
415    {
416        return map.clone();
417    }
418    set_matching(map, name, value, key_is)
419}
420
421fn remove_matching<F>(map: &Expr, name: &str, matches: F) -> Expr
422where
423    F: Fn(&Expr, &str) -> bool,
424{
425    let entries: &[(Expr, Expr)] = match map {
426        Expr::Map(entries) => entries,
427        _ => &[],
428    };
429    Expr::Map(
430        entries
431            .iter()
432            .filter(|(key, _)| !matches(key, name))
433            .cloned()
434            .collect(),
435    )
436}
437
438/// Remove a visible field by name, matching either a bare-symbol or string
439/// key, and returning a new map value.
440pub fn remove(map: &Expr, name: &str) -> Expr {
441    remove_matching(map, name, key_is_any)
442}
443
444/// Remove a strict bare-symbol field, returning a new map value.
445pub fn remove_strict(map: &Expr, name: &str) -> Expr {
446    remove_matching(map, name, key_is)
447}