Skip to main content

viva_genapi/
nodemap.rs

1//! NodeMap implementation for runtime feature access.
2
3use std::cell::Cell;
4use std::collections::{HashMap, HashSet, hash_map::Entry as HashMapEntry};
5
6use tracing::{debug, trace, warn};
7use viva_genapi_xml::{
8    AccessMode, AddressTerm, Addressing, ByteOrder, EnumEntryDecl, EnumValueSrc, FloatEncoding,
9    FormulaBindings, IndexOffset, NodeDecl, PredicateRefs, Sign, SkippedNode, Visibility, XmlModel,
10};
11
12use crate::bitops::{extract, insert};
13use crate::conversions::{
14    apply_scale, bytes_to_i64, decode_ieee754, encode_bitfield_value, encode_float, encode_ieee754,
15    get_raw_or_read, i64_to_bytes, interpret_bitfield_value, map_bitops_error, round_to_i64,
16};
17use crate::nodes::{
18    BooleanNode, CategoryNode, CommandNode, ConverterNode, EnumMapping, EnumNode, FloatNode,
19    IntConverterNode, IntegerNode, Node, RegisterNode, SkNode, StringNode,
20};
21use crate::swissknife::{
22    AstNode as SkAst, EvalError as SkEvalError, EvalMode, Value as SkValue, collect_identifiers,
23    evaluate as eval_ast, is_builtin_constant, parse_expression, substitute,
24};
25use crate::{GenApiError, RegisterIo, SkOutput};
26
27/// Runtime nodemap built from an [`XmlModel`] capable of reading and writing
28/// feature values via a [`RegisterIo`] transport.
29#[derive(Debug)]
30pub struct NodeMap {
31    version: String,
32    nodes: HashMap<String, Node>,
33    dependents: HashMap<String, Vec<String>>,
34    skipped: Vec<SkippedNode>,
35    generation: Cell<u64>,
36}
37
38fn register_addressing_dependency(
39    dependents: &mut HashMap<String, Vec<String>>,
40    node_name: &str,
41    addressing: &Addressing,
42) {
43    for provider in addressing.referenced_nodes() {
44        dependents
45            .entry(provider.to_string())
46            .or_default()
47            .push(node_name.to_string());
48    }
49}
50
51fn register_predicate_dependencies(
52    dependents: &mut HashMap<String, Vec<String>>,
53    node_name: &str,
54    predicates: &PredicateRefs,
55) {
56    for provider in predicates.references() {
57        dependents
58            .entry(provider.to_string())
59            .or_default()
60            .push(node_name.to_string());
61    }
62}
63
64fn ensure_readable(access: &AccessMode, name: &str) -> Result<(), GenApiError> {
65    if matches!(access, AccessMode::WO) {
66        return Err(GenApiError::Access(name.to_string()));
67    }
68    Ok(())
69}
70
71/// Refuse a register bound to a port we do not route.
72///
73/// `<pPort>` selects which port a register's address is relative to. Absent, or
74/// `"Device"`, means the device's own register space; anything else — a chunk
75/// port, an event port, a serial port — is a different address space entirely.
76///
77/// Reading such a node through the device port would not fail. It would return
78/// whatever happens to live at that address: the Micro-Epsilon scanCONTROL's
79/// three `Chunk*Results` registers all sit at address `0x0`, so a device-port
80/// read hands back GVCP bootstrap registers dressed as measurement data. That
81/// is the silent-wrong-answer failure ADR-0018 exists to refuse, so the node is
82/// parsed and listed but not readable until GA-12 routes ports properly.
83///
84/// This is deliberately stricter than the ~200 `IntReg`/`FloatReg`/`StringReg`
85/// nodes on non-device ports that this crate already exposes without a guard.
86/// The asymmetry is GA-12's debt, not a new inconsistency: new code conforms,
87/// and the old code is on the list.
88fn ensure_device_port(name: &str, port: Option<&str>) -> Result<(), GenApiError> {
89    match port {
90        None => Ok(()),
91        Some(p) if p.eq_ignore_ascii_case("Device") => Ok(()),
92        Some(p) => Err(GenApiError::Unavailable(format!(
93            "register '{name}' is bound to port '{p}'; \
94             non-device ports are not routed yet (GA-12)"
95        ))),
96    }
97}
98
99fn ensure_writable(access: &AccessMode, name: &str) -> Result<(), GenApiError> {
100    if matches!(access, AccessMode::RO) {
101        return Err(GenApiError::Access(name.to_string()));
102    }
103    Ok(())
104}
105
106impl NodeMap {
107    /// Return the schema version string associated with the XML description.
108    pub fn version(&self) -> &str {
109        &self.version
110    }
111
112    /// Fetch a node by name for inspection.
113    pub fn node(&self, name: &str) -> Option<&Node> {
114        self.nodes.get(name)
115    }
116
117    /// Return an iterator over all node names in the map.
118    pub fn node_names(&self) -> impl Iterator<Item = &str> {
119        self.nodes.keys().map(|s| s.as_str())
120    }
121
122    /// Return the list of nodes that should be invalidated when `name` changes.
123    ///
124    /// Returns an empty slice if the node has no dependents.
125    pub fn dependents(&self, name: &str) -> &[String] {
126        self.dependents
127            .get(name)
128            .map(|v| v.as_slice())
129            .unwrap_or(&[])
130    }
131
132    /// Return all category nodes as `(name, children)` pairs.
133    pub fn categories(&self) -> Vec<(&str, &[String])> {
134        self.nodes
135            .values()
136            .filter_map(|node| match node {
137                Node::Category(cat) => Some((cat.name.as_str(), cat.children.as_slice())),
138                _ => None,
139            })
140            .collect()
141    }
142
143    /// Return names of nodes visible at the given level or below.
144    ///
145    /// A node with `Visibility::Expert` is visible at level `Expert` and `Guru`,
146    /// but not at `Beginner`.
147    pub fn nodes_at_visibility(&self, level: Visibility) -> Vec<&str> {
148        self.nodes
149            .iter()
150            .filter(|(_, node)| node.visibility() <= level)
151            .map(|(name, _)| name.as_str())
152            .collect()
153    }
154
155    /// Construct a [`NodeMap`] from an [`XmlModel`], validating formulas.
156    ///
157    /// A declaration that cannot be turned into a runtime node is dropped and
158    /// recorded in [`NodeMap::skipped`] rather than failing the whole model.
159    /// Cameras carry thousands of nodes and only a handful of them matter to
160    /// any one application; refusing to open a camera because one obscure
161    /// feature is unrepresentable serves nobody.
162    pub fn try_from_xml(model: XmlModel) -> Result<Self, GenApiError> {
163        let mut nodes = HashMap::new();
164        let mut dependents: HashMap<String, Vec<String>> = HashMap::new();
165        // Losses from the XML layer travel with the ones from this layer. A
166        // consumer holding a NodeMap has no access to the XmlModel it was
167        // built from, so leaving them behind would make a feature the parser
168        // could not read indistinguishable from one the camera does not have.
169        let mut skipped = model.skipped;
170        for decl in model.nodes {
171            let tag = decl.kind().to_string();
172            let name = Some(decl.name().to_string());
173            // A node we cannot build costs that one feature, not the camera.
174            // The same isolation the XML layer already applies (issue #48) --
175            // a single unusual declaration must not make a camera unopenable.
176            let mut local: HashMap<String, Vec<String>> = HashMap::new();
177            match build_node(decl, &mut local) {
178                Ok((node_name, node)) => {
179                    for (provider, mut names) in local {
180                        dependents.entry(provider).or_default().append(&mut names);
181                    }
182                    nodes.insert(node_name, node);
183                }
184                Err(err) => {
185                    warn!(
186                        tag = %tag,
187                        node = name.as_deref().unwrap_or("<unnamed>"),
188                        error = %err,
189                        "dropping GenApi node"
190                    );
191                    skipped.push(SkippedNode {
192                        tag,
193                        name,
194                        error: err.to_string(),
195                    });
196                }
197            }
198        }
199
200        Ok(NodeMap {
201            version: model.version,
202            nodes,
203            dependents,
204            skipped,
205            generation: Cell::new(0),
206        })
207    }
208
209    /// Declarations this camera has that we do not expose.
210    ///
211    /// Covers both losses: a node the XML parser could not read, and one it
212    /// read but that could not be turned into a runtime node. Empty for a
213    /// document we fully understand; anything listed here is worth reporting
214    /// as a bug. `viva-camctl report` prints it.
215    pub fn skipped(&self) -> &[SkippedNode] {
216        &self.skipped
217    }
218
219    /// Read an integer feature value using the provided transport.
220    pub fn get_integer(&self, name: &str, io: &dyn RegisterIo) -> Result<i64, GenApiError> {
221        if let Some(Node::IntConverter(_)) = self.nodes.get(name) {
222            return self.get_int_converter(name, io);
223        }
224        if let Some(output) = self.nodes.get(name).and_then(|node| match node {
225            Node::SwissKnife(sk) => Some(sk.output),
226            _ => None,
227        }) {
228            return match output {
229                SkOutput::Integer => {
230                    let node = match self.nodes.get(name) {
231                        Some(Node::SwissKnife(node)) => node,
232                        _ => unreachable!("node vanished during lookup"),
233                    };
234                    let mut stack = HashSet::new();
235                    let value = self.evaluate_swissknife(node, io, &mut stack)?;
236                    sk_to_i64(name, value)
237                }
238                SkOutput::Float => Err(GenApiError::Type(name.to_string())),
239            };
240        }
241        let node = self.get_integer_node(name)?;
242        ensure_readable(&node.access, name)?;
243        self.ensure_selectors(name, &node.selected_if, io)?;
244        // Return static value if present.
245        if let Some(v) = node.value {
246            return Ok(v);
247        }
248        // Delegate to pValue node if present.
249        if let Some(ref pv) = node.pvalue {
250            let pv = pv.clone();
251            return self.get_integer(&pv, io);
252        }
253        let addressing = node
254            .addressing
255            .as_ref()
256            .ok_or_else(|| GenApiError::NodeNotFound(format!("{name}: no addressing or pValue")))?;
257        let (address, len) = self.resolve_address(name, addressing, io)?;
258        if let Some(value) = *node.cache.borrow() {
259            return Ok(value);
260        }
261        let raw = io.read(address, len as usize).map_err(|err| match err {
262            GenApiError::Io(_) => err,
263            other => other,
264        })?;
265        let value = if let Some(bitfield) = node.bitfield {
266            let extracted = extract(&raw, bitfield).map_err(|err| map_bitops_error(name, err))?;
267            interpret_bitfield_value(
268                name,
269                extracted,
270                bitfield.bit_length,
271                integer_sign(node).is_signed(),
272            )?
273        } else {
274            bytes_to_i64(name, &raw, integer_sign(node), node.byte_order)?
275        };
276        debug!(node = %name, raw = value, "read integer feature");
277        node.cache.replace(Some(value));
278        node.raw_cache.replace(Some(raw));
279        Ok(value)
280    }
281
282    /// Write an integer feature and update dependent caches.
283    pub fn set_integer(
284        &mut self,
285        name: &str,
286        value: i64,
287        io: &dyn RegisterIo,
288    ) -> Result<(), GenApiError> {
289        if let Some(Node::IntConverter(_)) = self.nodes.get(name) {
290            return self.set_int_converter(name, value, io);
291        }
292        let node = self.get_integer_node(name)?;
293        self.ensure_writable_now(name, &node.access, io)?;
294        self.ensure_selectors(name, &node.selected_if, io)?;
295        if let Some(ref pv) = node.pvalue {
296            let pv = pv.clone();
297            return self.set_integer(&pv, value, io);
298        }
299        let addressing = node
300            .addressing
301            .as_ref()
302            .ok_or_else(|| GenApiError::NodeNotFound(format!("{name}: no addressing or pValue")))?;
303        let (address, len) = self.resolve_address(name, addressing, io)?;
304        if value < node.min || value > node.max {
305            return Err(GenApiError::Range(name.to_string()));
306        }
307        if let Some(inc) = node.inc
308            && inc != 0
309            && (value - node.min) % inc != 0
310        {
311            return Err(GenApiError::Range(name.to_string()));
312        }
313        if let Some(bitfield) = node.bitfield {
314            let encoded = encode_bitfield_value(name, value, bitfield.bit_length, node.min < 0)?;
315            let mut raw = get_raw_or_read(&node.raw_cache, io, address, len)?;
316            insert(&mut raw, bitfield, encoded).map_err(|err| map_bitops_error(name, err))?;
317            debug!(node = %name, raw = value, "write integer feature");
318            io.write(address, &raw).map_err(|err| match err {
319                GenApiError::Io(_) => err,
320                other => other,
321            })?;
322            node.cache.replace(Some(value));
323            node.raw_cache.replace(Some(raw));
324        } else {
325            let bytes = i64_to_bytes(name, value, len, integer_sign(node), node.byte_order)?;
326            debug!(node = %name, raw = value, "write integer feature");
327            io.write(address, &bytes).map_err(|err| match err {
328                GenApiError::Io(_) => err,
329                other => other,
330            })?;
331            node.cache.replace(Some(value));
332            node.raw_cache.replace(Some(bytes));
333        }
334        self.invalidate_dependents(name);
335        Ok(())
336    }
337
338    /// Read a floating point feature.
339    pub fn get_float(&self, name: &str, io: &dyn RegisterIo) -> Result<f64, GenApiError> {
340        match self.nodes.get(name) {
341            Some(Node::Converter(_)) => return self.get_converter(name, io),
342            Some(Node::IntConverter(_)) => {
343                return self.get_int_converter(name, io).map(|v| v as f64);
344            }
345            _ => {}
346        }
347        if let Some(output) = self.nodes.get(name).and_then(|node| match node {
348            Node::SwissKnife(sk) => Some(sk.output),
349            _ => None,
350        }) {
351            return match output {
352                SkOutput::Float => {
353                    let node = match self.nodes.get(name) {
354                        Some(Node::SwissKnife(node)) => node,
355                        _ => unreachable!("node vanished during lookup"),
356                    };
357                    let mut stack = HashSet::new();
358                    let value = self.evaluate_swissknife(node, io, &mut stack)?;
359                    Ok(value.as_f64())
360                }
361                SkOutput::Integer => self.get_integer(name, io).map(|v| v as f64),
362            };
363        }
364        let node = self.get_float_node(name)?;
365        ensure_readable(&node.access, name)?;
366        self.ensure_selectors(name, &node.selected_if, io)?;
367        if let Some(ref pv) = node.pvalue {
368            let pv = pv.clone();
369            return self.get_float(&pv, io);
370        }
371        let addressing = node
372            .addressing
373            .as_ref()
374            .ok_or_else(|| GenApiError::NodeNotFound(format!("{name}: no addressing or pValue")))?;
375        let (address, len) = self.resolve_address(name, addressing, io)?;
376        if let Some(value) = *node.cache.borrow() {
377            return Ok(value);
378        }
379        let raw = io.read(address, len as usize).map_err(|err| match err {
380            GenApiError::Io(_) => err,
381            other => other,
382        })?;
383        let value = match node.encoding {
384            FloatEncoding::Ieee754 => {
385                let v = decode_ieee754(name, &raw, node.byte_order)?;
386                debug!(node = %name, value = v, "read float feature (ieee754)");
387                v
388            }
389            FloatEncoding::ScaledInteger => {
390                // `<Float>`/`<FloatReg>` declare no `<Sign>`; a scaled raw
391                // value is conventionally signed so an offset can go either way.
392                let raw_value = bytes_to_i64(name, &raw, Sign::Signed, node.byte_order)?;
393                let v = apply_scale(node, raw_value as f64);
394                debug!(node = %name, raw = raw_value, value = v, "read float feature (scaled)");
395                v
396            }
397        };
398        node.cache.replace(Some(value));
399        Ok(value)
400    }
401
402    /// Write a floating point feature using the scale/offset conversion.
403    pub fn set_float(
404        &mut self,
405        name: &str,
406        value: f64,
407        io: &dyn RegisterIo,
408    ) -> Result<(), GenApiError> {
409        match self.nodes.get(name) {
410            Some(Node::Converter(_)) => return self.set_converter(name, value, io),
411            Some(Node::IntConverter(_)) => {
412                return self.set_int_converter(name, round_to_i64(name, value)?, io);
413            }
414            _ => {}
415        }
416        let node = self.get_float_node(name)?;
417        self.ensure_writable_now(name, &node.access, io)?;
418        self.ensure_selectors(name, &node.selected_if, io)?;
419        if let Some(ref pv) = node.pvalue {
420            let pv = pv.clone();
421            return self.set_float(&pv, value, io);
422        }
423        let addressing = node
424            .addressing
425            .as_ref()
426            .ok_or_else(|| GenApiError::NodeNotFound(format!("{name}: no addressing or pValue")))?;
427        let (address, len) = self.resolve_address(name, addressing, io)?;
428        if value < node.min || value > node.max {
429            return Err(GenApiError::Range(name.to_string()));
430        }
431        let bytes = match node.encoding {
432            FloatEncoding::Ieee754 => {
433                let bytes = encode_ieee754(name, value, len, node.byte_order)?;
434                debug!(node = %name, value, "write float feature (ieee754)");
435                bytes
436            }
437            FloatEncoding::ScaledInteger => {
438                let raw = encode_float(node, value)?;
439                let bytes = i64_to_bytes(name, raw, len, Sign::Signed, node.byte_order)?;
440                debug!(node = %name, raw, value, "write float feature (scaled)");
441                bytes
442            }
443        };
444        io.write(address, &bytes).map_err(|err| match err {
445            GenApiError::Io(_) => err,
446            other => other,
447        })?;
448        node.cache.replace(Some(value));
449        self.invalidate_dependents(name);
450        Ok(())
451    }
452
453    /// Read an enumeration feature returning the symbolic entry name.
454    pub fn get_enum(&self, name: &str, io: &dyn RegisterIo) -> Result<String, GenApiError> {
455        let node = self.get_enum_node(name)?;
456        ensure_readable(&node.access, name)?;
457        self.ensure_selectors(name, &node.selected_if, io)?;
458        // When pValue is set, read the integer from the delegate node.
459        if let Some(ref pv) = node.pvalue {
460            let pv = pv.clone();
461            if let Some(value) = node.value_cache.borrow().clone() {
462                return Ok(value);
463            }
464            let raw_value = self.get_integer(&pv, io)?;
465            let entry = self.lookup_enum_entry(node, raw_value, io)?;
466            node.value_cache.replace(Some(entry.clone()));
467            return Ok(entry);
468        }
469        let addressing = node
470            .addressing
471            .as_ref()
472            .ok_or_else(|| GenApiError::NodeNotFound(format!("{name}: no addressing")))?;
473        let (address, len) = self.resolve_address(name, addressing, io)?;
474        if let Some(value) = node.value_cache.borrow().clone() {
475            return Ok(value);
476        }
477        let raw = io.read(address, len as usize).map_err(|err| match err {
478            GenApiError::Io(_) => err,
479            other => other,
480        })?;
481        // `<Enumeration>` declares no `<Sign>` and no `<Endianess>` — no
482        // document in the vendor corpus declares one — so the GenICam
483        // defaults apply: signed entry values, big-endian payload.
484        let raw_value = bytes_to_i64(name, &raw, Sign::Signed, ByteOrder::Big)?;
485        let entry = self.lookup_enum_entry(node, raw_value, io)?;
486        debug!(node = %name, raw = raw_value, entry = %entry, "read enum feature");
487        node.value_cache.replace(Some(entry.clone()));
488        Ok(entry)
489    }
490
491    /// Write an enumeration entry.
492    pub fn set_enum(
493        &mut self,
494        name: &str,
495        entry: &str,
496        io: &dyn RegisterIo,
497    ) -> Result<(), GenApiError> {
498        let node = self.get_enum_node(name)?;
499        self.ensure_writable_now(name, &node.access, io)?;
500        self.ensure_selectors(name, &node.selected_if, io)?;
501        if let Some(ref pv) = node.pvalue {
502            let pv = pv.clone();
503            let entry_decl = node
504                .entries
505                .iter()
506                .find(|candidate| candidate.name == entry)
507                .ok_or_else(|| GenApiError::EnumNoSuchEntry {
508                    node: name.to_string(),
509                    entry: entry.to_string(),
510                })?;
511            let raw_value = self.resolve_enum_entry_value(node, entry_decl, io)?;
512            let entry_str = entry.to_string();
513            // Re-borrow node after mutable self call.
514            self.set_integer(&pv, raw_value, io)?;
515            let node = self.get_enum_node(name)?;
516            node.value_cache.replace(Some(entry_str));
517            node.invalidate();
518            self.invalidate_dependents(name);
519            return Ok(());
520        }
521        let addressing = node
522            .addressing
523            .as_ref()
524            .ok_or_else(|| GenApiError::NodeNotFound(format!("{name}: no addressing")))?;
525        let (address, len) = self.resolve_address(name, addressing, io)?;
526        let entry_decl = node
527            .entries
528            .iter()
529            .find(|candidate| candidate.name == entry)
530            .ok_or_else(|| GenApiError::EnumNoSuchEntry {
531                node: name.to_string(),
532                entry: entry.to_string(),
533            })?;
534        let raw = self.resolve_enum_entry_value(node, entry_decl, io)?;
535        let bytes = i64_to_bytes(name, raw, len, Sign::Signed, ByteOrder::Big)?;
536        debug!(node = %name, raw, entry, "write enum feature");
537        io.write(address, &bytes).map_err(|err| match err {
538            GenApiError::Io(_) => err,
539            other => other,
540        })?;
541        node.value_cache.replace(None);
542        self.invalidate_dependents(name);
543        Ok(())
544    }
545
546    /// List the available entry names for an enumeration feature.
547    pub fn enum_entries(&self, name: &str) -> Result<Vec<String>, GenApiError> {
548        let node = self.get_enum_node(name)?;
549        if let Some(mapping) = node.mapping_cache.borrow().as_ref() {
550            let mut names: Vec<_> = mapping.by_name.keys().cloned().collect();
551            names.sort();
552            names.dedup();
553            return Ok(names);
554        }
555        let mut names: Vec<_> = node
556            .entries
557            .iter()
558            .map(|entry| entry.name.clone())
559            .collect();
560        names.sort();
561        names.dedup();
562        Ok(names)
563    }
564
565    /// Evaluate `pIsImplemented` for `name`, returning `true` when the feature
566    /// is implemented by the device.
567    ///
568    /// Absent `pIsImplemented` defaults to `true` (matching the GenICam spec:
569    /// an undeclared predicate means "always implemented"). Evaluation errors
570    /// propagate to the caller so bad XML is visible rather than silently
571    /// reported as implemented.
572    pub fn is_implemented(&self, name: &str, io: &dyn RegisterIo) -> Result<bool, GenApiError> {
573        let prefs = self.predicate_refs(name)?;
574        match &prefs.p_is_implemented {
575            None => Ok(true),
576            Some(provider) => self.eval_predicate_ref(name, provider, io),
577        }
578    }
579
580    /// Evaluate `pIsAvailable` plus selector gating for `name`.
581    ///
582    /// Returns `false` when the feature is not implemented, when
583    /// `pIsAvailable` evaluates to zero, or when any `selected_if` rule is
584    /// violated by the current selector value. Callers that want pure XML
585    /// gating without selector checks should use [`NodeMap::is_implemented`]
586    /// instead.
587    pub fn is_available(&self, name: &str, io: &dyn RegisterIo) -> Result<bool, GenApiError> {
588        if !self.is_implemented(name, io)? {
589            return Ok(false);
590        }
591        let prefs = self.predicate_refs(name)?;
592        if let Some(provider) = &prefs.p_is_available
593            && !self.eval_predicate_ref(name, provider, io)?
594        {
595            return Ok(false);
596        }
597        let selected_if = self
598            .nodes
599            .get(name)
600            .and_then(Self::selected_if_slice)
601            .unwrap_or(&[]);
602        self.selectors_allow(selected_if, io)
603    }
604
605    /// Refuse a write the device's current state does not permit.
606    ///
607    /// The static `<AccessMode>` is only half the picture, and for a great
608    /// many real nodes it is the less informative half: FLIR's `ExposureTime`
609    /// declares no `<AccessMode>` at all — so it defaults to `RW` — and puts
610    /// the entire restriction in `<pIsLocked>ExposureTime_Lck</pIsLocked>`, a
611    /// device register. Checking only the static mode meant we sent writes the
612    /// camera's own description said were not allowed, and the device answered
613    /// `ACCESS_DENIED` (issue #45).
614    ///
615    /// Deliberately *not* routed through [`NodeMap::effective_access_mode`]:
616    /// that function collapses "unavailable" into `RO` because it serves a UI
617    /// that has a separate availability flag. Here the distinction is the
618    /// whole value of the error, so the two conditions are checked separately.
619    ///
620    /// Reads keep the static [`AccessMode::WO`] check only. Evaluating
621    /// predicates on every `get` would add device round-trips to the hottest
622    /// path in the library for a check the subsequent read reports anyway; a
623    /// refused write, by contrast, is worth one predicate evaluation to turn a
624    /// wire error into a named local one. See backlog GA-06.
625    fn ensure_writable_now(
626        &self,
627        name: &str,
628        access: &AccessMode,
629        io: &dyn RegisterIo,
630    ) -> Result<(), GenApiError> {
631        ensure_writable(access, name)?;
632        if !self.is_available(name, io)? {
633            return Err(GenApiError::Unavailable(name.to_string()));
634        }
635        let prefs = self.predicate_refs(name)?;
636        if let Some(provider) = &prefs.p_is_locked
637            && self.eval_predicate_ref(name, provider, io)?
638        {
639            return Err(GenApiError::Locked {
640                name: name.to_string(),
641                locked_by: provider.to_string(),
642            });
643        }
644        Ok(())
645    }
646
647    /// Return the effective [`AccessMode`] for `name` given the current
648    /// device state.
649    ///
650    /// - If the feature is unavailable (see [`NodeMap::is_available`]), the
651    ///   function returns `AccessMode::RO` — we cannot report "NA" without
652    ///   introducing a new variant, and Studio's wire protocol carries the
653    ///   availability flag separately.
654    /// - If `pIsLocked` evaluates truthy, `RW` downgrades to `RO`; `RO` and
655    ///   `WO` are unaffected.
656    /// - Otherwise the statically declared access mode applies.
657    pub fn effective_access_mode(
658        &self,
659        name: &str,
660        io: &dyn RegisterIo,
661    ) -> Result<AccessMode, GenApiError> {
662        let node = self
663            .nodes
664            .get(name)
665            .ok_or_else(|| GenApiError::NodeNotFound(name.to_string()))?;
666        let base = node.access_mode().unwrap_or(AccessMode::RO);
667        if !self.is_available(name, io)? {
668            return Ok(AccessMode::RO);
669        }
670        let prefs = self.predicate_refs(name)?;
671        if let Some(provider) = &prefs.p_is_locked
672            && self.eval_predicate_ref(name, provider, io)?
673        {
674            return Ok(match base {
675                AccessMode::RW => AccessMode::RO,
676                other => other,
677            });
678        }
679        Ok(base)
680    }
681
682    /// Return the subset of enum entries currently reported as available by
683    /// the device, or the full static list when no entry declares an
684    /// `pIsImplemented`/`pIsAvailable`.
685    ///
686    /// Falling back to the full list preserves current behaviour for XMLs
687    /// that don't gate individual entries, so callers stop seeing the stale
688    /// static list when the new predicates are added and otherwise behave as
689    /// before.
690    pub fn available_enum_entries(
691        &self,
692        name: &str,
693        io: &dyn RegisterIo,
694    ) -> Result<Vec<String>, GenApiError> {
695        let node = self.get_enum_node(name)?;
696        let any_entry_predicate = node.entries.iter().any(|e| !e.predicates.is_empty());
697        if !any_entry_predicate {
698            return self.enum_entries(name);
699        }
700        let mut out = Vec::new();
701        for entry in &node.entries {
702            if let Some(provider) = &entry.predicates.p_is_implemented
703                && !self.eval_predicate_ref(name, provider, io)?
704            {
705                continue;
706            }
707            if let Some(provider) = &entry.predicates.p_is_available
708                && !self.eval_predicate_ref(name, provider, io)?
709            {
710                continue;
711            }
712            out.push(entry.name.clone());
713        }
714        out.sort();
715        out.dedup();
716        Ok(out)
717    }
718
719    fn predicate_refs(&self, name: &str) -> Result<&PredicateRefs, GenApiError> {
720        self.nodes
721            .get(name)
722            .map(Node::predicates)
723            .ok_or_else(|| GenApiError::NodeNotFound(name.to_string()))
724    }
725
726    /// Evaluate a `pIs*` reference by reading the target node as an integer
727    /// truthy value.
728    ///
729    /// `ctx` is the node that owns the predicate; it is used for diagnostics
730    /// and cycle detection so a predicate that accidentally resolves back to
731    /// its own owner fails fast rather than recursing. Providers can be any
732    /// numeric-resolvable node: Integer, Boolean, Enum (integer form),
733    /// SwissKnife, or a Converter.
734    fn eval_predicate_ref(
735        &self,
736        ctx: &str,
737        provider: &str,
738        io: &dyn RegisterIo,
739    ) -> Result<bool, GenApiError> {
740        if provider == ctx {
741            return Err(GenApiError::ExprEval {
742                name: ctx.to_string(),
743                msg: "predicate references the node it gates".into(),
744            });
745        }
746        let mut stack = HashSet::new();
747        stack.insert(ctx.to_string());
748        let value = self.resolve_numeric(provider, io, &mut stack)?;
749        trace!(node = %ctx, provider, value, "predicate eval");
750        Ok(value != 0.0)
751    }
752
753    /// Non-erroring cousin of [`NodeMap::ensure_selectors`] — returns `Ok(false)`
754    /// when a selector gating rule rejects the current state, rather than
755    /// converting that into a [`GenApiError::Unavailable`].
756    fn selectors_allow(
757        &self,
758        rules: &[(String, Vec<String>)],
759        io: &dyn RegisterIo,
760    ) -> Result<bool, GenApiError> {
761        for (selector, allowed) in rules {
762            if allowed.is_empty() {
763                continue;
764            }
765            let current = self.get_selector_value(selector, io)?;
766            if !allowed.iter().any(|v| v == &current) {
767                return Ok(false);
768            }
769        }
770        Ok(true)
771    }
772
773    fn selected_if_slice(node: &Node) -> Option<&[(String, Vec<String>)]> {
774        match node {
775            Node::Integer(n) => Some(&n.selected_if),
776            Node::Float(n) => Some(&n.selected_if),
777            Node::Enum(n) => Some(&n.selected_if),
778            Node::Boolean(n) => Some(&n.selected_if),
779            _ => None,
780        }
781    }
782
783    /// Read a boolean feature.
784    pub fn get_bool(&self, name: &str, io: &dyn RegisterIo) -> Result<bool, GenApiError> {
785        let node = self.get_bool_node(name)?;
786        ensure_readable(&node.access, name)?;
787        self.ensure_selectors(name, &node.selected_if, io)?;
788        if let Some(ref pv) = node.pvalue {
789            let pv = pv.clone();
790            let raw = self.get_integer(&pv, io)?;
791            let on = node.on_value.unwrap_or(1);
792            return Ok(raw == on);
793        }
794        let addressing = node
795            .addressing
796            .as_ref()
797            .ok_or_else(|| GenApiError::NodeNotFound(format!("{name}: no addressing or pValue")))?;
798        let bitfield = node
799            .bitfield
800            .ok_or_else(|| GenApiError::Parse(format!("{name}: boolean without bitfield")))?;
801        let (address, len) = self.resolve_address(name, addressing, io)?;
802        if let Some(value) = *node.cache.borrow() {
803            return Ok(value);
804        }
805        let raw = io.read(address, len as usize).map_err(|err| match err {
806            GenApiError::Io(_) => err,
807            other => other,
808        })?;
809        let raw_value = extract(&raw, bitfield).map_err(|err| map_bitops_error(name, err))?;
810        let value = raw_value != 0;
811        debug!(node = %name, raw = raw_value, value, "read boolean feature");
812        node.cache.replace(Some(value));
813        node.raw_cache.replace(Some(raw));
814        Ok(value)
815    }
816
817    /// Write a boolean feature.
818    pub fn set_bool(
819        &mut self,
820        name: &str,
821        value: bool,
822        io: &dyn RegisterIo,
823    ) -> Result<(), GenApiError> {
824        let node = self.get_bool_node(name)?;
825        self.ensure_writable_now(name, &node.access, io)?;
826        self.ensure_selectors(name, &node.selected_if, io)?;
827        if let Some(ref pv) = node.pvalue {
828            let pv = pv.clone();
829            let on = node.on_value.unwrap_or(1);
830            let off = node.off_value.unwrap_or(0);
831            let raw = if value { on } else { off };
832            return self.set_integer(&pv, raw, io);
833        }
834        let addressing = node
835            .addressing
836            .as_ref()
837            .ok_or_else(|| GenApiError::NodeNotFound(format!("{name}: no addressing or pValue")))?;
838        let bitfield = node
839            .bitfield
840            .ok_or_else(|| GenApiError::Parse(format!("{name}: boolean without bitfield")))?;
841        let (address, len) = self.resolve_address(name, addressing, io)?;
842        let encoded = if value { 1 } else { 0 };
843        let mut raw = get_raw_or_read(&node.raw_cache, io, address, len)?;
844        insert(&mut raw, bitfield, encoded).map_err(|err| map_bitops_error(name, err))?;
845        debug!(node = %name, raw = encoded, value, "write boolean feature");
846        io.write(address, &raw).map_err(|err| match err {
847            GenApiError::Io(_) => err,
848            other => other,
849        })?;
850        node.cache.replace(Some(value));
851        node.raw_cache.replace(Some(raw));
852        self.invalidate_dependents(name);
853        Ok(())
854    }
855
856    /// Execute a command feature by writing a value to the command register.
857    pub fn exec_command(&mut self, name: &str, io: &dyn RegisterIo) -> Result<(), GenApiError> {
858        let node = self.get_command_node(name)?;
859        // Determine the value to write and the target.
860        let cmd_value = node.command_value.unwrap_or(1);
861
862        if let Some(ref pv) = node.pvalue {
863            // Delegate to the pValue node.
864            let pv = pv.clone();
865            debug!(node = %name, "execute command via pValue");
866            return self.set_integer(&pv, cmd_value, io);
867        }
868
869        let address = node
870            .address
871            .ok_or_else(|| GenApiError::NodeNotFound(format!("{name}: no address or pValue")))?;
872        if node.len == 0 {
873            return Err(GenApiError::Parse(format!(
874                "command node {name} has zero length"
875            )));
876        }
877        let data = i64_to_bytes(name, cmd_value, node.len, Sign::Signed, ByteOrder::Big)?;
878        debug!(node = %name, "execute command");
879        io.write(address, &data).map_err(|err| match err {
880            GenApiError::Io(_) => err,
881            other => other,
882        })?;
883        self.invalidate_dependents(name);
884        Ok(())
885    }
886
887    fn get_integer_node(&self, name: &str) -> Result<&IntegerNode, GenApiError> {
888        match self.nodes.get(name) {
889            Some(Node::Integer(node)) => Ok(node),
890            Some(_) => Err(GenApiError::Type(name.to_string())),
891            None => Err(GenApiError::NodeNotFound(name.to_string())),
892        }
893    }
894
895    fn get_float_node(&self, name: &str) -> Result<&FloatNode, GenApiError> {
896        match self.nodes.get(name) {
897            Some(Node::Float(node)) => Ok(node),
898            Some(_) => Err(GenApiError::Type(name.to_string())),
899            None => Err(GenApiError::NodeNotFound(name.to_string())),
900        }
901    }
902
903    fn get_enum_node(&self, name: &str) -> Result<&EnumNode, GenApiError> {
904        match self.nodes.get(name) {
905            Some(Node::Enum(node)) => Ok(node),
906            Some(_) => Err(GenApiError::Type(name.to_string())),
907            None => Err(GenApiError::NodeNotFound(name.to_string())),
908        }
909    }
910
911    fn get_bool_node(&self, name: &str) -> Result<&BooleanNode, GenApiError> {
912        match self.nodes.get(name) {
913            Some(Node::Boolean(node)) => Ok(node),
914            Some(_) => Err(GenApiError::Type(name.to_string())),
915            None => Err(GenApiError::NodeNotFound(name.to_string())),
916        }
917    }
918
919    fn get_command_node(&self, name: &str) -> Result<&CommandNode, GenApiError> {
920        match self.nodes.get(name) {
921            Some(Node::Command(node)) => Ok(node),
922            Some(_) => Err(GenApiError::Type(name.to_string())),
923            None => Err(GenApiError::NodeNotFound(name.to_string())),
924        }
925    }
926
927    fn ensure_selectors(
928        &self,
929        node_name: &str,
930        rules: &[(String, Vec<String>)],
931        io: &dyn RegisterIo,
932    ) -> Result<(), GenApiError> {
933        for (selector, allowed) in rules {
934            if allowed.is_empty() {
935                continue;
936            }
937            let current = self.get_selector_value(selector, io)?;
938            if !allowed.iter().any(|value| value == &current) {
939                return Err(GenApiError::Unavailable(format!(
940                    "node '{node_name}' unavailable for selector '{selector}={current}'"
941                )));
942            }
943        }
944        Ok(())
945    }
946
947    fn lookup_enum_entry(
948        &self,
949        node: &EnumNode,
950        raw_value: i64,
951        io: &dyn RegisterIo,
952    ) -> Result<String, GenApiError> {
953        {
954            let mut cache = node.mapping_cache.borrow_mut();
955            if cache.is_none() {
956                *cache = Some(self.build_enum_mapping(node, io)?);
957            }
958            if let Some(mapping) = cache.as_ref()
959                && let Some(entry) = mapping.by_value.get(&raw_value)
960            {
961                return Ok(entry.clone());
962            }
963            *cache = Some(self.build_enum_mapping(node, io)?);
964            if let Some(mapping) = cache.as_ref()
965                && let Some(entry) = mapping.by_value.get(&raw_value)
966            {
967                return Ok(entry.clone());
968            }
969        }
970        Err(GenApiError::EnumValueUnknown {
971            node: node.name.clone(),
972            value: raw_value,
973        })
974    }
975
976    fn build_enum_mapping(
977        &self,
978        node: &EnumNode,
979        io: &dyn RegisterIo,
980    ) -> Result<EnumMapping, GenApiError> {
981        let mut by_value = HashMap::new();
982        let mut by_name = HashMap::new();
983
984        for entry in &node.entries {
985            let value = self.resolve_enum_entry_value(node, entry, io)?;
986            match by_value.entry(value) {
987                HashMapEntry::Vacant(slot) => {
988                    slot.insert(entry.name.clone());
989                }
990                HashMapEntry::Occupied(existing) => {
991                    warn!(
992                        enum_node = %node.name,
993                        value,
994                        kept = %existing.get(),
995                        dropped = %entry.name,
996                        "duplicate enum value"
997                    );
998                }
999            }
1000            by_name.insert(entry.name.clone(), value);
1001        }
1002
1003        let mut summary: Vec<_> = by_value
1004            .iter()
1005            .map(|(value, name)| (*value, name.clone()))
1006            .collect();
1007        summary.sort_by_key(|(value, _)| *value);
1008        debug!(node = %node.name, entries = ?summary, "build enum mapping");
1009
1010        Ok(EnumMapping { by_value, by_name })
1011    }
1012
1013    fn resolve_enum_entry_value(
1014        &self,
1015        node: &EnumNode,
1016        entry: &EnumEntryDecl,
1017        io: &dyn RegisterIo,
1018    ) -> Result<i64, GenApiError> {
1019        match &entry.value {
1020            EnumValueSrc::Literal(value) => Ok(*value),
1021            EnumValueSrc::FromNode(provider) => {
1022                let value = self.get_integer(provider, io)?;
1023                trace!(
1024                    enum_node = %node.name,
1025                    entry = %entry.name,
1026                    provider = %provider,
1027                    value,
1028                    "resolved enum entry from provider"
1029                );
1030                Ok(value)
1031            }
1032        }
1033    }
1034
1035    /// Resolve the device register address and length backing a feature.
1036    ///
1037    /// This is the addressing half of typed feature access, exposed for
1038    /// callers that need raw register I/O through [`RegisterIo`] — a
1039    /// file-transfer buffer, for instance, whose address the XML supplies
1040    /// through `<pAddress>` and which no typed accessor covers. Address
1041    /// terms, `<pIndex>` scaling and selector blocks resolve exactly as they
1042    /// do for `get_integer` and friends, so a caller does not have to
1043    /// reimplement GenICam addressing.
1044    ///
1045    /// Returns [`GenApiError::Unavailable`] for a node that has no addressing
1046    /// of its own because it delegates through `<pValue>`, and for a
1047    /// selector-mapped node whose current selector value has no block.
1048    pub fn register_address(
1049        &self,
1050        name: &str,
1051        io: &dyn RegisterIo,
1052    ) -> Result<(u64, u32), GenApiError> {
1053        let node = self
1054            .node(name)
1055            .ok_or_else(|| GenApiError::NodeNotFound(name.to_string()))?;
1056        let addressing = match node {
1057            Node::Integer(node) => node.addressing.as_ref(),
1058            Node::Float(node) => node.addressing.as_ref(),
1059            Node::Enum(node) => node.addressing.as_ref(),
1060            Node::Boolean(node) => node.addressing.as_ref(),
1061            Node::String(node) => Some(&node.addressing),
1062            Node::Register(node) => Some(&node.addressing),
1063            _ => None,
1064        }
1065        .ok_or_else(|| {
1066            GenApiError::Unavailable(format!("node '{name}' has no register addressing"))
1067        })?;
1068        self.resolve_address(name, addressing, io)
1069    }
1070
1071    fn resolve_address(
1072        &self,
1073        node_name: &str,
1074        addressing: &Addressing,
1075        io: &dyn RegisterIo,
1076    ) -> Result<(u64, u32), GenApiError> {
1077        match addressing {
1078            Addressing::Sum { terms, len } => {
1079                let mut address: u64 = 0;
1080                for term in terms {
1081                    address =
1082                        address.wrapping_add(self.resolve_address_term(node_name, term, *len, io)?);
1083                }
1084                if terms.len() > 1 {
1085                    debug!(
1086                        node = %node_name,
1087                        terms = terms.len(),
1088                        address = format_args!("0x{address:X}"),
1089                        len = *len,
1090                        "resolve summed address"
1091                    );
1092                }
1093                Ok((address, *len))
1094            }
1095            Addressing::BySelector { selector, map } => {
1096                let value = self.get_selector_value(selector, io)?;
1097                if let Some((_, (address, len))) = map.iter().find(|(name, _)| name == &value) {
1098                    let addr = *address;
1099                    let len = *len;
1100                    debug!(
1101                        node = %node_name,
1102                        selector = %selector,
1103                        value = %value,
1104                        address = format_args!("0x{addr:X}"),
1105                        len,
1106                        "resolve address via selector"
1107                    );
1108                    Ok((addr, len))
1109                } else {
1110                    Err(GenApiError::Unavailable(format!(
1111                        "node '{node_name}' unavailable for selector '{selector}={value}'"
1112                    )))
1113                }
1114            }
1115        }
1116    }
1117
1118    /// Resolve one address term to the offset it contributes.
1119    fn resolve_address_term(
1120        &self,
1121        node_name: &str,
1122        term: &AddressTerm,
1123        len: u32,
1124        io: &dyn RegisterIo,
1125    ) -> Result<u64, GenApiError> {
1126        let bad = |addr: i64| GenApiError::BadIndirectAddress {
1127            name: node_name.to_string(),
1128            addr,
1129        };
1130        match term {
1131            AddressTerm::Fixed(offset) => Ok(*offset),
1132            AddressTerm::Node(provider) => {
1133                let value = self.get_integer(provider, io)?;
1134                u64::try_from(value).map_err(|_| bad(value))
1135            }
1136            AddressTerm::Index { node, offset } => {
1137                let index = self.get_integer(node, io)?;
1138                let index = u64::try_from(index).map_err(|_| bad(index))?;
1139                let stride = match offset {
1140                    IndexOffset::Fixed(stride) => *stride,
1141                    IndexOffset::Node(provider) => {
1142                        let value = self.get_integer(provider, io)?;
1143                        u64::try_from(value).map_err(|_| bad(value))?
1144                    }
1145                    // A bare `<pIndex>` strides by the register length.
1146                    IndexOffset::Length => u64::from(len),
1147                };
1148                Ok(index.wrapping_mul(stride))
1149            }
1150        }
1151    }
1152
1153    fn get_selector_value(
1154        &self,
1155        selector: &str,
1156        io: &dyn RegisterIo,
1157    ) -> Result<String, GenApiError> {
1158        match self.nodes.get(selector) {
1159            Some(Node::Enum(_)) => self.get_enum(selector, io),
1160            Some(Node::Boolean(_)) => Ok(self.get_bool(selector, io)?.to_string()),
1161            Some(Node::Integer(_)) => Ok(self.get_integer(selector, io)?.to_string()),
1162            Some(_) => Err(GenApiError::Parse(format!(
1163                "selector {selector} has unsupported type"
1164            ))),
1165            None => Err(GenApiError::NodeNotFound(selector.to_string())),
1166        }
1167    }
1168
1169    /// Bind a formula's declared variables and evaluate it.
1170    ///
1171    /// Shared by SwissKnife, Converter and IntConverter: they differ only in
1172    /// which AST and variable list they hand over, in the arithmetic mode, and
1173    /// in any variables the caller binds directly (`FROM` and `OLD` on a
1174    /// write, which have no provider node to read).
1175    #[allow(clippy::too_many_arguments)]
1176    fn eval_formula(
1177        &self,
1178        name: &str,
1179        ast: &SkAst,
1180        vars: &[(String, String)],
1181        overrides: &[(&str, SkValue)],
1182        mode: EvalMode,
1183        io: &dyn RegisterIo,
1184        stack: &mut HashSet<String>,
1185    ) -> Result<SkValue, GenApiError> {
1186        let mut values: HashMap<String, SkValue> = HashMap::new();
1187        for (var, provider) in vars {
1188            if overrides.iter().any(|(ident, _)| ident == var) {
1189                continue;
1190            }
1191            values.insert(var.clone(), self.resolve_value(provider, io, stack)?);
1192        }
1193        for (ident, value) in overrides {
1194            values.insert((*ident).to_string(), *value);
1195        }
1196        let mut resolver = |ident: &str| -> Result<SkValue, SkEvalError> {
1197            values
1198                .get(ident)
1199                .copied()
1200                .ok_or_else(|| SkEvalError::UnknownVariable(ident.to_string()))
1201        };
1202        eval_ast(ast, &mut resolver, mode).map_err(|err| expr_error(name, err))
1203    }
1204
1205    fn evaluate_swissknife(
1206        &self,
1207        node: &SkNode,
1208        io: &dyn RegisterIo,
1209        stack: &mut HashSet<String>,
1210    ) -> Result<SkValue, GenApiError> {
1211        if let Some((value, generation)) = *node.cache.borrow()
1212            && generation == self.generation.get()
1213        {
1214            return Ok(value);
1215        }
1216        if !stack.insert(node.name.clone()) {
1217            stack.remove(&node.name);
1218            return Err(GenApiError::ExprEval {
1219                name: node.name.clone(),
1220                msg: "cyclic dependency".into(),
1221            });
1222        }
1223        let current_gen = self.generation.get();
1224        let result = self.eval_formula(
1225            &node.name,
1226            &node.ast,
1227            &node.vars,
1228            &[],
1229            eval_mode(node.output),
1230            io,
1231            stack,
1232        );
1233        stack.remove(&node.name);
1234        let value = result?;
1235        debug!(node = %node.name, value = %value, "evaluate SwissKnife");
1236        node.cache.replace(Some((value, current_gen)));
1237        Ok(value)
1238    }
1239
1240    /// Resolve a node reference to the value a formula should see.
1241    ///
1242    /// Integer-typed providers stay integral: routing a 64-bit register value
1243    /// through `f64` on the way into a formula would round away its low bits.
1244    fn resolve_value(
1245        &self,
1246        provider: &str,
1247        io: &dyn RegisterIo,
1248        stack: &mut HashSet<String>,
1249    ) -> Result<SkValue, GenApiError> {
1250        match self.nodes.get(provider) {
1251            Some(Node::Integer(_)) => self.get_integer(provider, io).map(SkValue::Int),
1252            Some(Node::Float(_)) => self.get_float(provider, io).map(SkValue::Float),
1253            Some(Node::Boolean(_)) => self
1254                .get_bool(provider, io)
1255                .map(|flag| SkValue::Int(i64::from(flag))),
1256            Some(Node::Enum(_)) => self.get_enum_numeric(provider, io).map(SkValue::Int),
1257            Some(Node::SwissKnife(node)) => self.evaluate_swissknife(node, io, stack),
1258            Some(Node::Converter(node)) => self.evaluate_converter(node, io, stack),
1259            Some(Node::IntConverter(node)) => self
1260                .evaluate_int_converter(node, io, stack)
1261                .map(SkValue::Int),
1262            Some(_) => Err(GenApiError::Type(provider.to_string())),
1263            None => Err(GenApiError::NodeNotFound(provider.to_string())),
1264        }
1265    }
1266
1267    fn resolve_numeric(
1268        &self,
1269        provider: &str,
1270        io: &dyn RegisterIo,
1271        stack: &mut HashSet<String>,
1272    ) -> Result<f64, GenApiError> {
1273        self.resolve_value(provider, io, stack)
1274            .map(|value| value.as_f64())
1275    }
1276
1277    fn get_enum_numeric(&self, name: &str, io: &dyn RegisterIo) -> Result<i64, GenApiError> {
1278        let entry = self.get_enum(name, io)?;
1279        let node = self.get_enum_node(name)?;
1280        {
1281            let mut mapping = node.mapping_cache.borrow_mut();
1282            if mapping.is_none() {
1283                *mapping = Some(self.build_enum_mapping(node, io)?);
1284            }
1285            if let Some(map) = mapping.as_ref()
1286                && let Some(value) = map.by_name.get(&entry)
1287            {
1288                return Ok(*value);
1289            }
1290        }
1291        Err(GenApiError::EnumNoSuchEntry {
1292            node: name.to_string(),
1293            entry,
1294        })
1295    }
1296
1297    fn invalidate_dependents(&self, name: &str) {
1298        self.bump_generation();
1299        if let Some(children) = self.dependents.get(name) {
1300            let mut visited = HashSet::new();
1301            for child in children {
1302                self.invalidate_recursive(child, &mut visited);
1303            }
1304        }
1305    }
1306
1307    fn invalidate_recursive(&self, name: &str, visited: &mut HashSet<String>) {
1308        if !visited.insert(name.to_string()) {
1309            return;
1310        }
1311        if let Some(node) = self.nodes.get(name) {
1312            node.invalidate_cache();
1313        }
1314        if let Some(children) = self.dependents.get(name) {
1315            for child in children {
1316                self.invalidate_recursive(child, visited);
1317            }
1318        }
1319    }
1320
1321    fn bump_generation(&self) {
1322        let current = self.generation.get();
1323        self.generation.set(current.wrapping_add(1));
1324    }
1325
1326    // ========================================================================
1327    // Converter/IntConverter/String support
1328    // ========================================================================
1329
1330    fn get_converter_node(&self, name: &str) -> Result<&ConverterNode, GenApiError> {
1331        match self.nodes.get(name) {
1332            Some(Node::Converter(node)) => Ok(node),
1333            Some(_) => Err(GenApiError::Type(name.to_string())),
1334            None => Err(GenApiError::NodeNotFound(name.to_string())),
1335        }
1336    }
1337
1338    fn get_int_converter_node(&self, name: &str) -> Result<&IntConverterNode, GenApiError> {
1339        match self.nodes.get(name) {
1340            Some(Node::IntConverter(node)) => Ok(node),
1341            Some(_) => Err(GenApiError::Type(name.to_string())),
1342            None => Err(GenApiError::NodeNotFound(name.to_string())),
1343        }
1344    }
1345
1346    fn get_string_node(&self, name: &str) -> Result<&StringNode, GenApiError> {
1347        match self.nodes.get(name) {
1348            Some(Node::String(node)) => Ok(node),
1349            Some(_) => Err(GenApiError::Type(name.to_string())),
1350            None => Err(GenApiError::NodeNotFound(name.to_string())),
1351        }
1352    }
1353
1354    fn get_register_node(&self, name: &str) -> Result<&RegisterNode, GenApiError> {
1355        match self.nodes.get(name) {
1356            Some(Node::Register(node)) => Ok(node),
1357            Some(_) => Err(GenApiError::Type(name.to_string())),
1358            None => Err(GenApiError::NodeNotFound(name.to_string())),
1359        }
1360    }
1361
1362    /// Read a Converter feature value (float) using the provided transport.
1363    pub fn get_converter(&self, name: &str, io: &dyn RegisterIo) -> Result<f64, GenApiError> {
1364        let node = self.get_converter_node(name)?;
1365        if let Some((value, generation)) = *node.cache.borrow()
1366            && generation == self.generation.get()
1367        {
1368            return Ok(value.as_f64());
1369        }
1370        let mut stack = HashSet::new();
1371        let value = self.evaluate_converter(node, io, &mut stack)?;
1372        node.cache.replace(Some((value, self.generation.get())));
1373        Ok(value.as_f64())
1374    }
1375
1376    /// Read an IntConverter feature value (integer) using the provided transport.
1377    pub fn get_int_converter(&self, name: &str, io: &dyn RegisterIo) -> Result<i64, GenApiError> {
1378        let node = self.get_int_converter_node(name)?;
1379        if let Some((value, generation)) = *node.cache.borrow()
1380            && generation == self.generation.get()
1381        {
1382            return Ok(value);
1383        }
1384        let mut stack = HashSet::new();
1385        let value = self.evaluate_int_converter(node, io, &mut stack)?;
1386        node.cache.replace(Some((value, self.generation.get())));
1387        Ok(value)
1388    }
1389
1390    /// Write a Converter feature value (float) through its `<FormulaTo>`.
1391    pub fn set_converter(
1392        &mut self,
1393        name: &str,
1394        value: f64,
1395        io: &dyn RegisterIo,
1396    ) -> Result<(), GenApiError> {
1397        let node = self.get_converter_node(name)?;
1398        let (p_value, raw) = self.converter_raw_write(
1399            &node.name,
1400            &node.ast_to,
1401            &node.vars_to,
1402            &node.p_value,
1403            SkValue::Float(value),
1404            eval_mode(node.output),
1405            io,
1406        )?;
1407        self.write_converter_raw(&p_value, raw, io)?;
1408        self.invalidate_dependents(name);
1409        Ok(())
1410    }
1411
1412    /// Write an IntConverter feature value through its `<FormulaTo>`.
1413    pub fn set_int_converter(
1414        &mut self,
1415        name: &str,
1416        value: i64,
1417        io: &dyn RegisterIo,
1418    ) -> Result<(), GenApiError> {
1419        let node = self.get_int_converter_node(name)?;
1420        let (p_value, raw) = self.converter_raw_write(
1421            &node.name,
1422            &node.ast_to,
1423            &node.vars_to,
1424            &node.p_value,
1425            SkValue::Int(value),
1426            EvalMode::Integer,
1427            io,
1428        )?;
1429        self.write_converter_raw(&p_value, raw, io)?;
1430        self.invalidate_dependents(name);
1431        Ok(())
1432    }
1433
1434    /// Evaluate a converter's `<FormulaTo>` to the raw value to write.
1435    ///
1436    /// `FROM` is the value the caller is setting, and `OLD` — where the
1437    /// formula declares it — is the register's current contents, which
1438    /// read-modify-write formulas such as `(FROM & 0x7FFFFFFF) | (OLD &
1439    /// 0x80000000)` depend on.
1440    #[allow(clippy::too_many_arguments)]
1441    fn converter_raw_write(
1442        &self,
1443        name: &str,
1444        ast_to: &SkAst,
1445        vars_to: &[(String, String)],
1446        p_value: &str,
1447        value: SkValue,
1448        mode: EvalMode,
1449        io: &dyn RegisterIo,
1450    ) -> Result<(String, SkValue), GenApiError> {
1451        let mut stack = HashSet::new();
1452        let mut overrides = vec![("FROM", value)];
1453        if vars_to.iter().any(|(var, _)| var == "OLD") {
1454            let old = self.resolve_value(p_value, io, &mut stack)?;
1455            overrides.push(("OLD", old));
1456        }
1457        let raw = self.eval_formula(name, ast_to, vars_to, &overrides, mode, io, &mut stack)?;
1458        Ok((p_value.to_string(), raw))
1459    }
1460
1461    fn write_converter_raw(
1462        &mut self,
1463        p_value: &str,
1464        raw: SkValue,
1465        io: &dyn RegisterIo,
1466    ) -> Result<(), GenApiError> {
1467        match self.nodes.get(p_value) {
1468            Some(Node::Float(_)) => self.set_float(p_value, raw.as_f64(), io),
1469            Some(Node::Boolean(_)) => self.set_bool(p_value, raw.is_truthy(), io),
1470            Some(_) => self.set_integer(p_value, sk_to_i64(p_value, raw)?, io),
1471            None => Err(GenApiError::NodeNotFound(p_value.to_string())),
1472        }
1473    }
1474
1475    /// Read a String feature value using the provided transport.
1476    pub fn get_string(&self, name: &str, io: &dyn RegisterIo) -> Result<String, GenApiError> {
1477        let node = self.get_string_node(name)?;
1478        ensure_readable(&node.access, name)?;
1479        if let Some((ref value, generation)) = *node.cache.borrow()
1480            && generation == self.generation.get()
1481        {
1482            return Ok(value.clone());
1483        }
1484        let (address, len) = self.resolve_address(name, &node.addressing, io)?;
1485        let raw = io.read(address, len as usize)?;
1486        // Convert bytes to string, stopping at first null byte
1487        let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
1488        let value = String::from_utf8_lossy(&raw[..end]).to_string();
1489        node.cache
1490            .replace(Some((value.clone(), self.generation.get())));
1491        debug!(node = %name, value = %value, "get_string");
1492        Ok(value)
1493    }
1494
1495    /// Write a String feature value using the provided transport.
1496    pub fn set_string(
1497        &self,
1498        name: &str,
1499        value: &str,
1500        io: &dyn RegisterIo,
1501    ) -> Result<(), GenApiError> {
1502        let node = self.get_string_node(name)?;
1503        self.ensure_writable_now(name, &node.access, io)?;
1504        let (address, len) = self.resolve_address(name, &node.addressing, io)?;
1505        // Build byte buffer with null termination
1506        let mut buf = vec![0u8; len as usize];
1507        let bytes = value.as_bytes();
1508        let copy_len = bytes.len().min(len as usize);
1509        buf[..copy_len].copy_from_slice(&bytes[..copy_len]);
1510        io.write(address, &buf)?;
1511        node.cache
1512            .replace(Some((value.to_string(), self.generation.get())));
1513        self.invalidate_dependents(name);
1514        debug!(node = %name, value = %value, "set_string");
1515        Ok(())
1516    }
1517
1518    /// Read a `<Register>` node's bytes using the provided transport.
1519    ///
1520    /// Returns the full declared length. For a large block — the Micro-Epsilon
1521    /// scanCONTROL declares `FileAccessBuffer` as 100 000 bytes — that is
1522    /// hundreds of chunked reads; use [`NodeMap::register_address`] and the
1523    /// transport directly when a partial read is what you want.
1524    pub fn get_register(&self, name: &str, io: &dyn RegisterIo) -> Result<Vec<u8>, GenApiError> {
1525        let node = self.get_register_node(name)?;
1526        ensure_readable(&node.access, name)?;
1527        ensure_device_port(name, node.port.as_deref())?;
1528        if let Some((ref value, generation)) = *node.cache.borrow()
1529            && generation == self.generation.get()
1530        {
1531            return Ok(value.clone());
1532        }
1533        let (address, len) = self.resolve_address(name, &node.addressing, io)?;
1534        let raw = io.read(address, len as usize)?;
1535        node.cache
1536            .replace(Some((raw.clone(), self.generation.get())));
1537        debug!(node = %name, len = raw.len(), "get_register");
1538        Ok(raw)
1539    }
1540
1541    /// Write a `<Register>` node's bytes using the provided transport.
1542    ///
1543    /// `data` must be exactly the declared length. Unlike [`NodeMap::set_string`],
1544    /// which pads with NULs, a short slice is refused: zero-padding a
1545    /// file-transfer buffer to 100 000 bytes because the caller supplied 12 is
1546    /// data loss, not a convenience.
1547    pub fn set_register(
1548        &self,
1549        name: &str,
1550        data: &[u8],
1551        io: &dyn RegisterIo,
1552    ) -> Result<(), GenApiError> {
1553        let node = self.get_register_node(name)?;
1554        self.ensure_writable_now(name, &node.access, io)?;
1555        ensure_device_port(name, node.port.as_deref())?;
1556        let (address, len) = self.resolve_address(name, &node.addressing, io)?;
1557        if data.len() != len as usize {
1558            return Err(GenApiError::Range(format!(
1559                "register '{name}' is {len} bytes; got {}",
1560                data.len()
1561            )));
1562        }
1563        io.write(address, data)?;
1564        node.cache
1565            .replace(Some((data.to_vec(), self.generation.get())));
1566        self.invalidate_dependents(name);
1567        debug!(node = %name, len = data.len(), "set_register");
1568        Ok(())
1569    }
1570
1571    /// Evaluate a Converter in the read direction (`<FormulaFrom>`).
1572    fn evaluate_converter(
1573        &self,
1574        node: &ConverterNode,
1575        io: &dyn RegisterIo,
1576        stack: &mut HashSet<String>,
1577    ) -> Result<SkValue, GenApiError> {
1578        if !stack.insert(node.name.clone()) {
1579            stack.remove(&node.name);
1580            return Err(GenApiError::ExprEval {
1581                name: node.name.clone(),
1582                msg: "cyclic dependency".into(),
1583            });
1584        }
1585        let result = self.eval_formula(
1586            &node.name,
1587            &node.ast_from,
1588            &node.vars_from,
1589            &[],
1590            eval_mode(node.output),
1591            io,
1592            stack,
1593        );
1594        stack.remove(&node.name);
1595        let value = result?;
1596        debug!(node = %node.name, value = %value, "evaluate Converter");
1597        Ok(value)
1598    }
1599
1600    /// Evaluate an IntConverter in the read direction (`<FormulaFrom>`).
1601    fn evaluate_int_converter(
1602        &self,
1603        node: &IntConverterNode,
1604        io: &dyn RegisterIo,
1605        stack: &mut HashSet<String>,
1606    ) -> Result<i64, GenApiError> {
1607        if !stack.insert(node.name.clone()) {
1608            stack.remove(&node.name);
1609            return Err(GenApiError::ExprEval {
1610                name: node.name.clone(),
1611                msg: "cyclic dependency".into(),
1612            });
1613        }
1614        let result = self.eval_formula(
1615            &node.name,
1616            &node.ast_from,
1617            &node.vars_from,
1618            &[],
1619            EvalMode::Integer,
1620            io,
1621            stack,
1622        );
1623        stack.remove(&node.name);
1624        let int_value = result?.as_i64();
1625        debug!(node = %node.name, int_value, "evaluate IntConverter");
1626        Ok(int_value)
1627    }
1628}
1629
1630/// Build one runtime node from its declaration, recording the nodes it depends
1631/// on in `dependents`.
1632fn build_node(
1633    decl: NodeDecl,
1634    dependents: &mut HashMap<String, Vec<String>>,
1635) -> Result<(String, Node), GenApiError> {
1636    match decl {
1637        NodeDecl::Integer {
1638            name,
1639            meta,
1640            addressing,
1641            len,
1642            access,
1643            min,
1644            max,
1645            inc,
1646            unit,
1647            bitfield,
1648            sign,
1649            byte_order,
1650            selectors,
1651            selected_if,
1652            pvalue,
1653            p_max,
1654            p_min,
1655            value,
1656            predicates,
1657        } => {
1658            if let Some(ref addr) = addressing {
1659                register_addressing_dependency(dependents, &name, addr);
1660            }
1661            if let Some(ref pv) = pvalue {
1662                dependents.entry(pv.clone()).or_default().push(name.clone());
1663            }
1664            if let Some(ref pm) = p_max {
1665                dependents.entry(pm.clone()).or_default().push(name.clone());
1666            }
1667            if let Some(ref pm) = p_min {
1668                dependents.entry(pm.clone()).or_default().push(name.clone());
1669            }
1670            for (selector, _) in &selected_if {
1671                dependents
1672                    .entry(selector.clone())
1673                    .or_default()
1674                    .push(name.clone());
1675            }
1676            register_predicate_dependencies(dependents, &name, &predicates);
1677            let node = IntegerNode {
1678                name: name.clone(),
1679                meta,
1680                addressing,
1681                len,
1682                access,
1683                min,
1684                max,
1685                inc,
1686                unit,
1687                bitfield,
1688                sign,
1689                byte_order,
1690                selectors,
1691                selected_if,
1692                pvalue,
1693                p_max,
1694                p_min,
1695                value,
1696                predicates,
1697                cache: std::cell::RefCell::new(None),
1698                raw_cache: std::cell::RefCell::new(None),
1699            };
1700            Ok((name, Node::Integer(node)))
1701        }
1702        NodeDecl::Float {
1703            name,
1704            meta,
1705            addressing,
1706            access,
1707            min,
1708            max,
1709            unit,
1710            scale,
1711            offset,
1712            selectors,
1713            selected_if,
1714            pvalue,
1715            encoding,
1716            byte_order,
1717            predicates,
1718        } => {
1719            if let Some(ref addr) = addressing {
1720                register_addressing_dependency(dependents, &name, addr);
1721            }
1722            if let Some(ref pv) = pvalue {
1723                dependents.entry(pv.clone()).or_default().push(name.clone());
1724            }
1725            for (selector, _) in &selected_if {
1726                dependents
1727                    .entry(selector.clone())
1728                    .or_default()
1729                    .push(name.clone());
1730            }
1731            register_predicate_dependencies(dependents, &name, &predicates);
1732            let node = FloatNode {
1733                name: name.clone(),
1734                meta,
1735                addressing,
1736                access,
1737                min,
1738                max,
1739                unit,
1740                scale,
1741                offset,
1742                selectors,
1743                selected_if,
1744                pvalue,
1745                encoding,
1746                byte_order,
1747                predicates,
1748                cache: std::cell::RefCell::new(None),
1749            };
1750            Ok((name, Node::Float(node)))
1751        }
1752        NodeDecl::Enum {
1753            name,
1754            meta,
1755            addressing,
1756            access,
1757            entries,
1758            default,
1759            selectors,
1760            selected_if,
1761            pvalue,
1762            predicates,
1763        } => {
1764            if let Some(ref addr) = addressing {
1765                register_addressing_dependency(dependents, &name, addr);
1766            }
1767            if let Some(ref pv) = pvalue {
1768                dependents.entry(pv.clone()).or_default().push(name.clone());
1769            }
1770            for (selector, _) in &selected_if {
1771                dependents
1772                    .entry(selector.clone())
1773                    .or_default()
1774                    .push(name.clone());
1775            }
1776            register_predicate_dependencies(dependents, &name, &predicates);
1777            let mut providers = Vec::new();
1778            let mut provider_set = HashSet::new();
1779            for entry in &entries {
1780                if let EnumValueSrc::FromNode(node_name) = &entry.value {
1781                    dependents
1782                        .entry(node_name.clone())
1783                        .or_default()
1784                        .push(name.clone());
1785                    if provider_set.insert(node_name.clone()) {
1786                        providers.push(node_name.clone());
1787                    }
1788                }
1789                register_predicate_dependencies(dependents, &name, &entry.predicates);
1790            }
1791            providers.sort();
1792            let node = EnumNode {
1793                name: name.clone(),
1794                meta,
1795                addressing,
1796                access,
1797                pvalue,
1798                entries,
1799                default,
1800                selectors,
1801                selected_if,
1802                providers,
1803                predicates,
1804                value_cache: std::cell::RefCell::new(None),
1805                mapping_cache: std::cell::RefCell::new(None),
1806            };
1807            Ok((name, Node::Enum(node)))
1808        }
1809        NodeDecl::Boolean {
1810            name,
1811            meta,
1812            addressing,
1813            len,
1814            access,
1815            bitfield,
1816            selectors,
1817            selected_if,
1818            pvalue,
1819            on_value,
1820            off_value,
1821            predicates,
1822        } => {
1823            if let Some(ref addr) = addressing {
1824                register_addressing_dependency(dependents, &name, addr);
1825            }
1826            if let Some(ref pv) = pvalue {
1827                dependents.entry(pv.clone()).or_default().push(name.clone());
1828            }
1829            for (selector, _) in &selected_if {
1830                dependents
1831                    .entry(selector.clone())
1832                    .or_default()
1833                    .push(name.clone());
1834            }
1835            register_predicate_dependencies(dependents, &name, &predicates);
1836            let node = BooleanNode {
1837                name: name.clone(),
1838                meta,
1839                addressing,
1840                len,
1841                access,
1842                bitfield,
1843                selectors,
1844                selected_if,
1845                pvalue,
1846                on_value,
1847                off_value,
1848                predicates,
1849                cache: std::cell::RefCell::new(None),
1850                raw_cache: std::cell::RefCell::new(None),
1851            };
1852            Ok((name, Node::Boolean(node)))
1853        }
1854        NodeDecl::Command {
1855            name,
1856            meta,
1857            address,
1858            len,
1859            pvalue,
1860            command_value,
1861            predicates,
1862        } => {
1863            if let Some(ref pv) = pvalue {
1864                dependents.entry(pv.clone()).or_default().push(name.clone());
1865            }
1866            register_predicate_dependencies(dependents, &name, &predicates);
1867            let node = CommandNode {
1868                name: name.clone(),
1869                meta,
1870                address,
1871                len,
1872                pvalue,
1873                command_value,
1874                predicates,
1875            };
1876            Ok((name, Node::Command(node)))
1877        }
1878        NodeDecl::Category {
1879            name,
1880            meta,
1881            children,
1882            predicates,
1883        } => {
1884            register_predicate_dependencies(dependents, &name, &predicates);
1885            let node = CategoryNode {
1886                name: name.clone(),
1887                meta,
1888                children,
1889                predicates,
1890            };
1891            Ok((name, Node::Category(node)))
1892        }
1893        NodeDecl::SwissKnife(decl) => {
1894            let name = decl.name;
1895            let meta = decl.meta;
1896            let expr = decl.expr;
1897            let variables = decl.variables;
1898            let output = decl.output;
1899            let predicates = decl.predicates;
1900            let mut ast = parse_expression(&expr).map_err(|err| GenApiError::ExprParse {
1901                name: name.clone(),
1902                msg: err.to_string(),
1903            })?;
1904            substitute(&mut ast, &formula_bindings(&name, &decl.bindings)?);
1905            let mut used = HashSet::new();
1906            collect_identifiers(&ast, &mut used);
1907            for ident in &used {
1908                // `E` and `PI` are language constants, not variables, so
1909                // they legitimately appear without a `<pVariable>`.
1910                if !variables.iter().any(|(var, _)| var == ident) && !is_builtin_constant(ident) {
1911                    return Err(GenApiError::UnknownVariable {
1912                        name: name.clone(),
1913                        var: ident.clone(),
1914                    });
1915                }
1916            }
1917            for (_, provider) in &variables {
1918                dependents
1919                    .entry(provider.clone())
1920                    .or_default()
1921                    .push(name.clone());
1922            }
1923            register_predicate_dependencies(dependents, &name, &predicates);
1924            let node = SkNode {
1925                name: name.clone(),
1926                meta,
1927                output,
1928                ast,
1929                vars: variables,
1930                predicates,
1931                cache: std::cell::RefCell::new(None),
1932            };
1933            Ok((name, Node::SwissKnife(node)))
1934        }
1935        NodeDecl::Converter(decl) => {
1936            let name = decl.name;
1937            let bindings = formula_bindings(&name, &decl.bindings)?;
1938            let mut ast_to =
1939                parse_expression(&decl.formula_to).map_err(|err| GenApiError::ExprParse {
1940                    name: name.clone(),
1941                    msg: format!("FormulaTo: {err}"),
1942                })?;
1943            substitute(&mut ast_to, &bindings);
1944            let mut ast_from =
1945                parse_expression(&decl.formula_from).map_err(|err| GenApiError::ExprParse {
1946                    name: name.clone(),
1947                    msg: format!("FormulaFrom: {err}"),
1948                })?;
1949            substitute(&mut ast_from, &bindings);
1950            // Register dependencies for all variable providers
1951            for (_, provider) in &decl.variables_to {
1952                dependents
1953                    .entry(provider.clone())
1954                    .or_default()
1955                    .push(name.clone());
1956            }
1957            for (_, provider) in &decl.variables_from {
1958                if !decl.variables_to.iter().any(|(_, p)| p == provider) {
1959                    dependents
1960                        .entry(provider.clone())
1961                        .or_default()
1962                        .push(name.clone());
1963                }
1964            }
1965            // Also depend on p_value
1966            dependents
1967                .entry(decl.p_value.clone())
1968                .or_default()
1969                .push(name.clone());
1970            register_predicate_dependencies(dependents, &name, &decl.predicates);
1971            let node = ConverterNode {
1972                name: name.clone(),
1973                meta: decl.meta,
1974                p_value: decl.p_value,
1975                ast_to,
1976                ast_from,
1977                vars_to: decl.variables_to,
1978                vars_from: decl.variables_from,
1979                unit: decl.unit,
1980                output: decl.output,
1981                predicates: decl.predicates,
1982                cache: std::cell::RefCell::new(None),
1983            };
1984            Ok((name, Node::Converter(node)))
1985        }
1986        NodeDecl::IntConverter(decl) => {
1987            let name = decl.name;
1988            let bindings = formula_bindings(&name, &decl.bindings)?;
1989            let mut ast_to =
1990                parse_expression(&decl.formula_to).map_err(|err| GenApiError::ExprParse {
1991                    name: name.clone(),
1992                    msg: format!("FormulaTo: {err}"),
1993                })?;
1994            substitute(&mut ast_to, &bindings);
1995            let mut ast_from =
1996                parse_expression(&decl.formula_from).map_err(|err| GenApiError::ExprParse {
1997                    name: name.clone(),
1998                    msg: format!("FormulaFrom: {err}"),
1999                })?;
2000            substitute(&mut ast_from, &bindings);
2001            for (_, provider) in &decl.variables_to {
2002                dependents
2003                    .entry(provider.clone())
2004                    .or_default()
2005                    .push(name.clone());
2006            }
2007            for (_, provider) in &decl.variables_from {
2008                if !decl.variables_to.iter().any(|(_, p)| p == provider) {
2009                    dependents
2010                        .entry(provider.clone())
2011                        .or_default()
2012                        .push(name.clone());
2013                }
2014            }
2015            dependents
2016                .entry(decl.p_value.clone())
2017                .or_default()
2018                .push(name.clone());
2019            register_predicate_dependencies(dependents, &name, &decl.predicates);
2020            let node = IntConverterNode {
2021                name: name.clone(),
2022                meta: decl.meta,
2023                p_value: decl.p_value,
2024                ast_to,
2025                ast_from,
2026                vars_to: decl.variables_to,
2027                vars_from: decl.variables_from,
2028                unit: decl.unit,
2029                predicates: decl.predicates,
2030                cache: std::cell::RefCell::new(None),
2031            };
2032            Ok((name, Node::IntConverter(node)))
2033        }
2034        NodeDecl::String(decl) => {
2035            let name = decl.name;
2036            register_addressing_dependency(dependents, &name, &decl.addressing);
2037            register_predicate_dependencies(dependents, &name, &decl.predicates);
2038            let node = StringNode {
2039                name: name.clone(),
2040                meta: decl.meta,
2041                addressing: decl.addressing,
2042                access: decl.access,
2043                predicates: decl.predicates,
2044                cache: std::cell::RefCell::new(None),
2045            };
2046            Ok((name, Node::String(node)))
2047        }
2048        NodeDecl::Register(decl) => {
2049            let name = decl.name;
2050            register_addressing_dependency(dependents, &name, &decl.addressing);
2051            register_predicate_dependencies(dependents, &name, &decl.predicates);
2052            let node = RegisterNode {
2053                name: name.clone(),
2054                meta: decl.meta,
2055                addressing: decl.addressing,
2056                access: decl.access,
2057                port: decl.port,
2058                predicates: decl.predicates,
2059                cache: std::cell::RefCell::new(None),
2060            };
2061            Ok((name, Node::Register(node)))
2062        }
2063        // `NodeDecl` is `#[non_exhaustive]`, so this crate can no longer match
2064        // it exhaustively and the compiler will not point at this function when
2065        // a variant is added. Fail loudly rather than defaulting: a node type
2066        // the XML layer understands but this one silently drops is precisely
2067        // the class of defect GA-02 existed to end.
2068        other => Err(GenApiError::Unsupported(format!(
2069            "node '{}' has declaration kind '{}', which this nodemap cannot build; \
2070             viva-genapi-xml understands it but viva-genapi has no arm for it",
2071            other.name(),
2072            other.kind()
2073        ))),
2074    }
2075}
2076
2077/// Effective signedness of an integer node's payload.
2078///
2079/// `<Sign>` is the only signal. `<Min>` says nothing about it: it constrains
2080/// the *feature* value, while `<Sign>` describes the *register payload*
2081/// encoding, and the two live on different nodes when a feature delegates
2082/// through `<pValue>`.
2083///
2084/// Inferring "signed" from a negative `<Min>` looks reasonable and is
2085/// unsalvageable in practice. `<Min>` is optional and defaults to `i64::MIN`,
2086/// which is negative — and across the whole vendor corpus **not one** of the
2087/// 4 300 `<IntReg>` or 5 479 `<MaskedIntReg>` nodes declares it. The
2088/// inference therefore fires on every register-backed integer on every real
2089/// camera, which is precisely the case `<Sign>` exists to decide.
2090fn integer_sign(node: &IntegerNode) -> Sign {
2091    node.sign
2092}
2093
2094/// Resolve a formula's `<Constant>` and named `<Expression>` declarations into
2095/// sub-ASTs ready for substitution.
2096///
2097/// Declaration order is significant: an `<Expression>` may reference constants
2098/// and expressions declared before it, so each is substituted against what has
2099/// been bound so far. That also makes a self- or forward-reference resolve to
2100/// nothing rather than looping.
2101fn formula_bindings(
2102    node: &str,
2103    declared: &FormulaBindings,
2104) -> Result<HashMap<String, SkAst>, GenApiError> {
2105    let mut bindings: HashMap<String, SkAst> = HashMap::new();
2106    for (name, literal) in &declared.constants {
2107        let value = parse_expression(literal).map_err(|err| GenApiError::ExprParse {
2108            name: node.to_string(),
2109            msg: format!("Constant {name}: {err}"),
2110        })?;
2111        bindings.insert(name.clone(), value);
2112    }
2113    for (name, formula) in &declared.expressions {
2114        let mut ast = parse_expression(formula).map_err(|err| GenApiError::ExprParse {
2115            name: node.to_string(),
2116            msg: format!("Expression {name}: {err}"),
2117        })?;
2118        substitute(&mut ast, &bindings);
2119        bindings.insert(name.clone(), ast);
2120    }
2121    Ok(bindings)
2122}
2123
2124/// Narrow a formula result to `i64` for an integer-typed feature.
2125///
2126/// An integer formula that stayed integral is exact; one that picked up a
2127/// float along the way (a fractional literal, a transcendental function)
2128/// rounds, as the GenApi integer output rule requires.
2129fn sk_to_i64(name: &str, value: SkValue) -> Result<i64, GenApiError> {
2130    match value {
2131        SkValue::Int(value) => Ok(value),
2132        SkValue::Float(value) => round_to_i64(name, value),
2133    }
2134}
2135
2136/// Arithmetic mode implied by a formula's declared output type.
2137fn eval_mode(output: SkOutput) -> EvalMode {
2138    match output {
2139        SkOutput::Integer => EvalMode::Integer,
2140        SkOutput::Float => EvalMode::Float,
2141    }
2142}
2143
2144/// Map a formula evaluation failure onto the public error type.
2145fn expr_error(name: &str, err: SkEvalError) -> GenApiError {
2146    match err {
2147        SkEvalError::UnknownVariable(var) => GenApiError::UnknownVariable {
2148            name: name.to_string(),
2149            var,
2150        },
2151        SkEvalError::DivisionByZero => GenApiError::ExprEval {
2152            name: name.to_string(),
2153            msg: "division by zero".into(),
2154        },
2155        SkEvalError::UnknownFunction(func) => GenApiError::ExprEval {
2156            name: name.to_string(),
2157            msg: format!("unknown function: {func}"),
2158        },
2159        SkEvalError::ArityMismatch {
2160            name: func,
2161            expected,
2162            got,
2163        } => GenApiError::ExprEval {
2164            name: name.to_string(),
2165            msg: format!("function {func} expects {expected} args, got {got}"),
2166        },
2167    }
2168}
2169
2170impl TryFrom<XmlModel> for NodeMap {
2171    type Error = GenApiError;
2172
2173    fn try_from(model: XmlModel) -> Result<Self, Self::Error> {
2174        NodeMap::try_from_xml(model)
2175    }
2176}