Skip to main content

viva_genapi/
swissknife.rs

1//! GenApi formula parser and evaluator.
2//!
3//! Covers the expression language used by `<SwissKnife>`, `<IntSwissKnife>`,
4//! `<Converter>` and `<IntConverter>` nodes:
5//!
6//! - Arithmetic: `+ - * / %` and `**` for power
7//! - Comparison: `< <= > >=`, `=` (equality) and `<>` (inequality)
8//! - Logical: `&& ||`
9//! - Bitwise: `& | ^ ~ << >>`
10//! - Ternary: `condition ? then : else`
11//! - Functions: `SGN NEG ATAN SIN COS TAN ABS EXP LN LG SQRT TRUNC ROUND
12//!   FLOOR CEIL ASIN ACOS`, plus the constants `E` and `PI`
13//!
14//! Two details trip up implementations that reach for a C-like grammar, and
15//! both appear in the majority of real vendor descriptions:
16//!
17//! 1. **`=` is equality, `<>` is inequality.** There is no assignment in the
18//!    language, so `=` is never ambiguous. `==` and `!=` are accepted as
19//!    tolerated aliases.
20//! 2. **Integer formulas evaluate in `i64`, not `f64`.** `IntSwissKnife` and
21//!    `IntConverter` use [`EvalMode::Integer`], where `/` truncates and 64-bit
22//!    register values stay exact. Evaluating `(HIGH << 32) | LOW` in `f64`
23//!    silently loses the low bits.
24//!
25//! Operator precedence and the integer/float promotion rules follow the GenApi
26//! specification, cross-checked against the reference implementation in
27//! aravis (`src/arvevaluator.c`: `arv_evaluator_token_infos` for precedence,
28//! the `integer_mode` branches of `arv_evaluator_evaluate` for promotion).
29
30use std::collections::HashSet;
31use std::fmt;
32
33/// Numeric value flowing through a GenApi formula.
34///
35/// The language is dynamically typed over `i64` and `f64`. Keeping the two
36/// apart matters: register values are integers up to 64 bits wide, and `f64`
37/// cannot represent them all exactly.
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub enum Value {
40    /// Integer value.
41    Int(i64),
42    /// Floating point value.
43    Float(f64),
44}
45
46impl Value {
47    /// View the value as an `i64`, truncating a float towards zero.
48    pub fn as_i64(self) -> i64 {
49        match self {
50            Value::Int(value) => value,
51            Value::Float(value) => value as i64,
52        }
53    }
54
55    /// View the value as an `f64`.
56    pub fn as_f64(self) -> f64 {
57        match self {
58            Value::Int(value) => value as f64,
59            Value::Float(value) => value,
60        }
61    }
62
63    /// Whether the value is held as an integer.
64    pub fn is_int(self) -> bool {
65        matches!(self, Value::Int(_))
66    }
67
68    /// Whether the value counts as true in a condition (non-zero).
69    pub fn is_truthy(self) -> bool {
70        match self {
71            Value::Int(value) => value != 0,
72            Value::Float(value) => value != 0.0,
73        }
74    }
75
76    fn from_bool(value: bool) -> Self {
77        Value::Int(i64::from(value))
78    }
79}
80
81impl fmt::Display for Value {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match self {
84            Value::Int(value) => write!(f, "{value}"),
85            Value::Float(value) => write!(f, "{value}"),
86        }
87    }
88}
89
90/// Arithmetic mode for a formula.
91///
92/// `IntSwissKnife` and `IntConverter` declare integer semantics; `SwissKnife`
93/// and `Converter` declare floating point ones. The distinction is observable:
94/// `7 / 2` is `3` in [`EvalMode::Integer`] and `3.5` in [`EvalMode::Float`].
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
96pub enum EvalMode {
97    /// Integer arithmetic: `/` truncates, results stay in `i64`.
98    Integer,
99    /// Floating point arithmetic. Bitwise operators still work on `i64`.
100    #[default]
101    Float,
102}
103
104/// Parsed GenApi formula represented as an abstract syntax tree.
105#[derive(Debug, Clone)]
106pub enum AstNode {
107    /// Numeric literal.
108    Literal(Value),
109    /// Variable lookup resolved at evaluation time.
110    Variable(String),
111    /// Unary operator applied to a sub-expression.
112    Unary {
113        /// Operator kind.
114        op: UnaryOp,
115        /// Operand expression.
116        expr: Box<AstNode>,
117    },
118    /// Binary operator combining two sub-expressions.
119    Binary {
120        /// Operator kind.
121        op: BinaryOp,
122        /// Left-hand side operand.
123        left: Box<AstNode>,
124        /// Right-hand side operand.
125        right: Box<AstNode>,
126    },
127    /// Ternary conditional: `condition ? then_expr : else_expr`.
128    Ternary {
129        /// Condition expression (non-zero is truthy).
130        cond: Box<AstNode>,
131        /// Expression evaluated when condition is truthy.
132        then_expr: Box<AstNode>,
133        /// Expression evaluated when condition is falsy.
134        else_expr: Box<AstNode>,
135    },
136    /// Function call with arguments.
137    FnCall {
138        /// Function name.
139        name: String,
140        /// Arguments to the function.
141        args: Vec<AstNode>,
142    },
143}
144
145/// Binary operator kinds supported by the GenApi formula language.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum BinaryOp {
148    // Arithmetic
149    Add,
150    Sub,
151    Mul,
152    Div,
153    Mod,
154    Pow,
155    // Comparison
156    Lt,
157    Le,
158    Gt,
159    Ge,
160    Eq,
161    Ne,
162    // Logical
163    And,
164    Or,
165    // Bitwise
166    BitAnd,
167    BitOr,
168    BitXor,
169    Shl,
170    Shr,
171}
172
173/// Unary operator kinds supported by the GenApi formula language.
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub enum UnaryOp {
176    Plus,
177    Minus,
178    Not,
179    BitNot,
180}
181
182/// Error produced while parsing a GenApi formula.
183#[derive(Debug, Clone)]
184pub struct ParseError {
185    msg: String,
186    offset: usize,
187}
188
189impl ParseError {
190    fn new<S: Into<String>>(msg: S, offset: usize) -> Self {
191        Self {
192            msg: msg.into(),
193            offset,
194        }
195    }
196
197    /// Byte offset within the formula at which the error was detected.
198    pub fn offset(&self) -> usize {
199        self.offset
200    }
201}
202
203impl fmt::Display for ParseError {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        write!(f, "{} (at offset {})", self.msg, self.offset)
206    }
207}
208
209impl std::error::Error for ParseError {}
210
211/// Error produced while evaluating a GenApi formula.
212#[derive(Debug, Clone)]
213pub enum EvalError {
214    /// Variable referenced by the expression has no bound value.
215    UnknownVariable(String),
216    /// Division by zero occurred.
217    DivisionByZero,
218    /// Unknown function name.
219    UnknownFunction(String),
220    /// Wrong number of arguments to function.
221    ArityMismatch {
222        name: String,
223        expected: usize,
224        got: usize,
225    },
226}
227
228impl fmt::Display for EvalError {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        match self {
231            EvalError::UnknownVariable(var) => write!(f, "unknown variable {var}"),
232            EvalError::DivisionByZero => write!(f, "division by zero"),
233            EvalError::UnknownFunction(name) => write!(f, "unknown function {name}"),
234            EvalError::ArityMismatch {
235                name,
236                expected,
237                got,
238            } => {
239                write!(f, "function {name} expects {expected} args, got {got}")
240            }
241        }
242    }
243}
244
245impl std::error::Error for EvalError {}
246
247/// Parse a GenApi formula into an [`AstNode`].
248pub fn parse_expression(input: &str) -> Result<AstNode, ParseError> {
249    let mut parser = Parser::new(input)?;
250    let expr = parser.parse_ternary()?;
251    if !matches!(parser.lookahead, Token::End) {
252        return Err(ParseError::new("unexpected trailing tokens", parser.pos));
253    }
254    Ok(expr)
255}
256
257/// Value of a GenApi built-in constant, if `name` denotes one.
258///
259/// The language defines `E` and `PI` as constants rather than variables, so a
260/// formula may reference them without a matching `<pVariable>`. Matching is
261/// case-insensitive, as in the reference implementation.
262pub fn builtin_constant(name: &str) -> Option<Value> {
263    match name.to_ascii_lowercase().as_str() {
264        "e" => Some(Value::Float(std::f64::consts::E)),
265        "pi" => Some(Value::Float(std::f64::consts::PI)),
266        _ => None,
267    }
268}
269
270/// Whether `name` denotes a GenApi built-in constant.
271///
272/// Callers validating that every identifier in a formula has a `<pVariable>`
273/// must exempt these.
274pub fn is_builtin_constant(name: &str) -> bool {
275    builtin_constant(name).is_some()
276}
277
278/// Evaluate an [`AstNode`] using the provided variable resolver.
279///
280/// The resolver receives variable identifiers and must return their value.
281/// An identifier the resolver rejects with [`EvalError::UnknownVariable`] is
282/// retried against the built-in constants before the error is propagated, so a
283/// declared `<pVariable>` always shadows a same-named constant.
284pub fn evaluate(
285    ast: &AstNode,
286    vars: &mut dyn FnMut(&str) -> Result<Value, EvalError>,
287    mode: EvalMode,
288) -> Result<Value, EvalError> {
289    match ast {
290        AstNode::Literal(value) => Ok(*value),
291        AstNode::Variable(name) => match vars(name) {
292            Ok(value) => Ok(value),
293            Err(EvalError::UnknownVariable(_)) => {
294                builtin_constant(name).ok_or_else(|| EvalError::UnknownVariable(name.clone()))
295            }
296            Err(other) => Err(other),
297        },
298        AstNode::Unary { op, expr } => {
299            let inner = evaluate(expr, vars, mode)?;
300            Ok(eval_unary(*op, inner, mode))
301        }
302        AstNode::Binary { op, left, right } => {
303            // Short-circuit evaluation for logical operators
304            match op {
305                BinaryOp::And => {
306                    let lhs = evaluate(left, vars, mode)?;
307                    if !lhs.is_truthy() {
308                        return Ok(Value::from_bool(false));
309                    }
310                    let rhs = evaluate(right, vars, mode)?;
311                    Ok(Value::from_bool(rhs.is_truthy()))
312                }
313                BinaryOp::Or => {
314                    let lhs = evaluate(left, vars, mode)?;
315                    if lhs.is_truthy() {
316                        return Ok(Value::from_bool(true));
317                    }
318                    let rhs = evaluate(right, vars, mode)?;
319                    Ok(Value::from_bool(rhs.is_truthy()))
320                }
321                _ => {
322                    let lhs = evaluate(left, vars, mode)?;
323                    let rhs = evaluate(right, vars, mode)?;
324                    eval_binary(*op, lhs, rhs, mode)
325                }
326            }
327        }
328        AstNode::Ternary {
329            cond,
330            then_expr,
331            else_expr,
332        } => {
333            if evaluate(cond, vars, mode)?.is_truthy() {
334                evaluate(then_expr, vars, mode)
335            } else {
336                evaluate(else_expr, vars, mode)
337            }
338        }
339        AstNode::FnCall { name, args } => {
340            let evaluated: Result<Vec<Value>, _> =
341                args.iter().map(|a| evaluate(a, vars, mode)).collect();
342            let arg_vals = evaluated?;
343            eval_function(name, &arg_vals, mode)
344        }
345    }
346}
347
348fn eval_unary(op: UnaryOp, value: Value, mode: EvalMode) -> Value {
349    match op {
350        UnaryOp::Plus => value,
351        UnaryOp::Minus => {
352            if mode == EvalMode::Integer || value.is_int() {
353                Value::Int(value.as_i64().wrapping_neg())
354            } else {
355                Value::Float(-value.as_f64())
356            }
357        }
358        UnaryOp::Not => Value::from_bool(!value.is_truthy()),
359        UnaryOp::BitNot => Value::Int(!value.as_i64()),
360    }
361}
362
363/// Whether an arithmetic result should stay integral.
364fn integral(mode: EvalMode, lhs: Value, rhs: Value) -> bool {
365    mode == EvalMode::Integer || (lhs.is_int() && rhs.is_int())
366}
367
368/// Shift counts are masked to the register width, matching the reference
369/// implementation's reliance on hardware shift semantics. Without this,
370/// `HIGH << 32` on a wide value panics in a debug build.
371fn shift_amount(value: Value) -> u32 {
372    (value.as_i64() as u64 & 63) as u32
373}
374
375fn eval_binary(op: BinaryOp, lhs: Value, rhs: Value, mode: EvalMode) -> Result<Value, EvalError> {
376    Ok(match op {
377        BinaryOp::Add => {
378            if integral(mode, lhs, rhs) {
379                Value::Int(lhs.as_i64().wrapping_add(rhs.as_i64()))
380            } else {
381                Value::Float(lhs.as_f64() + rhs.as_f64())
382            }
383        }
384        BinaryOp::Sub => {
385            if integral(mode, lhs, rhs) {
386                Value::Int(lhs.as_i64().wrapping_sub(rhs.as_i64()))
387            } else {
388                Value::Float(lhs.as_f64() - rhs.as_f64())
389            }
390        }
391        BinaryOp::Mul => {
392            if integral(mode, lhs, rhs) {
393                Value::Int(lhs.as_i64().wrapping_mul(rhs.as_i64()))
394            } else {
395                Value::Float(lhs.as_f64() * rhs.as_f64())
396            }
397        }
398        // Division is the one operator where the mode alone decides: an
399        // integer formula truncates even when a literal happens to be
400        // fractional, and a float formula never truncates.
401        BinaryOp::Div => {
402            if mode == EvalMode::Integer {
403                let divisor = rhs.as_i64();
404                if divisor == 0 {
405                    return Err(EvalError::DivisionByZero);
406                }
407                Value::Int(lhs.as_i64().wrapping_div(divisor))
408            } else {
409                let divisor = rhs.as_f64();
410                if divisor == 0.0 {
411                    return Err(EvalError::DivisionByZero);
412                }
413                Value::Float(lhs.as_f64() / divisor)
414            }
415        }
416        BinaryOp::Mod => {
417            let divisor = rhs.as_i64();
418            if divisor == 0 {
419                return Err(EvalError::DivisionByZero);
420            }
421            Value::Int(lhs.as_i64().wrapping_rem(divisor))
422        }
423        BinaryOp::Pow => {
424            let result = lhs.as_f64().powf(rhs.as_f64());
425            if mode == EvalMode::Integer {
426                Value::Int(result as i64)
427            } else {
428                Value::Float(result)
429            }
430        }
431        BinaryOp::Lt => compare(lhs, rhs, mode, |a, b| a < b, |a, b| a < b),
432        BinaryOp::Le => compare(lhs, rhs, mode, |a, b| a <= b, |a, b| a <= b),
433        BinaryOp::Gt => compare(lhs, rhs, mode, |a, b| a > b, |a, b| a > b),
434        BinaryOp::Ge => compare(lhs, rhs, mode, |a, b| a >= b, |a, b| a >= b),
435        BinaryOp::Eq => compare(lhs, rhs, mode, |a, b| a == b, |a, b| a == b),
436        BinaryOp::Ne => compare(lhs, rhs, mode, |a, b| a != b, |a, b| a != b),
437        BinaryOp::And | BinaryOp::Or => unreachable!("handled by short-circuit"),
438        BinaryOp::BitAnd => Value::Int(lhs.as_i64() & rhs.as_i64()),
439        BinaryOp::BitOr => Value::Int(lhs.as_i64() | rhs.as_i64()),
440        BinaryOp::BitXor => Value::Int(lhs.as_i64() ^ rhs.as_i64()),
441        BinaryOp::Shl => Value::Int(lhs.as_i64().wrapping_shl(shift_amount(rhs))),
442        BinaryOp::Shr => Value::Int(lhs.as_i64().wrapping_shr(shift_amount(rhs))),
443    })
444}
445
446/// Compare two values, exactly when both sides are integral.
447///
448/// Comparing register values through `f64` would make `0x100000000000001` and
449/// `0x100000000000000` equal; comparing them as `i64` does not.
450fn compare(
451    lhs: Value,
452    rhs: Value,
453    mode: EvalMode,
454    int_cmp: fn(i64, i64) -> bool,
455    float_cmp: fn(f64, f64) -> bool,
456) -> Value {
457    if integral(mode, lhs, rhs) {
458        Value::from_bool(int_cmp(lhs.as_i64(), rhs.as_i64()))
459    } else {
460        Value::from_bool(float_cmp(lhs.as_f64(), rhs.as_f64()))
461    }
462}
463
464fn eval_function(name: &str, args: &[Value], mode: EvalMode) -> Result<Value, EvalError> {
465    // GenApi function names are case-insensitive; vendors write both `LG` and
466    // `lg`, `SGN` and `sgn`.
467    let name_lower = name.to_ascii_lowercase();
468
469    // Transcendental functions always produce a float, matching the reference
470    // implementation.
471    let float_fn = |f: fn(f64) -> f64| -> Result<Value, EvalError> {
472        expect_args(name, args, 1).map(|a| Value::Float(f(a[0].as_f64())))
473    };
474    // Functions that preserve integrality when their argument is integral.
475    let rounding_fn = |f: fn(f64) -> f64| -> Result<Value, EvalError> {
476        expect_args(name, args, 1).map(|a| {
477            if mode == EvalMode::Integer || a[0].is_int() {
478                Value::Int(a[0].as_i64())
479            } else {
480                Value::Float(f(a[0].as_f64()))
481            }
482        })
483    };
484
485    match name_lower.as_str() {
486        // --- GenApi standard functions -----------------------------------
487        "sin" => float_fn(f64::sin),
488        "cos" => float_fn(f64::cos),
489        "tan" => float_fn(f64::tan),
490        "asin" => float_fn(f64::asin),
491        "acos" => float_fn(f64::acos),
492        "atan" => float_fn(f64::atan),
493        "sqrt" => float_fn(f64::sqrt),
494        "exp" => float_fn(f64::exp),
495        "ln" => float_fn(f64::ln),
496        // `LG` is the base-10 logarithm. Omitting it made every Baumer TXG
497        // description fail at evaluation time.
498        "lg" => float_fn(f64::log10),
499        "trunc" => rounding_fn(f64::trunc),
500        "floor" => rounding_fn(f64::floor),
501        "ceil" => rounding_fn(f64::ceil),
502        "round" => rounding_fn(f64::round),
503        "abs" => expect_args(name, args, 1).map(|a| {
504            if mode == EvalMode::Integer || a[0].is_int() {
505                Value::Int(a[0].as_i64().wrapping_abs())
506            } else {
507                Value::Float(a[0].as_f64().abs())
508            }
509        }),
510        "neg" => expect_args(name, args, 1).map(|a| eval_unary(UnaryOp::Minus, a[0], mode)),
511        "sgn" | "sign" => expect_args(name, args, 1).map(|a| {
512            if mode == EvalMode::Integer || a[0].is_int() {
513                Value::Int(a[0].as_i64().signum())
514            } else {
515                let value = a[0].as_f64();
516                Value::Float(if value > 0.0 {
517                    1.0
518                } else if value < 0.0 {
519                    -1.0
520                } else {
521                    0.0
522                })
523            }
524        }),
525        // The constants are usually written bare, but the call form appears
526        // in the wild too.
527        "e" => expect_args(name, args, 0).map(|_| Value::Float(std::f64::consts::E)),
528        "pi" => expect_args(name, args, 0).map(|_| Value::Float(std::f64::consts::PI)),
529
530        // --- Accepted extensions ------------------------------------------
531        // Not in the GenApi standard, but harmless to accept and already
532        // relied on by our own fixtures.
533        "log" | "log10" => float_fn(f64::log10),
534        "log2" => float_fn(f64::log2),
535        "atan2" => {
536            expect_args(name, args, 2).map(|a| Value::Float(a[0].as_f64().atan2(a[1].as_f64())))
537        }
538        "pow" => eval_binary(BinaryOp::Pow, args_at(name, args, 2)?[0], args[1], mode),
539        "fmod" => eval_binary(BinaryOp::Mod, args_at(name, args, 2)?[0], args[1], mode),
540        "min" => expect_args(name, args, 2).map(|a| pick(a[0], a[1], mode, true)),
541        "max" => expect_args(name, args, 2).map(|a| pick(a[0], a[1], mode, false)),
542
543        _ => Err(EvalError::UnknownFunction(name.to_string())),
544    }
545}
546
547fn pick(lhs: Value, rhs: Value, mode: EvalMode, want_min: bool) -> Value {
548    let take_lhs = if integral(mode, lhs, rhs) {
549        (lhs.as_i64() <= rhs.as_i64()) == want_min
550    } else {
551        (lhs.as_f64() <= rhs.as_f64()) == want_min
552    };
553    if take_lhs { lhs } else { rhs }
554}
555
556fn args_at<'a>(name: &str, args: &'a [Value], expected: usize) -> Result<&'a [Value], EvalError> {
557    expect_args(name, args, expected)
558}
559
560fn expect_args<'a>(
561    name: &str,
562    args: &'a [Value],
563    expected: usize,
564) -> Result<&'a [Value], EvalError> {
565    if args.len() != expected {
566        Err(EvalError::ArityMismatch {
567            name: name.to_string(),
568            expected,
569            got: args.len(),
570        })
571    } else {
572        Ok(args)
573    }
574}
575
576/// Replace variable references with sub-expressions.
577///
578/// Backs the GenApi `<Constant>` and named `<Expression>` elements, which let
579/// a formula name a literal or a reusable sub-formula:
580///
581/// ```xml
582/// <IntSwissKnife Name="Example">
583///   <pVariable Name="X">X</pVariable>
584///   <Constant Name="TEN">10</Constant>
585///   <Expression Name="XPLUS2">TEN + X</Expression>
586///   <Formula>TEN * XPLUS2</Formula>
587/// </IntSwissKnife>
588/// ```
589///
590/// Substitution happens once, at build time, so evaluation stays a plain walk
591/// over the tree.
592pub fn substitute(ast: &mut AstNode, bindings: &std::collections::HashMap<String, AstNode>) {
593    match ast {
594        AstNode::Literal(_) => {}
595        AstNode::Variable(name) => {
596            if let Some(replacement) = bindings.get(name.as_str()) {
597                *ast = replacement.clone();
598            }
599        }
600        AstNode::Unary { expr, .. } => substitute(expr, bindings),
601        AstNode::Binary { left, right, .. } => {
602            substitute(left, bindings);
603            substitute(right, bindings);
604        }
605        AstNode::Ternary {
606            cond,
607            then_expr,
608            else_expr,
609        } => {
610            substitute(cond, bindings);
611            substitute(then_expr, bindings);
612            substitute(else_expr, bindings);
613        }
614        AstNode::FnCall { args, .. } => {
615            for arg in args {
616                substitute(arg, bindings);
617            }
618        }
619    }
620}
621
622/// Collect all variable identifiers referenced by the AST.
623///
624/// Built-in constants are reported like any other identifier; callers that
625/// validate against `<pVariable>` declarations should filter them with
626/// [`is_builtin_constant`].
627pub fn collect_identifiers(ast: &AstNode, out: &mut HashSet<String>) {
628    match ast {
629        AstNode::Literal(_) => {}
630        AstNode::Variable(name) => {
631            out.insert(name.clone());
632        }
633        AstNode::Unary { expr, .. } => collect_identifiers(expr, out),
634        AstNode::Binary { left, right, .. } => {
635            collect_identifiers(left, out);
636            collect_identifiers(right, out);
637        }
638        AstNode::Ternary {
639            cond,
640            then_expr,
641            else_expr,
642        } => {
643            collect_identifiers(cond, out);
644            collect_identifiers(then_expr, out);
645            collect_identifiers(else_expr, out);
646        }
647        AstNode::FnCall { args, .. } => {
648            for arg in args {
649                collect_identifiers(arg, out);
650            }
651        }
652    }
653}
654
655// ============================================================================
656// Lexer
657// ============================================================================
658
659#[derive(Debug, Clone, PartialEq)]
660enum Token {
661    Int(i64),
662    Float(f64),
663    Ident(String),
664    // Arithmetic
665    Plus,
666    Minus,
667    Star,
668    Slash,
669    Percent,
670    StarStar, // **
671    // Comparison
672    Lt,
673    Le,
674    Gt,
675    Ge,
676    Eq, // `=` (and the tolerated `==`)
677    Ne, // `<>` (and the tolerated `!=`)
678    // Logical
679    AmpAmp,
680    PipePipe,
681    Bang,
682    // Bitwise
683    Amp,
684    Pipe,
685    Caret,
686    Tilde,
687    LtLt,
688    GtGt,
689    // Ternary
690    Question,
691    Colon,
692    // Grouping
693    LParen,
694    RParen,
695    Comma,
696    End,
697}
698
699struct Lexer<'a> {
700    input: &'a [u8],
701    pos: usize,
702}
703
704impl<'a> Lexer<'a> {
705    fn new(input: &'a str) -> Self {
706        Lexer {
707            input: input.as_bytes(),
708            pos: 0,
709        }
710    }
711
712    fn peek(&self) -> Option<u8> {
713        self.input.get(self.pos).copied()
714    }
715
716    fn peek_next(&self) -> Option<u8> {
717        self.input.get(self.pos + 1).copied()
718    }
719
720    fn advance_by(&mut self, n: usize) {
721        self.pos += n;
722    }
723
724    fn next_token(&mut self) -> Result<Token, ParseError> {
725        self.skip_ws();
726        let Some(byte) = self.peek() else {
727            return Ok(Token::End);
728        };
729
730        match byte {
731            b'0'..=b'9' | b'.' => self.lex_number(),
732            b'a'..=b'z' | b'A'..=b'Z' | b'_' => self.lex_ident(),
733            b'+' => {
734                self.advance_by(1);
735                Ok(Token::Plus)
736            }
737            b'-' => {
738                self.advance_by(1);
739                Ok(Token::Minus)
740            }
741            b'*' => {
742                if self.peek_next() == Some(b'*') {
743                    self.advance_by(2);
744                    Ok(Token::StarStar)
745                } else {
746                    self.advance_by(1);
747                    Ok(Token::Star)
748                }
749            }
750            b'/' => {
751                self.advance_by(1);
752                Ok(Token::Slash)
753            }
754            b'%' => {
755                self.advance_by(1);
756                Ok(Token::Percent)
757            }
758            b'<' => match self.peek_next() {
759                Some(b'=') => {
760                    self.advance_by(2);
761                    Ok(Token::Le)
762                }
763                Some(b'<') => {
764                    self.advance_by(2);
765                    Ok(Token::LtLt)
766                }
767                // `<>` is the GenApi inequality operator.
768                Some(b'>') => {
769                    self.advance_by(2);
770                    Ok(Token::Ne)
771                }
772                _ => {
773                    self.advance_by(1);
774                    Ok(Token::Lt)
775                }
776            },
777            b'>' => match self.peek_next() {
778                Some(b'=') => {
779                    self.advance_by(2);
780                    Ok(Token::Ge)
781                }
782                Some(b'>') => {
783                    self.advance_by(2);
784                    Ok(Token::GtGt)
785                }
786                _ => {
787                    self.advance_by(1);
788                    Ok(Token::Gt)
789                }
790            },
791            // `=` is equality. The language has no assignment, so there is
792            // nothing to disambiguate; `==` is accepted as an alias.
793            b'=' => {
794                if self.peek_next() == Some(b'=') {
795                    self.advance_by(2);
796                } else {
797                    self.advance_by(1);
798                }
799                Ok(Token::Eq)
800            }
801            b'!' => {
802                if self.peek_next() == Some(b'=') {
803                    self.advance_by(2);
804                    Ok(Token::Ne)
805                } else {
806                    self.advance_by(1);
807                    Ok(Token::Bang)
808                }
809            }
810            b'&' => {
811                if self.peek_next() == Some(b'&') {
812                    self.advance_by(2);
813                    Ok(Token::AmpAmp)
814                } else {
815                    self.advance_by(1);
816                    Ok(Token::Amp)
817                }
818            }
819            b'|' => {
820                if self.peek_next() == Some(b'|') {
821                    self.advance_by(2);
822                    Ok(Token::PipePipe)
823                } else {
824                    self.advance_by(1);
825                    Ok(Token::Pipe)
826                }
827            }
828            b'^' => {
829                self.advance_by(1);
830                Ok(Token::Caret)
831            }
832            b'~' => {
833                self.advance_by(1);
834                Ok(Token::Tilde)
835            }
836            b'?' => {
837                self.advance_by(1);
838                Ok(Token::Question)
839            }
840            b':' => {
841                self.advance_by(1);
842                Ok(Token::Colon)
843            }
844            b'(' => {
845                self.advance_by(1);
846                Ok(Token::LParen)
847            }
848            b')' => {
849                self.advance_by(1);
850                Ok(Token::RParen)
851            }
852            b',' => {
853                self.advance_by(1);
854                Ok(Token::Comma)
855            }
856            _ => Err(ParseError::new(
857                format!("unexpected character '{}'", byte as char),
858                self.pos,
859            )),
860        }
861    }
862
863    fn skip_ws(&mut self) {
864        while let Some(byte) = self.peek() {
865            if byte.is_ascii_whitespace() {
866                self.pos += 1;
867            } else {
868                break;
869            }
870        }
871    }
872
873    fn lex_number(&mut self) -> Result<Token, ParseError> {
874        let start = self.pos;
875
876        // Check for hex literal: 0x or 0X
877        if self.peek() == Some(b'0') {
878            let next = self.input.get(self.pos + 1).copied();
879            if next == Some(b'x') || next == Some(b'X') {
880                self.pos += 2; // skip "0x"
881                let hex_start = self.pos;
882                while let Some(b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F') = self.peek() {
883                    self.pos += 1;
884                }
885                if self.pos == hex_start {
886                    return Err(ParseError::new("hex literal has no digits", start));
887                }
888                let hex_text = std::str::from_utf8(&self.input[hex_start..self.pos])
889                    .map_err(|_| ParseError::new("invalid UTF-8 in hex literal", start))?;
890                let value = u64::from_str_radix(hex_text, 16).map_err(|_| {
891                    ParseError::new(format!("invalid hex literal: 0x{hex_text}"), start)
892                })?;
893                // Masks such as 0xFFFFFFFFFFFFFFFF wrap to their two's
894                // complement, as they do in the reference implementation.
895                return Ok(Token::Int(value as i64));
896            }
897        }
898
899        let mut seen_digit = false;
900        let mut seen_dot = false;
901        let mut seen_exp = false;
902
903        while let Some(byte) = self.peek() {
904            match byte {
905                b'0'..=b'9' => {
906                    seen_digit = true;
907                    self.pos += 1;
908                }
909                b'.' if !seen_dot && !seen_exp => {
910                    seen_dot = true;
911                    self.pos += 1;
912                }
913                b'e' | b'E' if !seen_exp && seen_digit => {
914                    // `1e3` is scientific notation, but `1 e` would be a
915                    // literal followed by the constant E. Only treat it as an
916                    // exponent when a digit or sign follows.
917                    match self.peek_next() {
918                        Some(b'0'..=b'9' | b'+' | b'-') => {
919                            seen_exp = true;
920                            self.pos += 1;
921                            if let Some(b'+' | b'-') = self.peek() {
922                                self.pos += 1;
923                            }
924                        }
925                        _ => break,
926                    }
927                }
928                _ => break,
929            }
930        }
931        if !seen_digit {
932            return Err(ParseError::new("invalid number literal", start));
933        }
934        let slice = &self.input[start..self.pos];
935        let text = std::str::from_utf8(slice)
936            .map_err(|_| ParseError::new("invalid UTF-8 in number", start))?;
937        if !seen_dot
938            && !seen_exp
939            && let Ok(value) = text.parse::<i64>()
940        {
941            return Ok(Token::Int(value));
942        }
943        let value = text
944            .parse::<f64>()
945            .map_err(|_| ParseError::new(format!("failed to parse number: {text}"), start))?;
946        Ok(Token::Float(value))
947    }
948
949    fn lex_ident(&mut self) -> Result<Token, ParseError> {
950        let start = self.pos;
951        self.pos += 1;
952        while let Some(byte) = self.peek() {
953            if byte.is_ascii_alphanumeric() || byte == b'_' {
954                self.pos += 1;
955            } else {
956                break;
957            }
958        }
959        let slice = &self.input[start..self.pos];
960        let text = std::str::from_utf8(slice)
961            .map_err(|_| ParseError::new("invalid UTF-8 in identifier", start))?;
962        Ok(Token::Ident(text.to_string()))
963    }
964}
965
966// ============================================================================
967// Parser
968//
969// Precedence, loosest to tightest. This chain mirrors the priority column of
970// aravis' `arv_evaluator_token_infos` table, which is itself the GenApi
971// specification's table:
972//
973//  1. Ternary:        ?:      (right-associative)
974//  2. Logical OR:     ||
975//  3. Logical AND:    &&
976//  4. Bitwise OR:     |
977//  5. Bitwise XOR:    ^
978//  6. Bitwise AND:    &
979//  7. Equality:       =  <>
980//  8. Comparison:     <  <=  >  >=
981//  9. Shift:          << >>
982// 10. Additive:       +  -
983// 11. Multiplicative: *  /  %
984// 12. Power:          **      (right-associative)
985// 13. Unary:          +  -  !  ~
986// 14. Primary:        numbers, identifiers, function calls, (expr)
987//
988// Note that equality binds *looser* than comparison, so `A < B = C` parses as
989// `(A < B) = C`. That is what the standard says, and vendors rely on it.
990// ============================================================================
991
992struct Parser<'a> {
993    lexer: Lexer<'a>,
994    lookahead: Token,
995    /// Offset of the start of the lookahead token, for error reporting.
996    pos: usize,
997}
998
999impl<'a> Parser<'a> {
1000    fn new(input: &'a str) -> Result<Self, ParseError> {
1001        let mut lexer = Lexer::new(input);
1002        let lookahead = lexer.next_token()?;
1003        let pos = lexer.pos;
1004        Ok(Parser {
1005            lexer,
1006            lookahead,
1007            pos,
1008        })
1009    }
1010
1011    fn advance(&mut self) -> Result<(), ParseError> {
1012        self.lookahead = self.lexer.next_token()?;
1013        self.pos = self.lexer.pos;
1014        Ok(())
1015    }
1016
1017    // Level 1: Ternary
1018    fn parse_ternary(&mut self) -> Result<AstNode, ParseError> {
1019        let cond = self.parse_or()?;
1020        if matches!(self.lookahead, Token::Question) {
1021            self.advance()?;
1022            let then_expr = self.parse_ternary()?;
1023            if !matches!(self.lookahead, Token::Colon) {
1024                return Err(ParseError::new(
1025                    "expected ':' in ternary expression",
1026                    self.pos,
1027                ));
1028            }
1029            self.advance()?;
1030            let else_expr = self.parse_ternary()?;
1031            Ok(AstNode::Ternary {
1032                cond: Box::new(cond),
1033                then_expr: Box::new(then_expr),
1034                else_expr: Box::new(else_expr),
1035            })
1036        } else {
1037            Ok(cond)
1038        }
1039    }
1040
1041    // Level 2: Logical OR
1042    fn parse_or(&mut self) -> Result<AstNode, ParseError> {
1043        let mut node = self.parse_and()?;
1044        while matches!(self.lookahead, Token::PipePipe) {
1045            self.advance()?;
1046            let rhs = self.parse_and()?;
1047            node = AstNode::Binary {
1048                op: BinaryOp::Or,
1049                left: Box::new(node),
1050                right: Box::new(rhs),
1051            };
1052        }
1053        Ok(node)
1054    }
1055
1056    // Level 3: Logical AND
1057    fn parse_and(&mut self) -> Result<AstNode, ParseError> {
1058        let mut node = self.parse_bitor()?;
1059        while matches!(self.lookahead, Token::AmpAmp) {
1060            self.advance()?;
1061            let rhs = self.parse_bitor()?;
1062            node = AstNode::Binary {
1063                op: BinaryOp::And,
1064                left: Box::new(node),
1065                right: Box::new(rhs),
1066            };
1067        }
1068        Ok(node)
1069    }
1070
1071    // Level 4: Bitwise OR
1072    fn parse_bitor(&mut self) -> Result<AstNode, ParseError> {
1073        let mut node = self.parse_bitxor()?;
1074        while matches!(self.lookahead, Token::Pipe) {
1075            self.advance()?;
1076            let rhs = self.parse_bitxor()?;
1077            node = AstNode::Binary {
1078                op: BinaryOp::BitOr,
1079                left: Box::new(node),
1080                right: Box::new(rhs),
1081            };
1082        }
1083        Ok(node)
1084    }
1085
1086    // Level 5: Bitwise XOR
1087    fn parse_bitxor(&mut self) -> Result<AstNode, ParseError> {
1088        let mut node = self.parse_bitand()?;
1089        while matches!(self.lookahead, Token::Caret) {
1090            self.advance()?;
1091            let rhs = self.parse_bitand()?;
1092            node = AstNode::Binary {
1093                op: BinaryOp::BitXor,
1094                left: Box::new(node),
1095                right: Box::new(rhs),
1096            };
1097        }
1098        Ok(node)
1099    }
1100
1101    // Level 6: Bitwise AND
1102    fn parse_bitand(&mut self) -> Result<AstNode, ParseError> {
1103        let mut node = self.parse_equality()?;
1104        while matches!(self.lookahead, Token::Amp) {
1105            self.advance()?;
1106            let rhs = self.parse_equality()?;
1107            node = AstNode::Binary {
1108                op: BinaryOp::BitAnd,
1109                left: Box::new(node),
1110                right: Box::new(rhs),
1111            };
1112        }
1113        Ok(node)
1114    }
1115
1116    // Level 7: Equality
1117    fn parse_equality(&mut self) -> Result<AstNode, ParseError> {
1118        let mut node = self.parse_comparison()?;
1119        loop {
1120            let op = match &self.lookahead {
1121                Token::Eq => BinaryOp::Eq,
1122                Token::Ne => BinaryOp::Ne,
1123                _ => break,
1124            };
1125            self.advance()?;
1126            let rhs = self.parse_comparison()?;
1127            node = AstNode::Binary {
1128                op,
1129                left: Box::new(node),
1130                right: Box::new(rhs),
1131            };
1132        }
1133        Ok(node)
1134    }
1135
1136    // Level 8: Comparison
1137    fn parse_comparison(&mut self) -> Result<AstNode, ParseError> {
1138        let mut node = self.parse_shift()?;
1139        loop {
1140            let op = match &self.lookahead {
1141                Token::Lt => BinaryOp::Lt,
1142                Token::Le => BinaryOp::Le,
1143                Token::Gt => BinaryOp::Gt,
1144                Token::Ge => BinaryOp::Ge,
1145                _ => break,
1146            };
1147            self.advance()?;
1148            let rhs = self.parse_shift()?;
1149            node = AstNode::Binary {
1150                op,
1151                left: Box::new(node),
1152                right: Box::new(rhs),
1153            };
1154        }
1155        Ok(node)
1156    }
1157
1158    // Level 9: Shift
1159    fn parse_shift(&mut self) -> Result<AstNode, ParseError> {
1160        let mut node = self.parse_additive()?;
1161        loop {
1162            let op = match &self.lookahead {
1163                Token::LtLt => BinaryOp::Shl,
1164                Token::GtGt => BinaryOp::Shr,
1165                _ => break,
1166            };
1167            self.advance()?;
1168            let rhs = self.parse_additive()?;
1169            node = AstNode::Binary {
1170                op,
1171                left: Box::new(node),
1172                right: Box::new(rhs),
1173            };
1174        }
1175        Ok(node)
1176    }
1177
1178    // Level 10: Additive
1179    fn parse_additive(&mut self) -> Result<AstNode, ParseError> {
1180        let mut node = self.parse_multiplicative()?;
1181        loop {
1182            let op = match &self.lookahead {
1183                Token::Plus => BinaryOp::Add,
1184                Token::Minus => BinaryOp::Sub,
1185                _ => break,
1186            };
1187            self.advance()?;
1188            let rhs = self.parse_multiplicative()?;
1189            node = AstNode::Binary {
1190                op,
1191                left: Box::new(node),
1192                right: Box::new(rhs),
1193            };
1194        }
1195        Ok(node)
1196    }
1197
1198    // Level 11: Multiplicative
1199    fn parse_multiplicative(&mut self) -> Result<AstNode, ParseError> {
1200        let mut node = self.parse_power()?;
1201        loop {
1202            let op = match &self.lookahead {
1203                Token::Star => BinaryOp::Mul,
1204                Token::Slash => BinaryOp::Div,
1205                Token::Percent => BinaryOp::Mod,
1206                _ => break,
1207            };
1208            self.advance()?;
1209            let rhs = self.parse_power()?;
1210            node = AstNode::Binary {
1211                op,
1212                left: Box::new(node),
1213                right: Box::new(rhs),
1214            };
1215        }
1216        Ok(node)
1217    }
1218
1219    // Level 12: Power (right-associative)
1220    fn parse_power(&mut self) -> Result<AstNode, ParseError> {
1221        let base = self.parse_unary()?;
1222        if matches!(self.lookahead, Token::StarStar) {
1223            self.advance()?;
1224            let exp = self.parse_power()?; // Right-associative
1225            Ok(AstNode::Binary {
1226                op: BinaryOp::Pow,
1227                left: Box::new(base),
1228                right: Box::new(exp),
1229            })
1230        } else {
1231            Ok(base)
1232        }
1233    }
1234
1235    // Level 13: Unary
1236    fn parse_unary(&mut self) -> Result<AstNode, ParseError> {
1237        let op = match &self.lookahead {
1238            Token::Plus => UnaryOp::Plus,
1239            Token::Minus => UnaryOp::Minus,
1240            Token::Bang => UnaryOp::Not,
1241            Token::Tilde => UnaryOp::BitNot,
1242            _ => return self.parse_primary(),
1243        };
1244        self.advance()?;
1245        let expr = self.parse_unary()?;
1246        Ok(AstNode::Unary {
1247            op,
1248            expr: Box::new(expr),
1249        })
1250    }
1251
1252    // Level 14: Primary
1253    fn parse_primary(&mut self) -> Result<AstNode, ParseError> {
1254        match self.lookahead.clone() {
1255            Token::Int(value) => {
1256                self.advance()?;
1257                Ok(AstNode::Literal(Value::Int(value)))
1258            }
1259            Token::Float(value) => {
1260                self.advance()?;
1261                Ok(AstNode::Literal(Value::Float(value)))
1262            }
1263            Token::Ident(name) => {
1264                self.advance()?;
1265                // Check for function call
1266                if matches!(self.lookahead, Token::LParen) {
1267                    self.advance()?;
1268                    let mut args = Vec::new();
1269                    if !matches!(self.lookahead, Token::RParen) {
1270                        args.push(self.parse_ternary()?);
1271                        while matches!(self.lookahead, Token::Comma) {
1272                            self.advance()?;
1273                            args.push(self.parse_ternary()?);
1274                        }
1275                    }
1276                    if !matches!(self.lookahead, Token::RParen) {
1277                        return Err(ParseError::new(
1278                            "expected ')' after function arguments",
1279                            self.pos,
1280                        ));
1281                    }
1282                    self.advance()?;
1283                    Ok(AstNode::FnCall { name, args })
1284                } else {
1285                    Ok(AstNode::Variable(name))
1286                }
1287            }
1288            Token::LParen => {
1289                self.advance()?;
1290                let expr = self.parse_ternary()?;
1291                if !matches!(self.lookahead, Token::RParen) {
1292                    return Err(ParseError::new("missing closing ')'", self.pos));
1293                }
1294                self.advance()?;
1295                Ok(expr)
1296            }
1297            Token::End => Err(ParseError::new("unexpected end of expression", self.pos)),
1298            other => Err(ParseError::new(
1299                format!("unexpected token {other:?}"),
1300                self.pos,
1301            )),
1302        }
1303    }
1304}
1305
1306#[cfg(test)]
1307mod tests {
1308    use super::*;
1309
1310    fn eval_with(expr: &str, vars: &[(&str, Value)], mode: EvalMode) -> Value {
1311        let ast = parse_expression(expr).expect("parse failed");
1312        let mut resolver = |name: &str| {
1313            vars.iter()
1314                .find(|(n, _)| *n == name)
1315                .map(|(_, v)| *v)
1316                .ok_or_else(|| EvalError::UnknownVariable(name.to_string()))
1317        };
1318        evaluate(&ast, &mut resolver, mode).expect("eval failed")
1319    }
1320
1321    /// Evaluate in float mode with float variables (the `SwissKnife` case).
1322    fn eval_expr(expr: &str, vars: &[(&str, f64)]) -> f64 {
1323        let bound: Vec<(&str, Value)> = vars.iter().map(|(n, v)| (*n, Value::Float(*v))).collect();
1324        eval_with(expr, &bound, EvalMode::Float).as_f64()
1325    }
1326
1327    /// Evaluate in integer mode with integer variables (the `IntSwissKnife`
1328    /// case).
1329    fn eval_int(expr: &str, vars: &[(&str, i64)]) -> i64 {
1330        let bound: Vec<(&str, Value)> = vars.iter().map(|(n, v)| (*n, Value::Int(*v))).collect();
1331        eval_with(expr, &bound, EvalMode::Integer).as_i64()
1332    }
1333
1334    #[test]
1335    fn basic_arithmetic() {
1336        assert!((eval_expr("(A + 2) * 3 - B / 4", &[("A", 4.0), ("B", 8.0)]) - 16.0).abs() < 1e-6);
1337        assert!((eval_expr("-A + 10 / (B - 5)", &[("A", 3.0), ("B", 7.0)]) - 2.0).abs() < 1e-6);
1338    }
1339
1340    #[test]
1341    fn comparisons() {
1342        assert_eq!(eval_expr("5 < 10", &[]), 1.0);
1343        assert_eq!(eval_expr("5 > 10", &[]), 0.0);
1344        assert_eq!(eval_expr("5 <= 5", &[]), 1.0);
1345        assert_eq!(eval_expr("5 >= 6", &[]), 0.0);
1346        assert_eq!(eval_expr("A < B", &[("A", 3.0), ("B", 5.0)]), 1.0);
1347    }
1348
1349    /// `=` is the GenApi equality operator. Rejecting it made 27 of the 30
1350    /// documents in the vendor corpus unopenable (issue #35).
1351    #[test]
1352    fn single_equals_is_equality() {
1353        assert_eq!(eval_int("5 = 5", &[]), 1);
1354        assert_eq!(eval_int("5 = 6", &[]), 0);
1355        // The formula from the reporter's Hikrobot MV-CS050-10GC.
1356        assert_eq!(
1357            eval_int("(TEMPCTRLMODE = 1) ? 1 : 0", &[("TEMPCTRLMODE", 1)]),
1358            1
1359        );
1360        assert_eq!(
1361            eval_int("(TEMPCTRLMODE = 1) ? 1 : 0", &[("TEMPCTRLMODE", 0)]),
1362            0
1363        );
1364        // `==` stays accepted.
1365        assert_eq!(eval_int("5 == 5", &[]), 1);
1366    }
1367
1368    /// `<>` is the GenApi inequality operator.
1369    #[test]
1370    fn angle_brackets_are_inequality() {
1371        assert_eq!(eval_int("5 <> 6", &[]), 1);
1372        assert_eq!(eval_int("5 <> 5", &[]), 0);
1373        assert_eq!(eval_int("(DS <> 0) ? 1 : 0", &[("DS", 3)]), 1);
1374        // Still distinct from shift and comparison.
1375        assert_eq!(eval_int("1 << 3", &[]), 8);
1376        assert_eq!(eval_int("(1 < 3) = 1", &[]), 1);
1377        // `!=` stays accepted.
1378        assert_eq!(eval_int("5 != 5", &[]), 0);
1379    }
1380
1381    #[test]
1382    fn equality_binds_looser_than_comparison() {
1383        // `A < B = C` is `(A < B) = C`, not `A < (B = C)`.
1384        assert_eq!(eval_int("1 < 3 = 1", &[]), 1);
1385        assert_eq!(eval_int("3 < 1 = 0", &[]), 1);
1386        // And bitwise AND binds looser than equality.
1387        assert_eq!(eval_int("1 & 1 = 1", &[]), 1);
1388    }
1389
1390    #[test]
1391    fn ternary_expression() {
1392        assert_eq!(eval_expr("1 ? 10 : 20", &[]), 10.0);
1393        assert_eq!(eval_expr("0 ? 10 : 20", &[]), 20.0);
1394        assert_eq!(eval_expr("A > 5 ? A : 5", &[("A", 3.0)]), 5.0);
1395        assert_eq!(eval_expr("A > 5 ? A : 5", &[("A", 10.0)]), 10.0);
1396        // Nested ternary
1397        assert_eq!(
1398            eval_expr("A < 0 ? -1 : A > 0 ? 1 : 0", &[("A", -5.0)]),
1399            -1.0
1400        );
1401        assert_eq!(eval_expr("A < 0 ? -1 : A > 0 ? 1 : 0", &[("A", 5.0)]), 1.0);
1402        assert_eq!(eval_expr("A < 0 ? -1 : A > 0 ? 1 : 0", &[("A", 0.0)]), 0.0);
1403    }
1404
1405    #[test]
1406    fn logical_operators() {
1407        assert_eq!(eval_expr("1 && 1", &[]), 1.0);
1408        assert_eq!(eval_expr("1 && 0", &[]), 0.0);
1409        assert_eq!(eval_expr("0 || 1", &[]), 1.0);
1410        assert_eq!(eval_expr("0 || 0", &[]), 0.0);
1411        assert_eq!(eval_expr("!0", &[]), 1.0);
1412        assert_eq!(eval_expr("!1", &[]), 0.0);
1413        assert_eq!(eval_expr("!5", &[]), 0.0);
1414    }
1415
1416    #[test]
1417    fn bitwise_operators() {
1418        assert_eq!(eval_expr("5 & 3", &[]), 1.0); // 101 & 011 = 001
1419        assert_eq!(eval_expr("5 | 3", &[]), 7.0); // 101 | 011 = 111
1420        assert_eq!(eval_expr("5 ^ 3", &[]), 6.0); // 101 ^ 011 = 110
1421        assert_eq!(eval_expr("1 << 3", &[]), 8.0);
1422        assert_eq!(eval_expr("8 >> 2", &[]), 2.0);
1423        assert_eq!(eval_int("~0", &[]), -1);
1424    }
1425
1426    /// A 64-bit register split across two 32-bit halves is the canonical
1427    /// reason integer formulas cannot go through `f64`: the low bits of the
1428    /// result are not representable.
1429    #[test]
1430    fn wide_shifts_stay_exact() {
1431        assert_eq!(
1432            eval_int(
1433                "(HIGH << 32) | LOW",
1434                &[("HIGH", 0x1234_5678), ("LOW", 0x9ABC_DEF1)]
1435            ),
1436            0x1234_5678_9ABC_DEF1
1437        );
1438        // MAC address composition, as used by GevMACAddrHigh/Low.
1439        assert_eq!(
1440            eval_int(
1441                "( ( HI & 0x0000FFFF ) << 32 ) | LO",
1442                &[("HI", 0x205C), ("LO", 0x208F_8000)]
1443            ),
1444            0x205C_208F_8000
1445        );
1446        // A shift wide enough to overflow must not panic in a debug build.
1447        assert_eq!(eval_int("V << 63", &[("V", 3)]), i64::MIN);
1448        assert_eq!(eval_int("V << 64", &[("V", 3)]), 3);
1449    }
1450
1451    /// Integer formulas truncate on division; float formulas do not.
1452    #[test]
1453    fn division_follows_the_mode() {
1454        assert_eq!(eval_int("7 / 2", &[]), 3);
1455        assert_eq!(eval_int("(IDX / 2) * 4", &[("IDX", 3)]), 4);
1456        assert!((eval_expr("7 / 2", &[]) - 3.5).abs() < 1e-9);
1457        // Reported by the Hikrobot: an offset table that only lands on the
1458        // right register because the division truncates.
1459        assert_eq!(eval_int("OFFSET * 4 / 2", &[("OFFSET", 3)]), 6);
1460    }
1461
1462    #[test]
1463    fn hex_literals() {
1464        assert_eq!(eval_expr("0xFF", &[]), 255.0);
1465        assert_eq!(eval_expr("0x10", &[]), 16.0);
1466        assert_eq!(eval_expr("0x0", &[]), 0.0);
1467        assert_eq!(eval_expr("0xDEAD", &[]), 0xDEAD as f64);
1468        assert_eq!(eval_expr("(0x01080001 >> 16) & 0xFF", &[]), 8.0);
1469        // The aravis PayloadSize formula
1470        assert_eq!(
1471            eval_expr(
1472                "W * H * ((PF>>16)&0xFF) / 8",
1473                &[("W", 512.0), ("H", 512.0), ("PF", 0x01080001_u32 as f64)]
1474            ),
1475            512.0 * 512.0 * 8.0 / 8.0
1476        );
1477        // Lowercase hex digits, as Hikrobot writes them.
1478        assert_eq!(eval_int("PF = 0x0110000c", &[("PF", 0x0110000C)]), 1);
1479    }
1480
1481    #[test]
1482    fn power_operator() {
1483        assert!((eval_expr("2 ** 3", &[]) - 8.0).abs() < 1e-6);
1484        assert!((eval_expr("2 ** 3 ** 2", &[]) - 512.0).abs() < 1e-6); // Right-associative: 2^(3^2) = 2^9
1485    }
1486
1487    #[test]
1488    fn modulo_operator() {
1489        assert!((eval_expr("10 % 3", &[]) - 1.0).abs() < 1e-6);
1490        assert!((eval_expr("17 % 5", &[]) - 2.0).abs() < 1e-6);
1491    }
1492
1493    #[test]
1494    fn functions() {
1495        assert!((eval_expr("abs(-5)", &[]) - 5.0).abs() < 1e-6);
1496        assert!((eval_expr("sqrt(16)", &[]) - 4.0).abs() < 1e-6);
1497        assert!((eval_expr("min(3, 7)", &[]) - 3.0).abs() < 1e-6);
1498        assert!((eval_expr("max(3, 7)", &[]) - 7.0).abs() < 1e-6);
1499        assert!((eval_expr("pow(2, 10)", &[]) - 1024.0).abs() < 1e-6);
1500        assert!((eval_expr("floor(3.7)", &[]) - 3.0).abs() < 1e-6);
1501        assert!((eval_expr("ceil(3.2)", &[]) - 4.0).abs() < 1e-6);
1502        assert!((eval_expr("round(3.5)", &[]) - 4.0).abs() < 1e-6);
1503        assert!((eval_expr("sgn(-5)", &[]) - -1.0).abs() < 1e-6);
1504        assert!((eval_expr("sgn(5)", &[]) - 1.0).abs() < 1e-6);
1505        assert!((eval_expr("sgn(0)", &[]) - 0.0).abs() < 1e-6);
1506    }
1507
1508    /// `LG` is base-10 log. Baumer TXG descriptions use it for a dB scale.
1509    #[test]
1510    fn genapi_standard_functions() {
1511        assert!((eval_expr("LG(1000)", &[]) - 3.0).abs() < 1e-9);
1512        assert!((eval_expr("(20 * (LG(TO/1024)))", &[("TO", 10240.0)]) - 20.0).abs() < 1e-9);
1513        assert!((eval_expr("LN(1)", &[]) - 0.0).abs() < 1e-9);
1514        assert!((eval_expr("TRUNC(3.9)", &[]) - 3.0).abs() < 1e-9);
1515        assert!((eval_expr("NEG(4)", &[]) + 4.0).abs() < 1e-9);
1516        // Names are case-insensitive.
1517        assert!((eval_expr("Sqrt(9)", &[]) - 3.0).abs() < 1e-9);
1518    }
1519
1520    #[test]
1521    fn builtin_constants() {
1522        assert!((eval_expr("PI", &[]) - std::f64::consts::PI).abs() < 1e-12);
1523        assert!((eval_expr("E", &[]) - std::f64::consts::E).abs() < 1e-12);
1524        assert!((eval_expr("2 * PI", &[]) - std::f64::consts::TAU).abs() < 1e-12);
1525        // A declared variable shadows the constant.
1526        assert_eq!(eval_expr("E", &[("E", 7.0)]), 7.0);
1527        assert!(is_builtin_constant("pi"));
1528        assert!(!is_builtin_constant("EXPMODE"));
1529    }
1530
1531    #[test]
1532    fn scientific_notation() {
1533        assert!((eval_expr("1e3", &[]) - 1000.0).abs() < 1e-6);
1534        assert!((eval_expr("1.5e-2", &[]) - 0.015).abs() < 1e-9);
1535        assert!((eval_expr("2.5E+3", &[]) - 2500.0).abs() < 1e-6);
1536    }
1537
1538    #[test]
1539    fn division_by_zero_error() {
1540        let ast = parse_expression("A / B").expect("parse");
1541        let mut vars = |name: &str| match name {
1542            "A" => Ok(Value::Float(5.0)),
1543            "B" => Ok(Value::Float(0.0)),
1544            _ => Err(EvalError::UnknownVariable(name.to_string())),
1545        };
1546        let err = evaluate(&ast, &mut vars, EvalMode::Float).expect_err("division by zero");
1547        assert!(matches!(err, EvalError::DivisionByZero));
1548    }
1549
1550    #[test]
1551    fn complex_basler_style_expression() {
1552        // Basler cameras often use expressions like this for exposure time conversion
1553        let expr = "RawValue < 0 ? 0 : RawValue * 1000 / TickFreq";
1554        assert!(
1555            (eval_expr(expr, &[("RawValue", 500.0), ("TickFreq", 1000.0)]) - 500.0).abs() < 1e-6
1556        );
1557        assert_eq!(
1558            eval_expr(expr, &[("RawValue", -10.0), ("TickFreq", 1000.0)]),
1559            0.0
1560        );
1561    }
1562
1563    /// A representative selection of the shapes seen across the vendor corpus
1564    /// and in the reporter's Hikrobot dump: multi-line formulas, chained
1565    /// ternaries, `=`/`<>` mixed with bitwise masks.
1566    #[test]
1567    fn vendor_formula_shapes_parse() {
1568        let formulas = [
1569            "( ( PINPRES = 1 ) && ( MODE = 0 ) && (SEL <> 2) ) ? 1 : 0",
1570            "((CTRL_REG & 0x03000000)=0x03000000)?1:0",
1571            "((CTRL_REG | 0xFDFFFFFF)=0xFFFFFFFF)?0:1",
1572            "ADDROFFSET = 0 ? 0 : ADDROFFSET * 0x80",
1573            "(FEAT<32)?((GEVOPT>>FEAT)&0x1):((FEAT<64)?((IPOPT>>(FEAT-32))&0x1) : ((FEAT=66)?SCCB:0))",
1574            "(   ((TM = 0) && (LINE1 = 1)) \n || ((TL = 1) && (REVERSE = 0)) \n || (DS <> 0)) ? 1 : 0",
1575            "((TYPE = 0x011a) || (TYPE = 0x011b)) ? (0.4232 * DKELVIN - 334.83) : (DKELVIN /100)",
1576            "(SHUTTERMODE = 0) && (ROLLING = 1)",
1577            "INDEX = 0",
1578            "( MAX % UNIT ) ? ( MAX - (  MAX) % UNIT  ) : ( MAX)",
1579        ];
1580        for formula in formulas {
1581            parse_expression(formula)
1582                .unwrap_or_else(|err| panic!("failed to parse {formula:?}: {err}"));
1583        }
1584    }
1585
1586    #[test]
1587    fn parse_error_reports_offset() {
1588        let err = parse_expression("A + ").expect_err("incomplete expression");
1589        assert!(err.offset() > 0, "offset should point past the operator");
1590        assert!(err.to_string().contains("at offset"));
1591    }
1592
1593    #[test]
1594    fn collect_identifiers_with_ternary() {
1595        let ast = parse_expression("A > B ? C + D : E * F").expect("parse");
1596        let mut ids = HashSet::new();
1597        collect_identifiers(&ast, &mut ids);
1598        assert!(ids.contains("A"));
1599        assert!(ids.contains("B"));
1600        assert!(ids.contains("C"));
1601        assert!(ids.contains("D"));
1602        assert!(ids.contains("E"));
1603        assert!(ids.contains("F"));
1604        assert_eq!(ids.len(), 6);
1605    }
1606
1607    #[test]
1608    fn collect_identifiers_with_functions() {
1609        let ast = parse_expression("max(A, min(B, C))").expect("parse");
1610        let mut ids = HashSet::new();
1611        collect_identifiers(&ast, &mut ids);
1612        assert!(ids.contains("A"));
1613        assert!(ids.contains("B"));
1614        assert!(ids.contains("C"));
1615        assert_eq!(ids.len(), 3);
1616    }
1617}