Skip to main content

cel_parser/
op_table.rs

1//! Operation table for dynamically dispatching operations based on type signatures.
2//!
3//! This module provides a scope-based registry for operations that can be looked up
4//! based on an operation name (string) and the types of the operands. Built-in operations
5//! use compile-time hash tables (via `phf`) for efficient lookup, while custom operations
6//! can be added dynamically through scope functions.
7//!
8//! # Design
9//!
10//! - **Operator symbols as names**: Operations are identified by their operator symbols
11//!   (e.g., `"+"`, `"-"`, `"*"`) to avoid conflicts with valid identifiers.
12//! - **Function pointers**: Built-in operations use stateless function pointers for
13//!   zero-allocation dispatch.
14//! - **Scope stack**: Custom operations are handled through a stack of scope functions
15//!   that can be pushed and popped as needed.
16//! - **Type optimization**: Most built-in operations are homogeneous (both operands share
17//!   a type), so signatures store a primary `TypeId` plus arity. Heterogeneous binary ops
18//!   (e.g. shifts, which accept every signed and unsigned integer RHS) additionally store an RHS
19//!   `TypeId` index.
20//!
21//! # Semantics
22//!
23//! Built-in operations follow Rust language semantics. Deviations are:
24//!
25//! - **Signed integer overflow**: CEL returns `Err` rather than panicking (debug) or wrapping
26//!   (release). Use wrapping arithmetic explicitly if overflow is intended.
27//! - **Bit-shift with out-of-range count**: CEL returns `Err` rather than panicking (debug)
28//!   or masking the shift count (release).
29
30use anyhow::{Result, anyhow};
31use cel_runtime::DynSegment;
32use once_cell::sync::Lazy;
33use phf::phf_map;
34use std::any::TypeId;
35
36use crate::SourceSpan;
37
38/// Wraps a runtime error with span context when the `span-diagnostics` feature is enabled.
39///
40/// When the feature is off this is a no-op and compiles to nothing.
41#[cfg(feature = "span-diagnostics")]
42#[inline]
43fn span_err(span: SourceSpan, e: anyhow::Error) -> anyhow::Error {
44    e.context(crate::SpanContext::new(span))
45}
46
47#[cfg(not(feature = "span-diagnostics"))]
48#[inline]
49fn span_err(_span: SourceSpan, e: anyhow::Error) -> anyhow::Error {
50    e
51}
52
53/// A function that pushes an operation onto a DynSegment.
54///
55/// Receives the segment and the source span of the expression that triggered
56/// this operation. This is a simple function pointer since built-in operations
57/// have no state.
58///
59/// # Examples
60/// See [`OpLookup::register_tuple_op`] for an operation emitter.
61pub type OpFn = fn(&mut DynSegment, SourceSpan) -> Result<()>;
62
63/// A signature for an operator/function whose selected operand is a tuple.
64///
65/// Matches when the operand at `tuple_operand_index` (0-based, in the same
66/// stack order [`DynSegment::peek_stack_infos`] returns) is a tuple whose
67/// element `TypeId`s equal `shape`, in order, and every other peeked operand's
68/// flat `TypeId` equals the corresponding entry in `operand_type_ids` (the
69/// entry at `tuple_operand_index` in `operand_type_ids` is never read).
70///
71/// `operand_type_ids` must have an entry for every non-tuple operand
72/// position: a missing entry (out of bounds, including an entirely empty
73/// vector when there are non-tuple operands) simply never matches, rather
74/// than panicking — it is only safe to omit the whole vector when
75/// `tuple_operand_index` is the *only* operand position.
76///
77/// `shape` is flat: an element position that is itself a nested tuple can
78/// only be recorded as a flat tuple `TypeId`, which matches *any* nested
79/// tuple at that position regardless of its inner arity or element types.
80/// Two registrations that would only differ by that inner shape are not
81/// distinguishable — do not rely on nested-tuple precision at this level.
82///
83/// # Examples
84/// See [`OpLookup::register_tuple_op`].
85pub struct TupleOpSignature {
86    /// Operator/function name this signature is registered under.
87    pub name: String,
88    /// Expected element `TypeId`s, in order, for the tuple-shaped operand.
89    /// See the struct-level note on nested tuples.
90    pub shape: Vec<TypeId>,
91    /// Which peeked operand position must be the tuple.
92    pub tuple_operand_index: usize,
93    /// Flat `TypeId`s expected for the non-tuple operands, in stack order
94    /// (the `tuple_operand_index` entry is ignored).
95    pub operand_type_ids: Vec<TypeId>,
96    /// Function that pushes the operation onto the segment.
97    pub op_fn: OpFn,
98}
99
100/// A scope function that attempts to resolve and apply an operation.
101///
102/// Receives the operation name, the segment, the number of operands on top of the stack,
103/// and the source span of the expression. The scope may call
104/// `segment.peek_stack_infos(num_operands)` to inspect types. Returns `Ok(true)` if
105/// handled, `Ok(false)` if not found, or `Err` on error.
106///
107/// Error messages returned by scope functions surface verbatim to the user. They should be
108/// lowercase, end without a period, and wrap identifiers and type names in backticks.
109///
110/// # Examples
111/// See [`OpLookup::push_scope`] for callback ownership and dispatch.
112pub type ScopeFn =
113    Box<dyn Fn(&str, &mut DynSegment, usize, SourceSpan) -> Result<bool> + Send + Sync>;
114
115/// A signature for a built-in operation.
116///
117/// For homogeneous ops (e.g. `u32 + u32`) `rhs_type_id_index` equals `type_id_index`.
118/// For heterogeneous binary ops (e.g. `u64 << u32`) they differ.
119#[derive(Clone, Copy)]
120struct OpSignature {
121    /// Index into TYPE_IDS for the LHS (or sole) operand type.
122    type_id_index: usize,
123    /// Index into TYPE_IDS for the RHS operand type; equals `type_id_index` for homogeneous ops.
124    rhs_type_id_index: usize,
125    /// Number of operands this operation accepts.
126    arity: u8,
127    /// Function pointer to the operation implementation.
128    op_fn: OpFn,
129}
130
131impl OpSignature {
132    /// Returns the `TypeId` of the LHS (or sole) operand.
133    fn lhs_type_id(&self) -> TypeId {
134        TYPE_IDS[self.type_id_index]
135    }
136
137    /// Returns the `TypeId` of the RHS operand.
138    fn rhs_type_id(&self) -> TypeId {
139        TYPE_IDS[self.rhs_type_id_index]
140    }
141}
142
143/// Single lazy-initialized vector containing all unique TypeIds for built-in types.
144///
145/// This avoids duplicating TypeId storage across all operation signatures.
146static TYPE_IDS: Lazy<Vec<TypeId>> = Lazy::new(|| {
147    vec![
148        TypeId::of::<u8>(),
149        TypeId::of::<u16>(),
150        TypeId::of::<u32>(),
151        TypeId::of::<u64>(),
152        TypeId::of::<u128>(),
153        TypeId::of::<usize>(),
154        TypeId::of::<i8>(),
155        TypeId::of::<i16>(),
156        TypeId::of::<i32>(),
157        TypeId::of::<i64>(),
158        TypeId::of::<i128>(),
159        TypeId::of::<isize>(),
160        TypeId::of::<f32>(),
161        TypeId::of::<f64>(),
162        TypeId::of::<bool>(),
163        TypeId::of::<String>(),
164    ]
165});
166
167// Type index constants for readability
168const TYPE_U8: usize = 0;
169const TYPE_U16: usize = 1;
170const TYPE_U32: usize = 2;
171const TYPE_U64: usize = 3;
172const TYPE_U128: usize = 4;
173const TYPE_USIZE: usize = 5;
174const TYPE_I8: usize = 6;
175const TYPE_I16: usize = 7;
176const TYPE_I32: usize = 8;
177const TYPE_I64: usize = 9;
178const TYPE_I128: usize = 10;
179const TYPE_ISIZE: usize = 11;
180const TYPE_F32: usize = 12;
181const TYPE_F64: usize = 13;
182const TYPE_BOOL: usize = 14;
183const TYPE_STR: usize = 15;
184
185// Helper macros to reduce boilerplate in signature definitions.
186// `sig!` builds a homogeneous signature; `sig_het!` a heterogeneous binary one.
187macro_rules! sig {
188    ($type_idx:expr, $arity:expr, $closure:expr) => {
189        OpSignature {
190            type_id_index: $type_idx,
191            rhs_type_id_index: $type_idx,
192            arity: $arity,
193            op_fn: $closure,
194        }
195    };
196}
197
198macro_rules! sig_het {
199    ($lhs_idx:expr, $rhs_idx:expr, $closure:expr) => {
200        OpSignature {
201            type_id_index: $lhs_idx,
202            rhs_type_id_index: $rhs_idx,
203            arity: 2,
204            op_fn: $closure,
205        }
206    };
207}
208
209// Addition signatures
210static ADD_SIGNATURES: &[OpSignature] = &[
211    sig!(TYPE_U8, 2, |seg, _span| seg
212        .op2(|a: u8, b: u8| a.wrapping_add(b))),
213    sig!(TYPE_U16, 2, |seg, _span| seg
214        .op2(|a: u16, b: u16| a.wrapping_add(b))),
215    sig!(TYPE_U32, 2, |seg, _span| seg
216        .op2(|a: u32, b: u32| a.wrapping_add(b))),
217    sig!(TYPE_U64, 2, |seg, _span| seg
218        .op2(|a: u64, b: u64| a.wrapping_add(b))),
219    sig!(TYPE_U128, 2, |seg, _span| seg
220        .op2(|a: u128, b: u128| a.wrapping_add(b))),
221    sig!(TYPE_USIZE, 2, |seg, _span| seg
222        .op2(|a: usize, b: usize| a.wrapping_add(b))),
223    sig!(TYPE_I8, 2, |seg, span| seg.op2r(move |a: i8, b: i8| a
224        .checked_add(b)
225        .ok_or_else(|| anyhow!("arithmetic overflow"))
226        .map_err(|e| span_err(span, e)))),
227    sig!(TYPE_I16, 2, |seg, span| seg.op2r(move |a: i16, b: i16| a
228        .checked_add(b)
229        .ok_or_else(|| anyhow!("arithmetic overflow"))
230        .map_err(|e| span_err(span, e)))),
231    sig!(TYPE_I32, 2, |seg, span| seg.op2r(move |a: i32, b: i32| a
232        .checked_add(b)
233        .ok_or_else(|| anyhow!("arithmetic overflow"))
234        .map_err(|e| span_err(span, e)))),
235    sig!(TYPE_I64, 2, |seg, span| seg.op2r(move |a: i64, b: i64| a
236        .checked_add(b)
237        .ok_or_else(|| anyhow!("arithmetic overflow"))
238        .map_err(|e| span_err(span, e)))),
239    sig!(TYPE_I128, 2, |seg, span| seg.op2r(
240        move |a: i128, b: i128| a
241            .checked_add(b)
242            .ok_or_else(|| anyhow!("arithmetic overflow"))
243            .map_err(|e| span_err(span, e))
244    )),
245    sig!(TYPE_ISIZE, 2, |seg, span| seg.op2r(
246        move |a: isize, b: isize| a
247            .checked_add(b)
248            .ok_or_else(|| anyhow!("arithmetic overflow"))
249            .map_err(|e| span_err(span, e))
250    )),
251    sig!(TYPE_F32, 2, |seg, _span| seg.op2(|a: f32, b: f32| a + b)),
252    sig!(TYPE_F64, 2, |seg, _span| seg.op2(|a: f64, b: f64| a + b)),
253    sig!(TYPE_STR, 2, |seg, _span| seg
254        .op2(|a: String, b: String| a + &b)),
255];
256
257// Subtraction signatures (both binary and unary)
258static SUB_SIGNATURES: &[OpSignature] = &[
259    // Binary subtraction
260    sig!(TYPE_U8, 2, |seg, _span| seg
261        .op2(|a: u8, b: u8| a.wrapping_sub(b))),
262    sig!(TYPE_U16, 2, |seg, _span| seg
263        .op2(|a: u16, b: u16| a.wrapping_sub(b))),
264    sig!(TYPE_U32, 2, |seg, _span| seg
265        .op2(|a: u32, b: u32| a.wrapping_sub(b))),
266    sig!(TYPE_U64, 2, |seg, _span| seg
267        .op2(|a: u64, b: u64| a.wrapping_sub(b))),
268    sig!(TYPE_U128, 2, |seg, _span| seg
269        .op2(|a: u128, b: u128| a.wrapping_sub(b))),
270    sig!(TYPE_USIZE, 2, |seg, _span| seg
271        .op2(|a: usize, b: usize| a.wrapping_sub(b))),
272    sig!(TYPE_I8, 2, |seg, span| seg.op2r(move |a: i8, b: i8| a
273        .checked_sub(b)
274        .ok_or_else(|| anyhow!("arithmetic overflow"))
275        .map_err(|e| span_err(span, e)))),
276    sig!(TYPE_I16, 2, |seg, span| seg.op2r(move |a: i16, b: i16| a
277        .checked_sub(b)
278        .ok_or_else(|| anyhow!("arithmetic overflow"))
279        .map_err(|e| span_err(span, e)))),
280    sig!(TYPE_I32, 2, |seg, span| seg.op2r(move |a: i32, b: i32| a
281        .checked_sub(b)
282        .ok_or_else(|| anyhow!("arithmetic overflow"))
283        .map_err(|e| span_err(span, e)))),
284    sig!(TYPE_I64, 2, |seg, span| seg.op2r(move |a: i64, b: i64| a
285        .checked_sub(b)
286        .ok_or_else(|| anyhow!("arithmetic overflow"))
287        .map_err(|e| span_err(span, e)))),
288    sig!(TYPE_I128, 2, |seg, span| seg.op2r(
289        move |a: i128, b: i128| a
290            .checked_sub(b)
291            .ok_or_else(|| anyhow!("arithmetic overflow"))
292            .map_err(|e| span_err(span, e))
293    )),
294    sig!(TYPE_ISIZE, 2, |seg, span| seg.op2r(
295        move |a: isize, b: isize| a
296            .checked_sub(b)
297            .ok_or_else(|| anyhow!("arithmetic overflow"))
298            .map_err(|e| span_err(span, e))
299    )),
300    sig!(TYPE_F32, 2, |seg, _span| seg.op2(|a: f32, b: f32| a - b)),
301    sig!(TYPE_F64, 2, |seg, _span| seg.op2(|a: f64, b: f64| a - b)),
302    // Unary negation
303    sig!(TYPE_I8, 1, |seg, span| seg.op1r(move |a: i8| a
304        .checked_neg()
305        .ok_or_else(|| anyhow!("arithmetic overflow"))
306        .map_err(|e| span_err(span, e)))),
307    sig!(TYPE_I16, 1, |seg, span| seg.op1r(move |a: i16| a
308        .checked_neg()
309        .ok_or_else(|| anyhow!("arithmetic overflow"))
310        .map_err(|e| span_err(span, e)))),
311    sig!(TYPE_I32, 1, |seg, span| seg.op1r(move |a: i32| a
312        .checked_neg()
313        .ok_or_else(|| anyhow!("arithmetic overflow"))
314        .map_err(|e| span_err(span, e)))),
315    sig!(TYPE_I64, 1, |seg, span| seg.op1r(move |a: i64| a
316        .checked_neg()
317        .ok_or_else(|| anyhow!("arithmetic overflow"))
318        .map_err(|e| span_err(span, e)))),
319    sig!(TYPE_I128, 1, |seg, span| seg.op1r(move |a: i128| a
320        .checked_neg()
321        .ok_or_else(|| anyhow!("arithmetic overflow"))
322        .map_err(|e| span_err(span, e)))),
323    sig!(TYPE_ISIZE, 1, |seg, span| seg.op1r(move |a: isize| a
324        .checked_neg()
325        .ok_or_else(|| anyhow!("arithmetic overflow"))
326        .map_err(|e| span_err(span, e)))),
327    sig!(TYPE_F32, 1, |seg, _span| seg.op1(|a: f32| -a)),
328    sig!(TYPE_F64, 1, |seg, _span| seg.op1(|a: f64| -a)),
329];
330
331// Multiplication signatures
332static MUL_SIGNATURES: &[OpSignature] = &[
333    sig!(TYPE_U8, 2, |seg, _span| seg
334        .op2(|a: u8, b: u8| a.wrapping_mul(b))),
335    sig!(TYPE_U16, 2, |seg, _span| seg
336        .op2(|a: u16, b: u16| a.wrapping_mul(b))),
337    sig!(TYPE_U32, 2, |seg, _span| seg
338        .op2(|a: u32, b: u32| a.wrapping_mul(b))),
339    sig!(TYPE_U64, 2, |seg, _span| seg
340        .op2(|a: u64, b: u64| a.wrapping_mul(b))),
341    sig!(TYPE_U128, 2, |seg, _span| seg
342        .op2(|a: u128, b: u128| a.wrapping_mul(b))),
343    sig!(TYPE_USIZE, 2, |seg, _span| seg
344        .op2(|a: usize, b: usize| a.wrapping_mul(b))),
345    sig!(TYPE_I8, 2, |seg, span| seg.op2r(move |a: i8, b: i8| a
346        .checked_mul(b)
347        .ok_or_else(|| anyhow!("arithmetic overflow"))
348        .map_err(|e| span_err(span, e)))),
349    sig!(TYPE_I16, 2, |seg, span| seg.op2r(move |a: i16, b: i16| a
350        .checked_mul(b)
351        .ok_or_else(|| anyhow!("arithmetic overflow"))
352        .map_err(|e| span_err(span, e)))),
353    sig!(TYPE_I32, 2, |seg, span| seg.op2r(move |a: i32, b: i32| a
354        .checked_mul(b)
355        .ok_or_else(|| anyhow!("arithmetic overflow"))
356        .map_err(|e| span_err(span, e)))),
357    sig!(TYPE_I64, 2, |seg, span| seg.op2r(move |a: i64, b: i64| a
358        .checked_mul(b)
359        .ok_or_else(|| anyhow!("arithmetic overflow"))
360        .map_err(|e| span_err(span, e)))),
361    sig!(TYPE_I128, 2, |seg, span| seg.op2r(
362        move |a: i128, b: i128| a
363            .checked_mul(b)
364            .ok_or_else(|| anyhow!("arithmetic overflow"))
365            .map_err(|e| span_err(span, e))
366    )),
367    sig!(TYPE_ISIZE, 2, |seg, span| seg.op2r(
368        move |a: isize, b: isize| a
369            .checked_mul(b)
370            .ok_or_else(|| anyhow!("arithmetic overflow"))
371            .map_err(|e| span_err(span, e))
372    )),
373    sig!(TYPE_F32, 2, |seg, _span| seg.op2(|a: f32, b: f32| a * b)),
374    sig!(TYPE_F64, 2, |seg, _span| seg.op2(|a: f64, b: f64| a * b)),
375];
376
377// Division signatures
378//
379// Integer division uses `checked_div` via `op2r` so that division by zero returns an error
380// instead of panicking. Float division keeps `op2` (IEEE 754 defines x/0.0 as inf/nan).
381static DIV_SIGNATURES: &[OpSignature] = &[
382    sig!(TYPE_U8, 2, |seg, span| seg.op2r(move |a: u8, b: u8| a
383        .checked_div(b)
384        .ok_or_else(|| anyhow!("division by zero"))
385        .map_err(|e| span_err(span, e)))),
386    sig!(TYPE_U16, 2, |seg, span| seg.op2r(move |a: u16, b: u16| a
387        .checked_div(b)
388        .ok_or_else(|| anyhow!("division by zero"))
389        .map_err(|e| span_err(span, e)))),
390    sig!(TYPE_U32, 2, |seg, span| seg.op2r(move |a: u32, b: u32| a
391        .checked_div(b)
392        .ok_or_else(|| anyhow!("division by zero"))
393        .map_err(|e| span_err(span, e)))),
394    sig!(TYPE_U64, 2, |seg, span| seg.op2r(move |a: u64, b: u64| a
395        .checked_div(b)
396        .ok_or_else(|| anyhow!("division by zero"))
397        .map_err(|e| span_err(span, e)))),
398    sig!(TYPE_U128, 2, |seg, span| seg.op2r(
399        move |a: u128, b: u128| a
400            .checked_div(b)
401            .ok_or_else(|| anyhow!("division by zero"))
402            .map_err(|e| span_err(span, e))
403    )),
404    sig!(TYPE_USIZE, 2, |seg, span| seg.op2r(
405        move |a: usize, b: usize| a
406            .checked_div(b)
407            .ok_or_else(|| anyhow!("division by zero"))
408            .map_err(|e| span_err(span, e))
409    )),
410    sig!(TYPE_I8, 2, |seg, span| seg.op2r(move |a: i8, b: i8| a
411        .checked_div(b)
412        .ok_or_else(|| anyhow!("division by zero"))
413        .map_err(|e| span_err(span, e)))),
414    sig!(TYPE_I16, 2, |seg, span| seg.op2r(move |a: i16, b: i16| a
415        .checked_div(b)
416        .ok_or_else(|| anyhow!("division by zero"))
417        .map_err(|e| span_err(span, e)))),
418    sig!(TYPE_I32, 2, |seg, span| seg.op2r(move |a: i32, b: i32| a
419        .checked_div(b)
420        .ok_or_else(|| anyhow!("division by zero"))
421        .map_err(|e| span_err(span, e)))),
422    sig!(TYPE_I64, 2, |seg, span| seg.op2r(move |a: i64, b: i64| a
423        .checked_div(b)
424        .ok_or_else(|| anyhow!("division by zero"))
425        .map_err(|e| span_err(span, e)))),
426    sig!(TYPE_I128, 2, |seg, span| seg.op2r(
427        move |a: i128, b: i128| a
428            .checked_div(b)
429            .ok_or_else(|| anyhow!("division by zero"))
430            .map_err(|e| span_err(span, e))
431    )),
432    sig!(TYPE_ISIZE, 2, |seg, span| seg.op2r(
433        move |a: isize, b: isize| a
434            .checked_div(b)
435            .ok_or_else(|| anyhow!("division by zero"))
436            .map_err(|e| span_err(span, e))
437    )),
438    sig!(TYPE_F32, 2, |seg, _span| seg.op2(|a: f32, b: f32| a / b)),
439    sig!(TYPE_F64, 2, |seg, _span| seg.op2(|a: f64, b: f64| a / b)),
440];
441
442// Modulo signatures
443//
444// Integer modulo uses `checked_rem` via `op2r` so that division by zero returns an error
445// instead of panicking. Float modulo keeps `op2` (x % 0.0 yields NaN without panicking).
446static MOD_SIGNATURES: &[OpSignature] = &[
447    sig!(TYPE_U8, 2, |seg, span| seg.op2r(move |a: u8, b: u8| a
448        .checked_rem(b)
449        .ok_or_else(|| anyhow!("division by zero"))
450        .map_err(|e| span_err(span, e)))),
451    sig!(TYPE_U16, 2, |seg, span| seg.op2r(move |a: u16, b: u16| a
452        .checked_rem(b)
453        .ok_or_else(|| anyhow!("division by zero"))
454        .map_err(|e| span_err(span, e)))),
455    sig!(TYPE_U32, 2, |seg, span| seg.op2r(move |a: u32, b: u32| a
456        .checked_rem(b)
457        .ok_or_else(|| anyhow!("division by zero"))
458        .map_err(|e| span_err(span, e)))),
459    sig!(TYPE_U64, 2, |seg, span| seg.op2r(move |a: u64, b: u64| a
460        .checked_rem(b)
461        .ok_or_else(|| anyhow!("division by zero"))
462        .map_err(|e| span_err(span, e)))),
463    sig!(TYPE_U128, 2, |seg, span| seg.op2r(
464        move |a: u128, b: u128| a
465            .checked_rem(b)
466            .ok_or_else(|| anyhow!("division by zero"))
467            .map_err(|e| span_err(span, e))
468    )),
469    sig!(TYPE_USIZE, 2, |seg, span| seg.op2r(
470        move |a: usize, b: usize| a
471            .checked_rem(b)
472            .ok_or_else(|| anyhow!("division by zero"))
473            .map_err(|e| span_err(span, e))
474    )),
475    sig!(TYPE_I8, 2, |seg, span| seg.op2r(move |a: i8, b: i8| a
476        .checked_rem(b)
477        .ok_or_else(|| anyhow!("division by zero"))
478        .map_err(|e| span_err(span, e)))),
479    sig!(TYPE_I16, 2, |seg, span| seg.op2r(move |a: i16, b: i16| a
480        .checked_rem(b)
481        .ok_or_else(|| anyhow!("division by zero"))
482        .map_err(|e| span_err(span, e)))),
483    sig!(TYPE_I32, 2, |seg, span| seg.op2r(move |a: i32, b: i32| a
484        .checked_rem(b)
485        .ok_or_else(|| anyhow!("division by zero"))
486        .map_err(|e| span_err(span, e)))),
487    sig!(TYPE_I64, 2, |seg, span| seg.op2r(move |a: i64, b: i64| a
488        .checked_rem(b)
489        .ok_or_else(|| anyhow!("division by zero"))
490        .map_err(|e| span_err(span, e)))),
491    sig!(TYPE_I128, 2, |seg, span| seg.op2r(
492        move |a: i128, b: i128| a
493            .checked_rem(b)
494            .ok_or_else(|| anyhow!("division by zero"))
495            .map_err(|e| span_err(span, e))
496    )),
497    sig!(TYPE_ISIZE, 2, |seg, span| seg.op2r(
498        move |a: isize, b: isize| a
499            .checked_rem(b)
500            .ok_or_else(|| anyhow!("division by zero"))
501            .map_err(|e| span_err(span, e))
502    )),
503    sig!(TYPE_F32, 2, |seg, _span| seg.op2(|a: f32, b: f32| a % b)),
504    sig!(TYPE_F64, 2, |seg, _span| seg.op2(|a: f64, b: f64| a % b)),
505];
506
507// Bitwise AND signatures
508static BITWISE_AND_SIGNATURES: &[OpSignature] = &[
509    sig!(TYPE_U8, 2, |seg, _span| seg.op2(|a: u8, b: u8| a & b)),
510    sig!(TYPE_U16, 2, |seg, _span| seg.op2(|a: u16, b: u16| a & b)),
511    sig!(TYPE_U32, 2, |seg, _span| seg.op2(|a: u32, b: u32| a & b)),
512    sig!(TYPE_U64, 2, |seg, _span| seg.op2(|a: u64, b: u64| a & b)),
513    sig!(TYPE_U128, 2, |seg, _span| seg.op2(|a: u128, b: u128| a & b)),
514    sig!(TYPE_USIZE, 2, |seg, _span| seg
515        .op2(|a: usize, b: usize| a & b)),
516    sig!(TYPE_I8, 2, |seg, _span| seg.op2(|a: i8, b: i8| a & b)),
517    sig!(TYPE_I16, 2, |seg, _span| seg.op2(|a: i16, b: i16| a & b)),
518    sig!(TYPE_I32, 2, |seg, _span| seg.op2(|a: i32, b: i32| a & b)),
519    sig!(TYPE_I64, 2, |seg, _span| seg.op2(|a: i64, b: i64| a & b)),
520    sig!(TYPE_I128, 2, |seg, _span| seg.op2(|a: i128, b: i128| a & b)),
521    sig!(TYPE_ISIZE, 2, |seg, _span| seg
522        .op2(|a: isize, b: isize| a & b)),
523];
524
525// Bitwise OR signatures
526static BITWISE_OR_SIGNATURES: &[OpSignature] = &[
527    sig!(TYPE_U8, 2, |seg, _span| seg.op2(|a: u8, b: u8| a | b)),
528    sig!(TYPE_U16, 2, |seg, _span| seg.op2(|a: u16, b: u16| a | b)),
529    sig!(TYPE_U32, 2, |seg, _span| seg.op2(|a: u32, b: u32| a | b)),
530    sig!(TYPE_U64, 2, |seg, _span| seg.op2(|a: u64, b: u64| a | b)),
531    sig!(TYPE_U128, 2, |seg, _span| seg.op2(|a: u128, b: u128| a | b)),
532    sig!(TYPE_USIZE, 2, |seg, _span| seg
533        .op2(|a: usize, b: usize| a | b)),
534    sig!(TYPE_I8, 2, |seg, _span| seg.op2(|a: i8, b: i8| a | b)),
535    sig!(TYPE_I16, 2, |seg, _span| seg.op2(|a: i16, b: i16| a | b)),
536    sig!(TYPE_I32, 2, |seg, _span| seg.op2(|a: i32, b: i32| a | b)),
537    sig!(TYPE_I64, 2, |seg, _span| seg.op2(|a: i64, b: i64| a | b)),
538    sig!(TYPE_I128, 2, |seg, _span| seg.op2(|a: i128, b: i128| a | b)),
539    sig!(TYPE_ISIZE, 2, |seg, _span| seg
540        .op2(|a: isize, b: isize| a | b)),
541];
542
543// Bitwise XOR signatures
544static BITWISE_XOR_SIGNATURES: &[OpSignature] = &[
545    sig!(TYPE_U8, 2, |seg, _span| seg.op2(|a: u8, b: u8| a ^ b)),
546    sig!(TYPE_U16, 2, |seg, _span| seg.op2(|a: u16, b: u16| a ^ b)),
547    sig!(TYPE_U32, 2, |seg, _span| seg.op2(|a: u32, b: u32| a ^ b)),
548    sig!(TYPE_U64, 2, |seg, _span| seg.op2(|a: u64, b: u64| a ^ b)),
549    sig!(TYPE_U128, 2, |seg, _span| seg.op2(|a: u128, b: u128| a ^ b)),
550    sig!(TYPE_USIZE, 2, |seg, _span| seg
551        .op2(|a: usize, b: usize| a ^ b)),
552    sig!(TYPE_I8, 2, |seg, _span| seg.op2(|a: i8, b: i8| a ^ b)),
553    sig!(TYPE_I16, 2, |seg, _span| seg.op2(|a: i16, b: i16| a ^ b)),
554    sig!(TYPE_I32, 2, |seg, _span| seg.op2(|a: i32, b: i32| a ^ b)),
555    sig!(TYPE_I64, 2, |seg, _span| seg.op2(|a: i64, b: i64| a ^ b)),
556    sig!(TYPE_I128, 2, |seg, _span| seg.op2(|a: i128, b: i128| a ^ b)),
557    sig!(TYPE_ISIZE, 2, |seg, _span| seg
558        .op2(|a: isize, b: isize| a ^ b)),
559];
560
561// Macros that push shift signatures onto a Vec as statements.
562// Rust macros may not expand to multiple comma-separated expressions in a static
563// array initialiser, so we use Lazy<Vec<_>> with push statements instead.
564//
565// RHS → u32 conversion (required by checked_shl / checked_shr):
566//   u8, u16              : u32::from  (infallible widening)
567//   u32                  : identity
568//   u64 / u128 / usize   : u32::try_from; fails if value > u32::MAX
569//   all signed types     : u32::try_from; fails if value < 0 or > u32::MAX
570//   In all failure cases the error is "shift overflow", matching Rust's
571//   debug-mode panic for shift-with-overflow.
572macro_rules! shl_push {
573    ($v:ident, $lhs_idx:expr, $lhs_ty:ty) => {
574        $v.push(sig_het!($lhs_idx, TYPE_U8, |seg, span| seg.op2r(
575            move |a: $lhs_ty, b: u8| a
576                .checked_shl(u32::from(b))
577                .ok_or_else(|| anyhow!("shift overflow"))
578                .map_err(|e| span_err(span, e))
579        )));
580        $v.push(sig_het!($lhs_idx, TYPE_U16, |seg, span| seg.op2r(
581            move |a: $lhs_ty, b: u16| a
582                .checked_shl(u32::from(b))
583                .ok_or_else(|| anyhow!("shift overflow"))
584                .map_err(|e| span_err(span, e))
585        )));
586        $v.push(sig_het!($lhs_idx, TYPE_U32, |seg, span| seg.op2r(
587            move |a: $lhs_ty, b: u32| a
588                .checked_shl(b)
589                .ok_or_else(|| anyhow!("shift overflow"))
590                .map_err(|e| span_err(span, e))
591        )));
592        $v.push(sig_het!($lhs_idx, TYPE_U64, |seg, span| seg.op2r(
593            move |a: $lhs_ty, b: u64| u32::try_from(b)
594                .ok()
595                .and_then(|r| a.checked_shl(r))
596                .ok_or_else(|| anyhow!("shift overflow"))
597                .map_err(|e| span_err(span, e))
598        )));
599        $v.push(sig_het!($lhs_idx, TYPE_U128, |seg, span| seg.op2r(
600            move |a: $lhs_ty, b: u128| u32::try_from(b)
601                .ok()
602                .and_then(|r| a.checked_shl(r))
603                .ok_or_else(|| anyhow!("shift overflow"))
604                .map_err(|e| span_err(span, e))
605        )));
606        $v.push(sig_het!($lhs_idx, TYPE_USIZE, |seg, span| seg.op2r(
607            move |a: $lhs_ty, b: usize| u32::try_from(b)
608                .ok()
609                .and_then(|r| a.checked_shl(r))
610                .ok_or_else(|| anyhow!("shift overflow"))
611                .map_err(|e| span_err(span, e))
612        )));
613        $v.push(sig_het!($lhs_idx, TYPE_I8, |seg, span| seg.op2r(
614            move |a: $lhs_ty, b: i8| u32::try_from(b)
615                .ok()
616                .and_then(|r| a.checked_shl(r))
617                .ok_or_else(|| anyhow!("shift overflow"))
618                .map_err(|e| span_err(span, e))
619        )));
620        $v.push(sig_het!($lhs_idx, TYPE_I16, |seg, span| seg.op2r(
621            move |a: $lhs_ty, b: i16| u32::try_from(b)
622                .ok()
623                .and_then(|r| a.checked_shl(r))
624                .ok_or_else(|| anyhow!("shift overflow"))
625                .map_err(|e| span_err(span, e))
626        )));
627        $v.push(sig_het!($lhs_idx, TYPE_I32, |seg, span| seg.op2r(
628            move |a: $lhs_ty, b: i32| u32::try_from(b)
629                .ok()
630                .and_then(|r| a.checked_shl(r))
631                .ok_or_else(|| anyhow!("shift overflow"))
632                .map_err(|e| span_err(span, e))
633        )));
634        $v.push(sig_het!($lhs_idx, TYPE_I64, |seg, span| seg.op2r(
635            move |a: $lhs_ty, b: i64| u32::try_from(b)
636                .ok()
637                .and_then(|r| a.checked_shl(r))
638                .ok_or_else(|| anyhow!("shift overflow"))
639                .map_err(|e| span_err(span, e))
640        )));
641        $v.push(sig_het!($lhs_idx, TYPE_I128, |seg, span| seg.op2r(
642            move |a: $lhs_ty, b: i128| u32::try_from(b)
643                .ok()
644                .and_then(|r| a.checked_shl(r))
645                .ok_or_else(|| anyhow!("shift overflow"))
646                .map_err(|e| span_err(span, e))
647        )));
648        $v.push(sig_het!($lhs_idx, TYPE_ISIZE, |seg, span| seg.op2r(
649            move |a: $lhs_ty, b: isize| u32::try_from(b)
650                .ok()
651                .and_then(|r| a.checked_shl(r))
652                .ok_or_else(|| anyhow!("shift overflow"))
653                .map_err(|e| span_err(span, e))
654        )));
655    };
656}
657
658macro_rules! shr_push {
659    ($v:ident, $lhs_idx:expr, $lhs_ty:ty) => {
660        $v.push(sig_het!($lhs_idx, TYPE_U8, |seg, span| seg.op2r(
661            move |a: $lhs_ty, b: u8| a
662                .checked_shr(u32::from(b))
663                .ok_or_else(|| anyhow!("shift overflow"))
664                .map_err(|e| span_err(span, e))
665        )));
666        $v.push(sig_het!($lhs_idx, TYPE_U16, |seg, span| seg.op2r(
667            move |a: $lhs_ty, b: u16| a
668                .checked_shr(u32::from(b))
669                .ok_or_else(|| anyhow!("shift overflow"))
670                .map_err(|e| span_err(span, e))
671        )));
672        $v.push(sig_het!($lhs_idx, TYPE_U32, |seg, span| seg.op2r(
673            move |a: $lhs_ty, b: u32| a
674                .checked_shr(b)
675                .ok_or_else(|| anyhow!("shift overflow"))
676                .map_err(|e| span_err(span, e))
677        )));
678        $v.push(sig_het!($lhs_idx, TYPE_U64, |seg, span| seg.op2r(
679            move |a: $lhs_ty, b: u64| u32::try_from(b)
680                .ok()
681                .and_then(|r| a.checked_shr(r))
682                .ok_or_else(|| anyhow!("shift overflow"))
683                .map_err(|e| span_err(span, e))
684        )));
685        $v.push(sig_het!($lhs_idx, TYPE_U128, |seg, span| seg.op2r(
686            move |a: $lhs_ty, b: u128| u32::try_from(b)
687                .ok()
688                .and_then(|r| a.checked_shr(r))
689                .ok_or_else(|| anyhow!("shift overflow"))
690                .map_err(|e| span_err(span, e))
691        )));
692        $v.push(sig_het!($lhs_idx, TYPE_USIZE, |seg, span| seg.op2r(
693            move |a: $lhs_ty, b: usize| u32::try_from(b)
694                .ok()
695                .and_then(|r| a.checked_shr(r))
696                .ok_or_else(|| anyhow!("shift overflow"))
697                .map_err(|e| span_err(span, e))
698        )));
699        $v.push(sig_het!($lhs_idx, TYPE_I8, |seg, span| seg.op2r(
700            move |a: $lhs_ty, b: i8| u32::try_from(b)
701                .ok()
702                .and_then(|r| a.checked_shr(r))
703                .ok_or_else(|| anyhow!("shift overflow"))
704                .map_err(|e| span_err(span, e))
705        )));
706        $v.push(sig_het!($lhs_idx, TYPE_I16, |seg, span| seg.op2r(
707            move |a: $lhs_ty, b: i16| u32::try_from(b)
708                .ok()
709                .and_then(|r| a.checked_shr(r))
710                .ok_or_else(|| anyhow!("shift overflow"))
711                .map_err(|e| span_err(span, e))
712        )));
713        $v.push(sig_het!($lhs_idx, TYPE_I32, |seg, span| seg.op2r(
714            move |a: $lhs_ty, b: i32| u32::try_from(b)
715                .ok()
716                .and_then(|r| a.checked_shr(r))
717                .ok_or_else(|| anyhow!("shift overflow"))
718                .map_err(|e| span_err(span, e))
719        )));
720        $v.push(sig_het!($lhs_idx, TYPE_I64, |seg, span| seg.op2r(
721            move |a: $lhs_ty, b: i64| u32::try_from(b)
722                .ok()
723                .and_then(|r| a.checked_shr(r))
724                .ok_or_else(|| anyhow!("shift overflow"))
725                .map_err(|e| span_err(span, e))
726        )));
727        $v.push(sig_het!($lhs_idx, TYPE_I128, |seg, span| seg.op2r(
728            move |a: $lhs_ty, b: i128| u32::try_from(b)
729                .ok()
730                .and_then(|r| a.checked_shr(r))
731                .ok_or_else(|| anyhow!("shift overflow"))
732                .map_err(|e| span_err(span, e))
733        )));
734        $v.push(sig_het!($lhs_idx, TYPE_ISIZE, |seg, span| seg.op2r(
735            move |a: $lhs_ty, b: isize| u32::try_from(b)
736                .ok()
737                .and_then(|r| a.checked_shr(r))
738                .ok_or_else(|| anyhow!("shift overflow"))
739                .map_err(|e| span_err(span, e))
740        )));
741    };
742}
743
744// Left shift: all 144 combinations T << U for integer T and U (mirrors Rust's Shl implementations).
745// Stored as Lazy<Vec<_>> because the shl_push! macro expands to statements, not array items.
746static LEFT_SHIFT_SIGNATURES: Lazy<Vec<OpSignature>> = Lazy::new(|| {
747    let mut v = Vec::with_capacity(144);
748    shl_push!(v, TYPE_U8, u8);
749    shl_push!(v, TYPE_U16, u16);
750    shl_push!(v, TYPE_U32, u32);
751    shl_push!(v, TYPE_U64, u64);
752    shl_push!(v, TYPE_U128, u128);
753    shl_push!(v, TYPE_USIZE, usize);
754    shl_push!(v, TYPE_I8, i8);
755    shl_push!(v, TYPE_I16, i16);
756    shl_push!(v, TYPE_I32, i32);
757    shl_push!(v, TYPE_I64, i64);
758    shl_push!(v, TYPE_I128, i128);
759    shl_push!(v, TYPE_ISIZE, isize);
760    v
761});
762
763// Right shift: all 144 combinations T >> U for integer T and U (mirrors Rust's Shr implementations).
764static RIGHT_SHIFT_SIGNATURES: Lazy<Vec<OpSignature>> = Lazy::new(|| {
765    let mut v = Vec::with_capacity(144);
766    shr_push!(v, TYPE_U8, u8);
767    shr_push!(v, TYPE_U16, u16);
768    shr_push!(v, TYPE_U32, u32);
769    shr_push!(v, TYPE_U64, u64);
770    shr_push!(v, TYPE_U128, u128);
771    shr_push!(v, TYPE_USIZE, usize);
772    shr_push!(v, TYPE_I8, i8);
773    shr_push!(v, TYPE_I16, i16);
774    shr_push!(v, TYPE_I32, i32);
775    shr_push!(v, TYPE_I64, i64);
776    shr_push!(v, TYPE_I128, i128);
777    shr_push!(v, TYPE_ISIZE, isize);
778    v
779});
780
781// Logical NOT signatures
782static LOGICAL_NOT_SIGNATURES: &[OpSignature] =
783    &[sig!(TYPE_BOOL, 1, |seg, _span| seg.op1(|a: bool| !a))];
784
785// Equality signatures
786static EQUAL_SIGNATURES: &[OpSignature] = &[
787    sig!(TYPE_U8, 2, |seg, _span| seg.op2(|a: u8, b: u8| a == b)),
788    sig!(TYPE_U16, 2, |seg, _span| seg.op2(|a: u16, b: u16| a == b)),
789    sig!(TYPE_U32, 2, |seg, _span| seg.op2(|a: u32, b: u32| a == b)),
790    sig!(TYPE_U64, 2, |seg, _span| seg.op2(|a: u64, b: u64| a == b)),
791    sig!(TYPE_U128, 2, |seg, _span| seg
792        .op2(|a: u128, b: u128| a == b)),
793    sig!(TYPE_USIZE, 2, |seg, _span| seg
794        .op2(|a: usize, b: usize| a == b)),
795    sig!(TYPE_I8, 2, |seg, _span| seg.op2(|a: i8, b: i8| a == b)),
796    sig!(TYPE_I16, 2, |seg, _span| seg.op2(|a: i16, b: i16| a == b)),
797    sig!(TYPE_I32, 2, |seg, _span| seg.op2(|a: i32, b: i32| a == b)),
798    sig!(TYPE_I64, 2, |seg, _span| seg.op2(|a: i64, b: i64| a == b)),
799    sig!(TYPE_I128, 2, |seg, _span| seg
800        .op2(|a: i128, b: i128| a == b)),
801    sig!(TYPE_ISIZE, 2, |seg, _span| seg
802        .op2(|a: isize, b: isize| a == b)),
803    sig!(TYPE_F32, 2, |seg, _span| seg.op2(|a: f32, b: f32| a == b)),
804    sig!(TYPE_F64, 2, |seg, _span| seg.op2(|a: f64, b: f64| a == b)),
805    sig!(TYPE_BOOL, 2, |seg, _span| seg
806        .op2(|a: bool, b: bool| a == b)),
807    sig!(TYPE_STR, 2, |seg, _span| seg
808        .op2(|a: String, b: String| a == b)),
809];
810
811// Inequality signatures
812static NOT_EQUAL_SIGNATURES: &[OpSignature] = &[
813    sig!(TYPE_U8, 2, |seg, _span| seg.op2(|a: u8, b: u8| a != b)),
814    sig!(TYPE_U16, 2, |seg, _span| seg.op2(|a: u16, b: u16| a != b)),
815    sig!(TYPE_U32, 2, |seg, _span| seg.op2(|a: u32, b: u32| a != b)),
816    sig!(TYPE_U64, 2, |seg, _span| seg.op2(|a: u64, b: u64| a != b)),
817    sig!(TYPE_U128, 2, |seg, _span| seg
818        .op2(|a: u128, b: u128| a != b)),
819    sig!(TYPE_USIZE, 2, |seg, _span| seg
820        .op2(|a: usize, b: usize| a != b)),
821    sig!(TYPE_I8, 2, |seg, _span| seg.op2(|a: i8, b: i8| a != b)),
822    sig!(TYPE_I16, 2, |seg, _span| seg.op2(|a: i16, b: i16| a != b)),
823    sig!(TYPE_I32, 2, |seg, _span| seg.op2(|a: i32, b: i32| a != b)),
824    sig!(TYPE_I64, 2, |seg, _span| seg.op2(|a: i64, b: i64| a != b)),
825    sig!(TYPE_I128, 2, |seg, _span| seg
826        .op2(|a: i128, b: i128| a != b)),
827    sig!(TYPE_ISIZE, 2, |seg, _span| seg
828        .op2(|a: isize, b: isize| a != b)),
829    sig!(TYPE_F32, 2, |seg, _span| seg.op2(|a: f32, b: f32| a != b)),
830    sig!(TYPE_F64, 2, |seg, _span| seg.op2(|a: f64, b: f64| a != b)),
831    sig!(TYPE_BOOL, 2, |seg, _span| seg
832        .op2(|a: bool, b: bool| a != b)),
833    sig!(TYPE_STR, 2, |seg, _span| seg
834        .op2(|a: String, b: String| a != b)),
835];
836
837// Less than signatures
838static LESS_THAN_SIGNATURES: &[OpSignature] = &[
839    sig!(TYPE_U8, 2, |seg, _span| seg.op2(|a: u8, b: u8| a < b)),
840    sig!(TYPE_U16, 2, |seg, _span| seg.op2(|a: u16, b: u16| a < b)),
841    sig!(TYPE_U32, 2, |seg, _span| seg.op2(|a: u32, b: u32| a < b)),
842    sig!(TYPE_U64, 2, |seg, _span| seg.op2(|a: u64, b: u64| a < b)),
843    sig!(TYPE_U128, 2, |seg, _span| seg.op2(|a: u128, b: u128| a < b)),
844    sig!(TYPE_USIZE, 2, |seg, _span| seg
845        .op2(|a: usize, b: usize| a < b)),
846    sig!(TYPE_I8, 2, |seg, _span| seg.op2(|a: i8, b: i8| a < b)),
847    sig!(TYPE_I16, 2, |seg, _span| seg.op2(|a: i16, b: i16| a < b)),
848    sig!(TYPE_I32, 2, |seg, _span| seg.op2(|a: i32, b: i32| a < b)),
849    sig!(TYPE_I64, 2, |seg, _span| seg.op2(|a: i64, b: i64| a < b)),
850    sig!(TYPE_I128, 2, |seg, _span| seg.op2(|a: i128, b: i128| a < b)),
851    sig!(TYPE_ISIZE, 2, |seg, _span| seg
852        .op2(|a: isize, b: isize| a < b)),
853    sig!(TYPE_F32, 2, |seg, _span| seg.op2(|a: f32, b: f32| a < b)),
854    sig!(TYPE_F64, 2, |seg, _span| seg.op2(|a: f64, b: f64| a < b)),
855    sig!(TYPE_STR, 2, |seg, _span| seg
856        .op2(|a: String, b: String| a < b)),
857];
858
859// Less than or equal signatures
860static LESS_THAN_OR_EQUAL_SIGNATURES: &[OpSignature] = &[
861    sig!(TYPE_U8, 2, |seg, _span| seg.op2(|a: u8, b: u8| a <= b)),
862    sig!(TYPE_U16, 2, |seg, _span| seg.op2(|a: u16, b: u16| a <= b)),
863    sig!(TYPE_U32, 2, |seg, _span| seg.op2(|a: u32, b: u32| a <= b)),
864    sig!(TYPE_U64, 2, |seg, _span| seg.op2(|a: u64, b: u64| a <= b)),
865    sig!(TYPE_U128, 2, |seg, _span| seg
866        .op2(|a: u128, b: u128| a <= b)),
867    sig!(TYPE_USIZE, 2, |seg, _span| seg
868        .op2(|a: usize, b: usize| a <= b)),
869    sig!(TYPE_I8, 2, |seg, _span| seg.op2(|a: i8, b: i8| a <= b)),
870    sig!(TYPE_I16, 2, |seg, _span| seg.op2(|a: i16, b: i16| a <= b)),
871    sig!(TYPE_I32, 2, |seg, _span| seg.op2(|a: i32, b: i32| a <= b)),
872    sig!(TYPE_I64, 2, |seg, _span| seg.op2(|a: i64, b: i64| a <= b)),
873    sig!(TYPE_I128, 2, |seg, _span| seg
874        .op2(|a: i128, b: i128| a <= b)),
875    sig!(TYPE_ISIZE, 2, |seg, _span| seg
876        .op2(|a: isize, b: isize| a <= b)),
877    sig!(TYPE_F32, 2, |seg, _span| seg.op2(|a: f32, b: f32| a <= b)),
878    sig!(TYPE_F64, 2, |seg, _span| seg.op2(|a: f64, b: f64| a <= b)),
879    sig!(TYPE_STR, 2, |seg, _span| seg
880        .op2(|a: String, b: String| a <= b)),
881];
882
883// Greater than signatures
884static GREATER_THAN_SIGNATURES: &[OpSignature] = &[
885    sig!(TYPE_U8, 2, |seg, _span| seg.op2(|a: u8, b: u8| a > b)),
886    sig!(TYPE_U16, 2, |seg, _span| seg.op2(|a: u16, b: u16| a > b)),
887    sig!(TYPE_U32, 2, |seg, _span| seg.op2(|a: u32, b: u32| a > b)),
888    sig!(TYPE_U64, 2, |seg, _span| seg.op2(|a: u64, b: u64| a > b)),
889    sig!(TYPE_U128, 2, |seg, _span| seg.op2(|a: u128, b: u128| a > b)),
890    sig!(TYPE_USIZE, 2, |seg, _span| seg
891        .op2(|a: usize, b: usize| a > b)),
892    sig!(TYPE_I8, 2, |seg, _span| seg.op2(|a: i8, b: i8| a > b)),
893    sig!(TYPE_I16, 2, |seg, _span| seg.op2(|a: i16, b: i16| a > b)),
894    sig!(TYPE_I32, 2, |seg, _span| seg.op2(|a: i32, b: i32| a > b)),
895    sig!(TYPE_I64, 2, |seg, _span| seg.op2(|a: i64, b: i64| a > b)),
896    sig!(TYPE_I128, 2, |seg, _span| seg.op2(|a: i128, b: i128| a > b)),
897    sig!(TYPE_ISIZE, 2, |seg, _span| seg
898        .op2(|a: isize, b: isize| a > b)),
899    sig!(TYPE_F32, 2, |seg, _span| seg.op2(|a: f32, b: f32| a > b)),
900    sig!(TYPE_F64, 2, |seg, _span| seg.op2(|a: f64, b: f64| a > b)),
901    sig!(TYPE_STR, 2, |seg, _span| seg
902        .op2(|a: String, b: String| a > b)),
903];
904
905// Greater than or equal signatures
906static GREATER_THAN_OR_EQUAL_SIGNATURES: &[OpSignature] = &[
907    sig!(TYPE_U8, 2, |seg, _span| seg.op2(|a: u8, b: u8| a >= b)),
908    sig!(TYPE_U16, 2, |seg, _span| seg.op2(|a: u16, b: u16| a >= b)),
909    sig!(TYPE_U32, 2, |seg, _span| seg.op2(|a: u32, b: u32| a >= b)),
910    sig!(TYPE_U64, 2, |seg, _span| seg.op2(|a: u64, b: u64| a >= b)),
911    sig!(TYPE_U128, 2, |seg, _span| seg
912        .op2(|a: u128, b: u128| a >= b)),
913    sig!(TYPE_USIZE, 2, |seg, _span| seg
914        .op2(|a: usize, b: usize| a >= b)),
915    sig!(TYPE_I8, 2, |seg, _span| seg.op2(|a: i8, b: i8| a >= b)),
916    sig!(TYPE_I16, 2, |seg, _span| seg.op2(|a: i16, b: i16| a >= b)),
917    sig!(TYPE_I32, 2, |seg, _span| seg.op2(|a: i32, b: i32| a >= b)),
918    sig!(TYPE_I64, 2, |seg, _span| seg.op2(|a: i64, b: i64| a >= b)),
919    sig!(TYPE_I128, 2, |seg, _span| seg
920        .op2(|a: i128, b: i128| a >= b)),
921    sig!(TYPE_ISIZE, 2, |seg, _span| seg
922        .op2(|a: isize, b: isize| a >= b)),
923    sig!(TYPE_F32, 2, |seg, _span| seg.op2(|a: f32, b: f32| a >= b)),
924    sig!(TYPE_F64, 2, |seg, _span| seg.op2(|a: f64, b: f64| a >= b)),
925    sig!(TYPE_STR, 2, |seg, _span| seg
926        .op2(|a: String, b: String| a >= b)),
927];
928
929// Range construction (`a..b`, `a..=b`) — no operations beyond construction; see the
930// design spec's "Compatibility Notes" for why the recognized structural form is a real
931// `RangeInclusive<T>` type rather than a name-matched builtin.
932static RANGE_SIGNATURES: &[OpSignature] = &[
933    sig!(TYPE_U8, 2, |seg, _span| seg.op2(|a: u8, b: u8| a..b)),
934    sig!(TYPE_U16, 2, |seg, _span| seg.op2(|a: u16, b: u16| a..b)),
935    sig!(TYPE_U32, 2, |seg, _span| seg.op2(|a: u32, b: u32| a..b)),
936    sig!(TYPE_U64, 2, |seg, _span| seg.op2(|a: u64, b: u64| a..b)),
937    sig!(TYPE_U128, 2, |seg, _span| seg.op2(|a: u128, b: u128| a..b)),
938    sig!(TYPE_USIZE, 2, |seg, _span| seg
939        .op2(|a: usize, b: usize| a..b)),
940    sig!(TYPE_I8, 2, |seg, _span| seg.op2(|a: i8, b: i8| a..b)),
941    sig!(TYPE_I16, 2, |seg, _span| seg.op2(|a: i16, b: i16| a..b)),
942    sig!(TYPE_I32, 2, |seg, _span| seg.op2(|a: i32, b: i32| a..b)),
943    sig!(TYPE_I64, 2, |seg, _span| seg.op2(|a: i64, b: i64| a..b)),
944    sig!(TYPE_I128, 2, |seg, _span| seg.op2(|a: i128, b: i128| a..b)),
945    sig!(TYPE_ISIZE, 2, |seg, _span| seg
946        .op2(|a: isize, b: isize| a..b)),
947    sig!(TYPE_F32, 2, |seg, _span| seg.op2(|a: f32, b: f32| a..b)),
948    sig!(TYPE_F64, 2, |seg, _span| seg.op2(|a: f64, b: f64| a..b)),
949];
950
951static RANGE_INCLUSIVE_SIGNATURES: &[OpSignature] = &[
952    sig!(TYPE_U8, 2, |seg, _span| seg.op2(|a: u8, b: u8| a..=b)),
953    sig!(TYPE_U16, 2, |seg, _span| seg.op2(|a: u16, b: u16| a..=b)),
954    sig!(TYPE_U32, 2, |seg, _span| seg.op2(|a: u32, b: u32| a..=b)),
955    sig!(TYPE_U64, 2, |seg, _span| seg.op2(|a: u64, b: u64| a..=b)),
956    sig!(TYPE_U128, 2, |seg, _span| seg.op2(|a: u128, b: u128| a..=b)),
957    sig!(TYPE_USIZE, 2, |seg, _span| seg
958        .op2(|a: usize, b: usize| a..=b)),
959    sig!(TYPE_I8, 2, |seg, _span| seg.op2(|a: i8, b: i8| a..=b)),
960    sig!(TYPE_I16, 2, |seg, _span| seg.op2(|a: i16, b: i16| a..=b)),
961    sig!(TYPE_I32, 2, |seg, _span| seg.op2(|a: i32, b: i32| a..=b)),
962    sig!(TYPE_I64, 2, |seg, _span| seg.op2(|a: i64, b: i64| a..=b)),
963    sig!(TYPE_I128, 2, |seg, _span| seg.op2(|a: i128, b: i128| a..=b)),
964    sig!(TYPE_ISIZE, 2, |seg, _span| seg
965        .op2(|a: isize, b: isize| a..=b)),
966    sig!(TYPE_F32, 2, |seg, _span| seg.op2(|a: f32, b: f32| a..=b)),
967    sig!(TYPE_F64, 2, |seg, _span| seg.op2(|a: f64, b: f64| a..=b)),
968];
969
970static RANGE_FROM_SIGNATURES: &[OpSignature] = &[
971    sig!(TYPE_U8, 1, |seg, _span| seg.op1(|a: u8| a..)),
972    sig!(TYPE_U16, 1, |seg, _span| seg.op1(|a: u16| a..)),
973    sig!(TYPE_U32, 1, |seg, _span| seg.op1(|a: u32| a..)),
974    sig!(TYPE_U64, 1, |seg, _span| seg.op1(|a: u64| a..)),
975    sig!(TYPE_U128, 1, |seg, _span| seg.op1(|a: u128| a..)),
976    sig!(TYPE_USIZE, 1, |seg, _span| seg.op1(|a: usize| a..)),
977    sig!(TYPE_I8, 1, |seg, _span| seg.op1(|a: i8| a..)),
978    sig!(TYPE_I16, 1, |seg, _span| seg.op1(|a: i16| a..)),
979    sig!(TYPE_I32, 1, |seg, _span| seg.op1(|a: i32| a..)),
980    sig!(TYPE_I64, 1, |seg, _span| seg.op1(|a: i64| a..)),
981    sig!(TYPE_I128, 1, |seg, _span| seg.op1(|a: i128| a..)),
982    sig!(TYPE_ISIZE, 1, |seg, _span| seg.op1(|a: isize| a..)),
983    sig!(TYPE_F32, 1, |seg, _span| seg.op1(|a: f32| a..)),
984    sig!(TYPE_F64, 1, |seg, _span| seg.op1(|a: f64| a..)),
985];
986
987static RANGE_TO_SIGNATURES: &[OpSignature] = &[
988    sig!(TYPE_U8, 1, |seg, _span| seg.op1(|a: u8| ..a)),
989    sig!(TYPE_U16, 1, |seg, _span| seg.op1(|a: u16| ..a)),
990    sig!(TYPE_U32, 1, |seg, _span| seg.op1(|a: u32| ..a)),
991    sig!(TYPE_U64, 1, |seg, _span| seg.op1(|a: u64| ..a)),
992    sig!(TYPE_U128, 1, |seg, _span| seg.op1(|a: u128| ..a)),
993    sig!(TYPE_USIZE, 1, |seg, _span| seg.op1(|a: usize| ..a)),
994    sig!(TYPE_I8, 1, |seg, _span| seg.op1(|a: i8| ..a)),
995    sig!(TYPE_I16, 1, |seg, _span| seg.op1(|a: i16| ..a)),
996    sig!(TYPE_I32, 1, |seg, _span| seg.op1(|a: i32| ..a)),
997    sig!(TYPE_I64, 1, |seg, _span| seg.op1(|a: i64| ..a)),
998    sig!(TYPE_I128, 1, |seg, _span| seg.op1(|a: i128| ..a)),
999    sig!(TYPE_ISIZE, 1, |seg, _span| seg.op1(|a: isize| ..a)),
1000    sig!(TYPE_F32, 1, |seg, _span| seg.op1(|a: f32| ..a)),
1001    sig!(TYPE_F64, 1, |seg, _span| seg.op1(|a: f64| ..a)),
1002];
1003
1004static RANGE_TO_INCLUSIVE_SIGNATURES: &[OpSignature] = &[
1005    sig!(TYPE_U8, 1, |seg, _span| seg.op1(|a: u8| ..=a)),
1006    sig!(TYPE_U16, 1, |seg, _span| seg.op1(|a: u16| ..=a)),
1007    sig!(TYPE_U32, 1, |seg, _span| seg.op1(|a: u32| ..=a)),
1008    sig!(TYPE_U64, 1, |seg, _span| seg.op1(|a: u64| ..=a)),
1009    sig!(TYPE_U128, 1, |seg, _span| seg.op1(|a: u128| ..=a)),
1010    sig!(TYPE_USIZE, 1, |seg, _span| seg.op1(|a: usize| ..=a)),
1011    sig!(TYPE_I8, 1, |seg, _span| seg.op1(|a: i8| ..=a)),
1012    sig!(TYPE_I16, 1, |seg, _span| seg.op1(|a: i16| ..=a)),
1013    sig!(TYPE_I32, 1, |seg, _span| seg.op1(|a: i32| ..=a)),
1014    sig!(TYPE_I64, 1, |seg, _span| seg.op1(|a: i64| ..=a)),
1015    sig!(TYPE_I128, 1, |seg, _span| seg.op1(|a: i128| ..=a)),
1016    sig!(TYPE_ISIZE, 1, |seg, _span| seg.op1(|a: isize| ..=a)),
1017    sig!(TYPE_F32, 1, |seg, _span| seg.op1(|a: f32| ..=a)),
1018    sig!(TYPE_F64, 1, |seg, _span| seg.op1(|a: f64| ..=a)),
1019];
1020
1021/// Compile-time perfect hash map for built-in operations.
1022///
1023/// Maps operator symbols to their signature arrays for O(1) lookup.
1024static BUILTINS: phf::Map<&'static str, &'static [OpSignature]> = phf_map! {
1025    "+" => ADD_SIGNATURES,
1026    "-" => SUB_SIGNATURES,
1027    "*" => MUL_SIGNATURES,
1028    "/" => DIV_SIGNATURES,
1029    "%" => MOD_SIGNATURES,
1030    "&" => BITWISE_AND_SIGNATURES,
1031    "|" => BITWISE_OR_SIGNATURES,
1032    "^" => BITWISE_XOR_SIGNATURES,
1033    "!" => LOGICAL_NOT_SIGNATURES,
1034    "==" => EQUAL_SIGNATURES,
1035    "!=" => NOT_EQUAL_SIGNATURES,
1036    "<" => LESS_THAN_SIGNATURES,
1037    "<=" => LESS_THAN_OR_EQUAL_SIGNATURES,
1038    ">" => GREATER_THAN_SIGNATURES,
1039    ">=" => GREATER_THAN_OR_EQUAL_SIGNATURES,
1040    "range" => RANGE_SIGNATURES,
1041    "range_inclusive" => RANGE_INCLUSIVE_SIGNATURES,
1042    "range_from" => RANGE_FROM_SIGNATURES,
1043    "range_to" => RANGE_TO_SIGNATURES,
1044    "range_to_inclusive" => RANGE_TO_INCLUSIVE_SIGNATURES,
1045};
1046
1047/// A single built-in overload's declared operand types, exposed for the static type checker
1048/// (`cel_parser::ty::check_expr`) — never `op_fn`, which is execution-only and stays private.
1049///
1050/// # Examples
1051/// See [`builtin_operand_types`] for inspecting overload metadata.
1052#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1053pub struct OperandTypes {
1054    /// Number of operands this overload accepts (1 or 2).
1055    pub arity: u8,
1056    /// The LHS (or sole, for arity 1) operand's `TypeId`.
1057    pub lhs: TypeId,
1058    /// The RHS operand's `TypeId`; equal to `lhs` for a homogeneous or arity-1 overload.
1059    pub rhs: TypeId,
1060}
1061
1062/// Returns every built-in overload's declared operand types for `name`, in registration order.
1063///
1064/// Reads the exact same static signature tables `BuiltinScope::lookup` dispatches against, so
1065/// the static type checker and the runtime dispatcher share one source of truth and can't drift
1066/// apart on which operand-type combinations a built-in operator accepts.
1067///
1068/// - Postcondition: returns an empty `Vec` if `name` names no built-in operator. A custom scope
1069///   registered via [`OpLookup::push_scope`] or a tuple-shaped op registered via
1070///   [`OpLookup::register_tuple_op`] is runtime-only (attached to one `OpLookup` instance, not
1071///   this module's static tables) and is not visible here — the type checker treats such an
1072///   operator as unchecked, not an error.
1073///
1074/// - Complexity: O(s) where s is the number of overloads registered for `name`.
1075///
1076/// # Examples
1077///
1078/// ```rust
1079/// use cel_parser::op_table::builtin_operand_types;
1080///
1081/// assert!(builtin_operand_types("+").iter().any(|sig| sig.arity == 2));
1082/// assert!(builtin_operand_types("not_an_operator").is_empty());
1083/// ```
1084pub fn builtin_operand_types(name: &str) -> Vec<OperandTypes> {
1085    let Some(signatures) = signatures_for(name) else {
1086        return Vec::new();
1087    };
1088    signatures
1089        .iter()
1090        .map(|sig| OperandTypes {
1091            arity: sig.arity,
1092            lhs: sig.lhs_type_id(),
1093            rhs: sig.rhs_type_id(),
1094        })
1095        .collect()
1096}
1097
1098/// Routes an operator name to its static signature table, or `None` if `name` names no built-in
1099/// operator. Shared by [`builtin_operand_types`] and [`BuiltinScope::lookup`] so a future
1100/// heterogeneous operator only needs its routing added in one place.
1101fn signatures_for(name: &str) -> Option<&'static [OpSignature]> {
1102    match name {
1103        "<<" => Some(&LEFT_SHIFT_SIGNATURES),
1104        ">>" => Some(&RIGHT_SHIFT_SIGNATURES),
1105        _ => BUILTINS.get(name).copied(),
1106    }
1107}
1108
1109// ─────────────────────────────────────────────────────────────────────────
1110// Casts (`expr as Type`)
1111// ─────────────────────────────────────────────────────────────────────────
1112//
1113// Covers every conversion Rust's own `as` operator supports among the
1114// built-in types this crate's `Ty` model recognizes: the 12 integer widths,
1115// `f32`/`f64`, `bool`, and `String`. A source/target pair not registered
1116// here is one Rust's `as` itself rejects at compile time - e.g. `x as bool`
1117// for a numeric `x` (E0054: "cannot cast ... as `bool`") or `x as f64` for
1118// a `bool` `x` (E0606: "casting `bool` as `f64` is invalid") - so `bool` and
1119// `String` are legitimate, fully-recognized cast *targets*, just ones with a
1120// narrow set of legal sources, not omitted types.
1121//
1122// Fallibility follows one rule, not Rust's `as` (which never fails - it
1123// saturates or truncates silently): a conversion that can always represent
1124// its source value exactly is infallible (`op1`); one that might not is
1125// checked and returns `Err` rather than silently losing information,
1126// matching this crate's established convention for arithmetic (see the
1127// module doc comment's "Semantics" section) and `round`'s own design.
1128//   - int -> int: checked via `TryFrom`, uniformly (including same-width and
1129//     identity pairs, which just always succeed).
1130//   - int -> float: infallible (`as`) - Rust's own behavior; precision loss
1131//     for very large integers is accepted, matching Rust exactly.
1132//   - float -> int: checked - `Err` for non-finite or out-of-range values;
1133//     a value with a fractional part is *truncated* toward zero, same as
1134//     Rust's `as` (checking is only about range/finiteness, not fractional
1135//     truncation policy - use a standard-library rounding function before
1136//     converting when nearest-integer behavior is needed).
1137//   - f32 -> f64: infallible (always exact). f64 -> f32: checked (may not
1138//     fit in `f32`'s finite range).
1139//   - bool -> int: infallible (`as`), matching Rust exactly (`true` -> `1`,
1140//     `false` -> `0`) for all 12 integer widths. Rust has no `bool -> float`
1141//     `as` cast (E0606), so none is registered here either.
1142//   - bool -> bool, String -> String: the only legal `as bool` / `as String`
1143//     sources are their own identity conversions (Rust allows `e as T`
1144//     whenever `e`'s type is already `T`, for any `T`, not just numerics);
1145//     infallible, a plain pass-through.
1146
1147/// A signature for a numeric cast (`expr as Type`): one per legal source
1148/// type for a given target type.
1149#[derive(Clone, Copy)]
1150struct CastSignature {
1151    /// Index into TYPE_IDS for the source (operand) type.
1152    source_type_id_index: usize,
1153    /// Function pointer to the conversion implementation.
1154    op_fn: OpFn,
1155}
1156
1157impl CastSignature {
1158    /// Returns the `TypeId` of the source (operand) type.
1159    fn source_type_id(&self) -> TypeId {
1160        TYPE_IDS[self.source_type_id_index]
1161    }
1162}
1163
1164/// Pushes one checked int->int `CastSignature` (via `TryFrom`) onto `$v`.
1165macro_rules! try_from_cast_push {
1166    ($v:ident, $src_idx:expr, $src_ty:ty, $tgt_ty:ty) => {
1167        $v.push(CastSignature {
1168            source_type_id_index: $src_idx,
1169            op_fn: |seg, span| {
1170                seg.op1r(move |x: $src_ty| -> Result<$tgt_ty> {
1171                    <$tgt_ty>::try_from(x).map_err(|_| {
1172                        span_err(
1173                            span,
1174                            anyhow!(concat!("value does not fit in `", stringify!($tgt_ty), "`")),
1175                        )
1176                    })
1177                })
1178            },
1179        });
1180    };
1181}
1182
1183/// Pushes all 12 checked int->int `CastSignature`s targeting `$tgt_ty` onto `$v`.
1184macro_rules! all_int_to_int_casts {
1185    ($v:ident, $tgt_ty:ty) => {
1186        try_from_cast_push!($v, TYPE_U8, u8, $tgt_ty);
1187        try_from_cast_push!($v, TYPE_U16, u16, $tgt_ty);
1188        try_from_cast_push!($v, TYPE_U32, u32, $tgt_ty);
1189        try_from_cast_push!($v, TYPE_U64, u64, $tgt_ty);
1190        try_from_cast_push!($v, TYPE_U128, u128, $tgt_ty);
1191        try_from_cast_push!($v, TYPE_USIZE, usize, $tgt_ty);
1192        try_from_cast_push!($v, TYPE_I8, i8, $tgt_ty);
1193        try_from_cast_push!($v, TYPE_I16, i16, $tgt_ty);
1194        try_from_cast_push!($v, TYPE_I32, i32, $tgt_ty);
1195        try_from_cast_push!($v, TYPE_I64, i64, $tgt_ty);
1196        try_from_cast_push!($v, TYPE_I128, i128, $tgt_ty);
1197        try_from_cast_push!($v, TYPE_ISIZE, isize, $tgt_ty);
1198    };
1199}
1200
1201/// Pushes one infallible int->float `CastSignature` (via `as`) onto `$v`.
1202macro_rules! as_cast_push {
1203    ($v:ident, $src_idx:expr, $src_ty:ty, $tgt_ty:ty) => {
1204        $v.push(CastSignature {
1205            source_type_id_index: $src_idx,
1206            op_fn: |seg, _span| seg.op1(|x: $src_ty| x as $tgt_ty),
1207        });
1208    };
1209}
1210
1211/// Pushes all 12 infallible int->float `CastSignature`s targeting `$tgt_ty` onto `$v`.
1212macro_rules! all_int_to_float_casts {
1213    ($v:ident, $tgt_ty:ty) => {
1214        as_cast_push!($v, TYPE_U8, u8, $tgt_ty);
1215        as_cast_push!($v, TYPE_U16, u16, $tgt_ty);
1216        as_cast_push!($v, TYPE_U32, u32, $tgt_ty);
1217        as_cast_push!($v, TYPE_U64, u64, $tgt_ty);
1218        as_cast_push!($v, TYPE_U128, u128, $tgt_ty);
1219        as_cast_push!($v, TYPE_USIZE, usize, $tgt_ty);
1220        as_cast_push!($v, TYPE_I8, i8, $tgt_ty);
1221        as_cast_push!($v, TYPE_I16, i16, $tgt_ty);
1222        as_cast_push!($v, TYPE_I32, i32, $tgt_ty);
1223        as_cast_push!($v, TYPE_I64, i64, $tgt_ty);
1224        as_cast_push!($v, TYPE_I128, i128, $tgt_ty);
1225        as_cast_push!($v, TYPE_ISIZE, isize, $tgt_ty);
1226    };
1227}
1228
1229/// Pushes one checked float->int `CastSignature` onto `$v`: `Err` for a
1230/// non-finite or out-of-range value, otherwise truncates toward zero (same
1231/// as Rust's `as`).
1232///
1233/// The upper bound is deliberately not `<$tgt_ty>::MAX as $src_ty`: `MAX` (`0b0111...1`) is one
1234/// less than the power of two `MAX + 1`, so whenever `$tgt_ty` has more value bits than
1235/// `$src_ty`'s mantissa (e.g. `i32::MAX as f32`), the conversion has nowhere to round to but
1236/// *up*, landing exactly on `MAX + 1` - a value that itself is genuinely out of range but would
1237/// then compare equal to (not greater than) the bound, silently passing the check and letting
1238/// Rust's `as` saturate it to `MAX` instead of returning `Err`. Deriving the exclusive upper
1239/// bound directly as a power of two (`2^bits` unsigned, `2^(bits-1)` signed) sidesteps the
1240/// rounding entirely: whenever that power of two is within `$src_ty`'s finite range, it's exact
1241/// (no mantissa bits are needed to represent a power of two, only the exponent). The one case
1242/// where it isn't - `u128` (`2^128`) as an `f32` source, since `2^128` exceeds `f32::MAX` - falls
1243/// back to `f32::INFINITY` as the bound, which is still correct: every finite `f32` value is
1244/// already below `u128::MAX`, so a bound of infinity never wrongly accepts one.
1245macro_rules! float_to_int_cast_push {
1246    ($v:ident, $src_idx:expr, $src_ty:ty, $tgt_ty:ty) => {
1247        $v.push(CastSignature {
1248            source_type_id_index: $src_idx,
1249            op_fn: |seg, span| {
1250                seg.op1r(move |x: $src_ty| -> Result<$tgt_ty> {
1251                    if !x.is_finite() {
1252                        return Err(span_err(span, anyhow!("value is not finite")));
1253                    }
1254                    let is_unsigned = <$tgt_ty>::MIN == 0;
1255                    let bits = <$tgt_ty>::BITS as i32;
1256                    let exponent = if is_unsigned { bits } else { bits - 1 };
1257                    let upper_exclusive: $src_ty = (2.0 as $src_ty).powi(exponent);
1258                    if x < <$tgt_ty>::MIN as $src_ty || x >= upper_exclusive {
1259                        return Err(span_err(
1260                            span,
1261                            anyhow!(concat!("value does not fit in `", stringify!($tgt_ty), "`")),
1262                        ));
1263                    }
1264                    Ok(x as $tgt_ty)
1265                })
1266            },
1267        });
1268    };
1269}
1270
1271/// Pushes both checked float->int `CastSignature`s (`f32`, `f64`) targeting `$tgt_ty` onto `$v`.
1272macro_rules! all_float_to_int_casts {
1273    ($v:ident, $tgt_ty:ty) => {
1274        float_to_int_cast_push!($v, TYPE_F32, f32, $tgt_ty);
1275        float_to_int_cast_push!($v, TYPE_F64, f64, $tgt_ty);
1276    };
1277}
1278
1279macro_rules! int_cast_sources {
1280    ($name:ident, $tgt_ty:ty) => {
1281        static $name: Lazy<Vec<CastSignature>> = Lazy::new(|| {
1282            let mut v = Vec::with_capacity(15);
1283            all_int_to_int_casts!(v, $tgt_ty);
1284            all_float_to_int_casts!(v, $tgt_ty);
1285            as_cast_push!(v, TYPE_BOOL, bool, $tgt_ty);
1286            v
1287        });
1288    };
1289}
1290
1291int_cast_sources!(U8_CAST_SOURCES, u8);
1292int_cast_sources!(U16_CAST_SOURCES, u16);
1293int_cast_sources!(U32_CAST_SOURCES, u32);
1294int_cast_sources!(U64_CAST_SOURCES, u64);
1295int_cast_sources!(U128_CAST_SOURCES, u128);
1296int_cast_sources!(USIZE_CAST_SOURCES, usize);
1297int_cast_sources!(I8_CAST_SOURCES, i8);
1298int_cast_sources!(I16_CAST_SOURCES, i16);
1299int_cast_sources!(I32_CAST_SOURCES, i32);
1300int_cast_sources!(I64_CAST_SOURCES, i64);
1301int_cast_sources!(I128_CAST_SOURCES, i128);
1302int_cast_sources!(ISIZE_CAST_SOURCES, isize);
1303
1304static F32_CAST_SOURCES: Lazy<Vec<CastSignature>> = Lazy::new(|| {
1305    let mut v = vec![
1306        // f64 -> f32: checked (may not fit in f32's finite range). Unlike the int-narrowing
1307        // checks below, comparing directly against `f32::MAX as f64` is safe here rather than
1308        // needing the power-of-two-derived bound: f64's 53-bit mantissa always represents
1309        // f32::MAX exactly (f32 needs at most 24 significant bits), so this conversion never
1310        // rounds and the bound is exact.
1311        CastSignature {
1312            source_type_id_index: TYPE_F64,
1313            op_fn: |seg, span| {
1314                seg.op1r(move |x: f64| -> Result<f32> {
1315                    if !x.is_finite() {
1316                        return Err(span_err(span, anyhow!("value is not finite")));
1317                    }
1318                    if x.abs() > f32::MAX as f64 {
1319                        return Err(span_err(span, anyhow!("value does not fit in `f32`")));
1320                    }
1321                    Ok(x as f32)
1322                })
1323            },
1324        },
1325        CastSignature {
1326            source_type_id_index: TYPE_F32,
1327            op_fn: |seg, _span| seg.op1(|x: f32| x),
1328        },
1329    ];
1330    all_int_to_float_casts!(v, f32);
1331    v
1332});
1333
1334static F64_CAST_SOURCES: Lazy<Vec<CastSignature>> = Lazy::new(|| {
1335    let mut v = vec![
1336        // f32 -> f64: always exact.
1337        CastSignature {
1338            source_type_id_index: TYPE_F32,
1339            op_fn: |seg, _span| seg.op1(|x: f32| x as f64),
1340        },
1341        CastSignature {
1342            source_type_id_index: TYPE_F64,
1343            op_fn: |seg, _span| seg.op1(|x: f64| x),
1344        },
1345    ];
1346    all_int_to_float_casts!(v, f64);
1347    v
1348});
1349
1350/// `bool`'s only legal `as` source is `bool` itself (Rust has no
1351/// int/float/String -> `bool` `as` cast - E0054): a no-op identity
1352/// conversion.
1353static BOOL_CAST_SOURCES: Lazy<Vec<CastSignature>> = Lazy::new(|| {
1354    vec![CastSignature {
1355        source_type_id_index: TYPE_BOOL,
1356        op_fn: |seg, _span| seg.op1(|x: bool| x),
1357    }]
1358});
1359
1360/// `String`'s only legal `as` source is `String` itself (Rust's `as` has no
1361/// conversion at all to `String` from any other type): a no-op identity
1362/// conversion.
1363static STRING_CAST_SOURCES: Lazy<Vec<CastSignature>> = Lazy::new(|| {
1364    vec![CastSignature {
1365        source_type_id_index: TYPE_STR,
1366        op_fn: |seg, _span| seg.op1(|x: String| x),
1367    }]
1368});
1369
1370/// Routes a cast target type name to its static signature table (one legal
1371/// source per entry), or `None` if `target_name` names no type this
1372/// language's `as` operator recognizes as a cast target at all. Shared by
1373/// [`cast_source_types`] (the type checker) and [`OpLookup::lookup_cast`]
1374/// (execution) so they can't drift.
1375fn signatures_for_cast(target_name: &str) -> Option<&'static [CastSignature]> {
1376    match target_name {
1377        "u8" => Some(&U8_CAST_SOURCES),
1378        "u16" => Some(&U16_CAST_SOURCES),
1379        "u32" => Some(&U32_CAST_SOURCES),
1380        "u64" => Some(&U64_CAST_SOURCES),
1381        "u128" => Some(&U128_CAST_SOURCES),
1382        "usize" => Some(&USIZE_CAST_SOURCES),
1383        "i8" => Some(&I8_CAST_SOURCES),
1384        "i16" => Some(&I16_CAST_SOURCES),
1385        "i32" => Some(&I32_CAST_SOURCES),
1386        "i64" => Some(&I64_CAST_SOURCES),
1387        "i128" => Some(&I128_CAST_SOURCES),
1388        "isize" => Some(&ISIZE_CAST_SOURCES),
1389        "f32" => Some(&F32_CAST_SOURCES),
1390        "f64" => Some(&F64_CAST_SOURCES),
1391        "bool" => Some(&BOOL_CAST_SOURCES),
1392        "String" => Some(&STRING_CAST_SOURCES),
1393        _ => None,
1394    }
1395}
1396
1397/// Lists every source `TypeId` with a registered cast to `target_name`. Used by the type checker
1398/// (`ty.rs`) to validate a cast without needing to touch a [`DynSegment`] - reads the exact same
1399/// tables [`OpLookup::lookup_cast`] does, so the two can't drift out of sync.
1400///
1401/// - Postcondition: yields no items if `target_name` names no recognized cast-target type.
1402///
1403/// - Complexity: O(s) where s is the number of registered sources for `target_name`.
1404///
1405/// # Examples
1406///
1407/// ```rust
1408/// use cel_parser::op_table::cast_source_types;
1409///
1410/// assert!(cast_source_types("i32").count() > 0);
1411/// assert_eq!(cast_source_types("not_a_type").count(), 0);
1412/// ```
1413pub fn cast_source_types(target_name: &str) -> impl Iterator<Item = TypeId> {
1414    signatures_for_cast(target_name)
1415        .into_iter()
1416        .flat_map(|sigs| sigs.iter().map(CastSignature::source_type_id))
1417}
1418
1419/// One built-in name and its clone-capable runtime descriptor.
1420///
1421/// The default type resolver shares the scalar name set with [`signatures_for_cast`];
1422/// configured resolvers may additionally supply host-registered descriptors.
1423pub(crate) struct BuiltinScalarType {
1424    pub(crate) type_name: &'static str,
1425    /// Builds complete metadata at the concrete built-in registration site.
1426    pub(crate) value_type: fn() -> cel_runtime::value_type::ValueType,
1427}
1428
1429/// Builds independent clone, equality, and formatting plans for a known built-in leaf.
1430fn complete_builtin_leaf<T: Clone + PartialEq + std::fmt::Debug + 'static>()
1431-> cel_runtime::value_type::ValueType {
1432    use cel_runtime::value_type::{Leaf, ValueType};
1433    ValueType::cloneable_leaf::<T>()
1434        .with_equality::<Leaf<T>>()
1435        .expect("same concrete leaf")
1436        .with_debug::<Leaf<T>>()
1437        .expect("same concrete leaf")
1438}
1439
1440/// Declares the one built-in scalar table every built-in name lookup reads: the test-only name list
1441/// `BUILTIN_SCALAR_NAMES` exposes and the descriptors [`builtin_scalar_type`] resolves, so a
1442/// name can never be recognized by one and missed by the other.
1443macro_rules! builtin_scalars {
1444    ($($name:literal => $ty:ty),* $(,)?) => {
1445        /// Every built-in scalar type name, in table order. Used by the cross-table consistency
1446        /// tests that keep this table, [`crate::Ty`], and the CEL type resolver in agreement.
1447        #[cfg(test)]
1448        pub(crate) const BUILTIN_SCALAR_NAMES: &[&str] = &[$($name),*];
1449
1450        /// Returns complete scalar metadata from concrete registration factories.
1451        pub(crate) fn builtin_value_type(id: TypeId) -> Option<cel_runtime::ValueType> {
1452            $(if id == TypeId::of::<$ty>() {
1453                return Some(complete_builtin_leaf::<$ty>());
1454            })*
1455            None
1456        }
1457
1458        /// Resolves a built-in scalar type's bare identifier (a closure parameter annotation, a
1459        /// CEL array annotation leaf) to its full descriptor, or `None` if `name` names no
1460        /// recognized scalar type.
1461        ///
1462        /// - Complexity: O(1).
1463        pub(crate) fn builtin_scalar_type(name: &str) -> Option<BuiltinScalarType> {
1464            Some(match name {
1465                $($name => BuiltinScalarType {
1466                    type_name: $name,
1467                    value_type: complete_builtin_leaf::<$ty>,
1468                },)*
1469                _ => return None,
1470            })
1471        }
1472    };
1473}
1474
1475builtin_scalars! {
1476    "u8" => u8,
1477    "u16" => u16,
1478    "u32" => u32,
1479    "u64" => u64,
1480    "u128" => u128,
1481    "usize" => usize,
1482    "i8" => i8,
1483    "i16" => i16,
1484    "i32" => i32,
1485    "i64" => i64,
1486    "i128" => i128,
1487    "isize" => isize,
1488    "f32" => f32,
1489    "f64" => f64,
1490    "bool" => bool,
1491    "String" => String,
1492}
1493
1494/// Declares the built-in *generic* (parameterized) type table: one entry per
1495/// `(generic constructor name, argument type name)` pair, in the same
1496/// `BuiltinScalarType` shape [`builtin_scalar_type`] returns for plain names. Covers exactly
1497/// the numeric type set each range operator (`..`, `..=`, etc. — see `RANGE_SIGNATURES` and
1498/// its siblings above) already supports.
1499macro_rules! builtin_generic_types {
1500    ($($generic:literal => { $($arg_name:literal => $ty:ty),* $(,)? }),* $(,)?) => {
1501        /// Resolves a one-argument built-in generic type (e.g. `Range(u8)`) to its full
1502        /// descriptor, given the generic constructor's name and its already-resolved argument
1503        /// type's name (e.g. `"u8"`). Returns `None` if `generic_name`/`arg_type_name` names no
1504        /// recognized combination.
1505        ///
1506        /// - Complexity: O(1).
1507        pub(crate) fn builtin_generic_type(
1508            generic_name: &str,
1509            arg_type_name: &str,
1510        ) -> Option<BuiltinScalarType> {
1511            match generic_name {
1512                $($generic => match arg_type_name {
1513                    $($arg_name => Some(BuiltinScalarType {
1514                        type_name: concat!($generic, "(", $arg_name, ")"),
1515                        value_type: complete_builtin_leaf::<$ty>,
1516                    }),)*
1517                    _ => None,
1518                },)*
1519                _ => None,
1520            }
1521        }
1522    };
1523}
1524
1525builtin_generic_types! {
1526    "Range" => {
1527        "u8" => std::ops::Range<u8>, "u16" => std::ops::Range<u16>,
1528        "u32" => std::ops::Range<u32>, "u64" => std::ops::Range<u64>,
1529        "u128" => std::ops::Range<u128>, "usize" => std::ops::Range<usize>,
1530        "i8" => std::ops::Range<i8>, "i16" => std::ops::Range<i16>,
1531        "i32" => std::ops::Range<i32>, "i64" => std::ops::Range<i64>,
1532        "i128" => std::ops::Range<i128>, "isize" => std::ops::Range<isize>,
1533        "f32" => std::ops::Range<f32>, "f64" => std::ops::Range<f64>,
1534    },
1535    "RangeInclusive" => {
1536        "u8" => std::ops::RangeInclusive<u8>, "u16" => std::ops::RangeInclusive<u16>,
1537        "u32" => std::ops::RangeInclusive<u32>, "u64" => std::ops::RangeInclusive<u64>,
1538        "u128" => std::ops::RangeInclusive<u128>, "usize" => std::ops::RangeInclusive<usize>,
1539        "i8" => std::ops::RangeInclusive<i8>, "i16" => std::ops::RangeInclusive<i16>,
1540        "i32" => std::ops::RangeInclusive<i32>, "i64" => std::ops::RangeInclusive<i64>,
1541        "i128" => std::ops::RangeInclusive<i128>, "isize" => std::ops::RangeInclusive<isize>,
1542        "f32" => std::ops::RangeInclusive<f32>, "f64" => std::ops::RangeInclusive<f64>,
1543    },
1544    "RangeFrom" => {
1545        "u8" => std::ops::RangeFrom<u8>, "u16" => std::ops::RangeFrom<u16>,
1546        "u32" => std::ops::RangeFrom<u32>, "u64" => std::ops::RangeFrom<u64>,
1547        "u128" => std::ops::RangeFrom<u128>, "usize" => std::ops::RangeFrom<usize>,
1548        "i8" => std::ops::RangeFrom<i8>, "i16" => std::ops::RangeFrom<i16>,
1549        "i32" => std::ops::RangeFrom<i32>, "i64" => std::ops::RangeFrom<i64>,
1550        "i128" => std::ops::RangeFrom<i128>, "isize" => std::ops::RangeFrom<isize>,
1551        "f32" => std::ops::RangeFrom<f32>, "f64" => std::ops::RangeFrom<f64>,
1552    },
1553    "RangeTo" => {
1554        "u8" => std::ops::RangeTo<u8>, "u16" => std::ops::RangeTo<u16>,
1555        "u32" => std::ops::RangeTo<u32>, "u64" => std::ops::RangeTo<u64>,
1556        "u128" => std::ops::RangeTo<u128>, "usize" => std::ops::RangeTo<usize>,
1557        "i8" => std::ops::RangeTo<i8>, "i16" => std::ops::RangeTo<i16>,
1558        "i32" => std::ops::RangeTo<i32>, "i64" => std::ops::RangeTo<i64>,
1559        "i128" => std::ops::RangeTo<i128>, "isize" => std::ops::RangeTo<isize>,
1560        "f32" => std::ops::RangeTo<f32>, "f64" => std::ops::RangeTo<f64>,
1561    },
1562    "RangeToInclusive" => {
1563        "u8" => std::ops::RangeToInclusive<u8>, "u16" => std::ops::RangeToInclusive<u16>,
1564        "u32" => std::ops::RangeToInclusive<u32>, "u64" => std::ops::RangeToInclusive<u64>,
1565        "u128" => std::ops::RangeToInclusive<u128>, "usize" => std::ops::RangeToInclusive<usize>,
1566        "i8" => std::ops::RangeToInclusive<i8>, "i16" => std::ops::RangeToInclusive<i16>,
1567        "i32" => std::ops::RangeToInclusive<i32>, "i64" => std::ops::RangeToInclusive<i64>,
1568        "i128" => std::ops::RangeToInclusive<i128>, "isize" => std::ops::RangeToInclusive<isize>,
1569        "f32" => std::ops::RangeToInclusive<f32>, "f64" => std::ops::RangeToInclusive<f64>,
1570    },
1571}
1572
1573/// Resolves a zero-argument built-in generic type (currently only `RangeFull`) to its full
1574/// descriptor.
1575///
1576/// - Complexity: O(1).
1577pub(crate) fn builtin_generic_type_0(name: &str) -> Option<BuiltinScalarType> {
1578    match name {
1579        "RangeFull" => Some(BuiltinScalarType {
1580            type_name: "RangeFull",
1581            value_type: complete_builtin_leaf::<std::ops::RangeFull>,
1582        }),
1583        _ => None,
1584    }
1585}
1586
1587/// Built-in operation scope.
1588///
1589/// Provides lookup for standard operations using a compile-time hash table.
1590struct BuiltinScope;
1591
1592impl BuiltinScope {
1593    /// Attempts to find and apply a built-in operation.
1594    ///
1595    /// Returns `Ok(true)` if found and applied, `Ok(false)` if not found.
1596    ///
1597    /// - Complexity: O(s) where s is the number of signatures registered for `name`.
1598    fn lookup(
1599        &self,
1600        name: &str,
1601        segment: &mut DynSegment,
1602        num_operands: usize,
1603        span: SourceSpan,
1604    ) -> Result<bool> {
1605        let stack_infos = segment.peek_stack_infos(num_operands);
1606        let Some(signatures) = signatures_for(name) else {
1607            return Ok(false);
1608        };
1609        for sig in signatures {
1610            let arity = sig.arity as usize;
1611            let matches = arity == stack_infos.len()
1612                && stack_infos[0].value_type.type_id() == sig.lhs_type_id()
1613                && (arity < 2 || stack_infos[1].value_type.type_id() == sig.rhs_type_id());
1614            if matches {
1615                (sig.op_fn)(segment, span)?;
1616                return Ok(true);
1617            }
1618        }
1619        Ok(false)
1620    }
1621}
1622
1623/// Scope function implementing the arity-0 `range_full` internal op: constructs
1624/// `std::ops::RangeFull`, the value a bare `..` produces. There is no second
1625/// half because `RangeFull` is never called with arguments.
1626///
1627/// Registered by every [`OpLookup::new()`] (see there). `"range_full"` is an internal
1628/// dispatch name the parser selects when it recognizes a bare `..` with neither a left
1629/// nor right endpoint — never a name CEL source can reference directly, matching
1630/// `"range"`/`"range_inclusive"`/`"range_from"`/`"range_to"`/`"range_to_inclusive"`.
1631fn range_full_scope(
1632    name: &str,
1633    segment: &mut DynSegment,
1634    num_operands: usize,
1635    _span: SourceSpan,
1636) -> Result<bool> {
1637    match (name, num_operands) {
1638        ("range_full", 0) => {
1639            segment.op0(|| std::ops::RangeFull);
1640            Ok(true)
1641        }
1642        _ => Ok(false),
1643    }
1644}
1645
1646/// Operation lookup with scope stack support.
1647///
1648/// Resolves scopes in LIFO order, then persistent named registrations, tuple
1649/// signatures, and built-in operations. Named registrations survive scope isolation.
1650///
1651/// # Examples
1652///
1653/// ```rust
1654/// use cel_parser::op_table::OpLookup;
1655/// use cel_runtime::DynSegment;
1656/// use std::any::TypeId;
1657///
1658/// let mut lookup = OpLookup::new();
1659///
1660/// // Use built-in addition
1661/// let mut segment = DynSegment::new::<()>();
1662/// segment.just(10u32);
1663/// segment.just(20u32);
1664/// lookup.lookup("+", &mut segment, 2, proc_macro2::Span::call_site(), proc_macro2::Span::call_site()).unwrap();
1665/// assert_eq!(segment.call0::<u32>().unwrap(), 30);
1666/// ```
1667pub struct OpLookup {
1668    pub(crate) registered: crate::registered_ops::RegisteredOps,
1669    scopes: Vec<ScopeFn>,
1670    library_scope_count: usize,
1671    builtin_scope: BuiltinScope,
1672    tuple_signatures: Vec<TupleOpSignature>,
1673}
1674
1675impl OpLookup {
1676    /// Creates a new operation lookup with only built-in operations - the
1677    /// infix/prefix operators, the cast operator (`as`), and range syntax.
1678    ///
1679    /// # Examples
1680    ///
1681    /// ```rust
1682    /// use cel_parser::OpLookup;
1683    ///
1684    /// let lookup = OpLookup::new();
1685    /// ```
1686    pub fn new() -> Self {
1687        let mut lookup = OpLookup {
1688            registered: Default::default(),
1689            scopes: Vec::new(),
1690            library_scope_count: 0,
1691            builtin_scope: BuiltinScope,
1692            tuple_signatures: Vec::new(),
1693        };
1694        lookup.push_library_scope(range_full_scope);
1695        lookup
1696    }
1697
1698    /// Registers a tuple-shaped operator signature, matched by element
1699    /// `TypeId` sequence the same way built-in operators are matched by flat
1700    /// `TypeId`.
1701    ///
1702    /// - Complexity: Amortized O(1) registration, transferring the owned signature.
1703    ///
1704    /// # Examples
1705    /// ```
1706    /// use cel_parser::op_table::{OpLookup, TupleOpSignature};
1707    /// use cel_runtime::DynSegment;
1708    /// use proc_macro2::Span;
1709    /// use std::any::TypeId;
1710    /// let mut lookup = OpLookup::new();
1711    /// lookup.register_tuple_op(TupleOpSignature {
1712    ///     name: "first".into(),
1713    ///     shape: vec![TypeId::of::<i32>()],
1714    ///     tuple_operand_index: 0,
1715    ///     operand_type_ids: Vec::new(),
1716    ///     op_fn: |segment, _| segment.tuple_index(0),
1717    /// });
1718    /// let mut segment = DynSegment::new::<()>();
1719    /// segment.just(42i32);
1720    /// segment.make_tuple(1, 0).unwrap();
1721    /// lookup.lookup("first", &mut segment, 1, Span::call_site(), Span::call_site()).unwrap();
1722    /// assert_eq!(segment.call0::<i32>().unwrap(), 42);
1723    /// ```
1724    pub fn register_tuple_op(&mut self, signature: TupleOpSignature) {
1725        self.tuple_signatures.push(signature);
1726    }
1727
1728    /// Attempts to find and apply a registered tuple-shaped signature.
1729    ///
1730    /// Returns `Ok(true)` if found and applied, `Ok(false)` if not found.
1731    ///
1732    /// - Complexity: O(s) where s is the number of registered tuple signatures.
1733    fn lookup_tuple_signature(
1734        &self,
1735        name: &str,
1736        segment: &mut DynSegment,
1737        num_operands: usize,
1738        span: SourceSpan,
1739    ) -> Result<bool> {
1740        let stack_infos = segment.peek_stack_infos(num_operands);
1741        for sig in &self.tuple_signatures {
1742            if sig.name != name || sig.tuple_operand_index >= stack_infos.len() {
1743                continue;
1744            }
1745            let tuple_info = &stack_infos[sig.tuple_operand_index];
1746            let shape_matches = tuple_info
1747                .value_type
1748                .tuple_elements()
1749                .is_some_and(|elements| {
1750                    elements.len() == sig.shape.len()
1751                        && elements
1752                            .iter()
1753                            .zip(&sig.shape)
1754                            .all(|(a, t)| a.value_type.type_id() == *t)
1755                });
1756            if !shape_matches {
1757                continue;
1758            }
1759            let others_match = stack_infos.iter().enumerate().all(|(i, info)| {
1760                i == sig.tuple_operand_index
1761                    || sig.operand_type_ids.get(i) == Some(&info.value_type.type_id())
1762            });
1763            if others_match {
1764                (sig.op_fn)(segment, span)?;
1765                return Ok(true);
1766            }
1767        }
1768        Ok(false)
1769    }
1770
1771    /// Pushes a new scope onto the stack.
1772    ///
1773    /// Accepts a closure directly; it is boxed internally. The scope should return
1774    /// `Ok(true)` if it handled the operation, `Ok(false)` to pass to the next scope,
1775    /// or `Err` on error. Error messages surface verbatim; they should be lowercase, end
1776    /// without a period, and wrap identifiers and type names in backticks.
1777    ///
1778    /// - Complexity: Amortized O(1) registration, including owned callback storage.
1779    ///
1780    /// # Examples
1781    /// ```
1782    /// use cel_parser::OpLookup;
1783    /// use cel_runtime::DynSegment;
1784    /// use proc_macro2::Span;
1785    /// let mut lookup = OpLookup::new();
1786    /// lookup.push_library_scope(|name, segment, arity, _| {
1787    ///     if name == "answer" && arity == 0 {
1788    ///         segment.just(42i32);
1789    ///         Ok(true)
1790    ///     } else {
1791    ///         Ok(false)
1792    ///     }
1793    /// });
1794    /// lookup.push_scope(|_, _, _, _| Ok(false));
1795    /// assert!(lookup.pop_scope().is_some());
1796    /// let mut segment = DynSegment::new::<()>();
1797    /// lookup.lookup("answer", &mut segment, 0, Span::call_site(), Span::call_site()).unwrap();
1798    /// assert_eq!(segment.call0::<i32>().unwrap(), 42);
1799    /// ```
1800    pub fn push_scope<F>(&mut self, scope: F)
1801    where
1802        F: Fn(&str, &mut DynSegment, usize, SourceSpan) -> Result<bool> + Send + Sync + 'static,
1803    {
1804        self.scopes.push(Box::new(scope));
1805    }
1806
1807    /// Pops the most recent scope from the stack.
1808    ///
1809    /// Returns the popped scope, or `None` if the stack is empty.
1810    ///
1811    /// - Precondition: must not remove a library scope registered via
1812    ///   [`push_library_scope`](Self::push_library_scope) — those are permanent setup-time
1813    ///   registrations that must survive across multiple parses.
1814    ///
1815    /// # Examples
1816    /// See [`Self::push_scope`] for adding and removing a transient scope without removing a library.
1817    pub fn pop_scope(&mut self) -> Option<ScopeFn> {
1818        debug_assert!(
1819            self.scopes.len() > self.library_scope_count,
1820            "pop_scope must not remove a library scope — use isolate_scopes/restore_scopes semantics instead"
1821        );
1822        self.scopes.pop()
1823    }
1824
1825    /// Registers a permanent, library-level scope that is reachable from every parse,
1826    /// including inside closure bodies.
1827    ///
1828    /// Used for built-in language features and statically-installed library
1829    /// functions (like `round` and `clamp` from a `cel-std`-style crate). These scopes are registered
1830    /// once at setup time and must always be available, even when [`isolate_scopes`](Self::isolate_scopes)
1831    /// is active — library scopes are *never* isolated.
1832    ///
1833    /// Do not use for scopes tied to a single parse's lifetime — use [`push_scope`](Self::push_scope)
1834    /// for those, which can be isolated during nested body compilation (closures).
1835    ///
1836    /// - Precondition: no transient scopes are installed.
1837    /// - Complexity: Amortized O(1) registration, including owned callback storage.
1838    ///
1839    /// # Examples
1840    /// See [`Self::push_scope`] for permanent and transient registrations.
1841    pub fn push_library_scope<F>(&mut self, scope: F)
1842    where
1843        F: Fn(&str, &mut DynSegment, usize, SourceSpan) -> Result<bool> + Send + Sync + 'static,
1844    {
1845        debug_assert!(
1846            self.scopes.len() == self.library_scope_count,
1847            "push_library_scope must not be called after a transient scope — use isolate_scopes/restore_scopes semantics instead"
1848        );
1849        self.scopes.push(Box::new(scope));
1850        self.library_scope_count = self.scopes.len();
1851    }
1852
1853    /// Temporarily removes every transient scope (those pushed via [`push_scope`](Self::push_scope)),
1854    /// returning them so a later [`restore_scopes`](Self::restore_scopes) call can put them back.
1855    /// Library scopes registered via [`push_library_scope`](Self::push_library_scope) are *never*
1856    /// isolated and remain reachable.
1857    ///
1858    /// Used when compiling an independent nested body (a closure literal) that must resolve names
1859    /// against only its own declared parameters and library functions (like `round`, `clamp`) —
1860    /// never whatever transient per-parse scopes happen to be active. This maintains the invariant
1861    /// that library functions are always available, including inside closures, while per-declaration
1862    /// scopes (which tie to a single outer parse's lifetime) are hidden from nested bodies.
1863    ///
1864    /// - Postcondition: library scopes remain reachable; transient scopes are inaccessible until
1865    ///   [`restore_scopes`](Self::restore_scopes) is called.
1866    ///
1867    /// # Examples
1868    ///
1869    /// ```rust
1870    /// use cel_parser::OpLookup;
1871    /// use cel_runtime::DynSegment;
1872    ///
1873    /// let mut lookup = OpLookup::new();
1874    /// lookup.push_scope(|name, segment, arity, _span| {
1875    ///     if name == "custom" && arity == 0 {
1876    ///         segment.just(42i32);
1877    ///         Ok(true)
1878    ///     } else {
1879    ///         Ok(false)
1880    ///     }
1881    /// });
1882    ///
1883    /// let saved = lookup.isolate_scopes();
1884    /// // Now the custom scope is inaccessible
1885    /// let mut segment = DynSegment::new::<()>();
1886    /// let result = lookup.lookup("custom", &mut segment, 0, proc_macro2::Span::call_site(), proc_macro2::Span::call_site());
1887    /// assert!(result.is_err());
1888    ///
1889    /// lookup.restore_scopes(saved);
1890    /// // Now the custom scope is reachable again
1891    /// let mut segment = DynSegment::new::<()>();
1892    /// let result = lookup.lookup("custom", &mut segment, 0, proc_macro2::Span::call_site(), proc_macro2::Span::call_site());
1893    /// assert!(result.is_ok());
1894    /// ```
1895    pub fn isolate_scopes(&mut self) -> Vec<ScopeFn> {
1896        self.scopes.split_off(self.library_scope_count)
1897    }
1898
1899    /// Restores a scope stack previously removed by [`isolate_scopes`](Self::isolate_scopes),
1900    /// discarding whatever scopes were pushed while isolated.
1901    ///
1902    /// Library scopes (those registered via [`push_library_scope`](Self::push_library_scope))
1903    /// are unaffected by isolation and restoration — they persist across the entire operation.
1904    ///
1905    /// - Precondition: `scopes` came from a matching `isolate_scopes()` call on this same
1906    ///   `OpLookup` — restoring an arbitrary `Vec<ScopeFn>` is well-typed but not a meaningful use
1907    ///   of this method.
1908    ///
1909    /// # Examples
1910    ///
1911    /// ```rust
1912    /// use cel_parser::OpLookup;
1913    /// use cel_runtime::DynSegment;
1914    ///
1915    /// let mut lookup = OpLookup::new();
1916    /// lookup.push_scope(|name, segment, arity, _span| {
1917    ///     if name == "outer" && arity == 0 {
1918    ///         segment.just(100i32);
1919    ///         Ok(true)
1920    ///     } else {
1921    ///         Ok(false)
1922    ///     }
1923    /// });
1924    ///
1925    /// let saved = lookup.isolate_scopes();
1926    /// lookup.restore_scopes(saved);
1927    /// // The outer scope is now reachable again
1928    /// ```
1929    pub fn restore_scopes(&mut self, scopes: Vec<ScopeFn>) {
1930        self.scopes.truncate(self.library_scope_count);
1931        self.scopes.extend(scopes);
1932    }
1933
1934    /// Looks up and applies an operation, attaching the expression span to any error.
1935    ///
1936    /// Searches scopes in LIFO order, then persistent named registrations, tuple
1937    /// signatures, and built-in operations.
1938    ///
1939    /// # Errors
1940    ///
1941    /// Returns a [`crate::ParseError`] spanning `start..=end` if no scope or built-in
1942    /// handles the request, or if a scope itself returns an error.
1943    ///
1944    /// - Complexity: O(k) in the number of registered scopes, plus O(s) for the built-in
1945    ///   signature scan where s is the number of signatures for the operator.
1946    ///
1947    /// # Examples
1948    ///
1949    /// ```rust
1950    /// use proc_macro2::Span;
1951    /// use cel_parser::OpLookup;
1952    /// use cel_runtime::DynSegment;
1953    ///
1954    /// let lookup = OpLookup::new();
1955    /// let mut seg = DynSegment::new::<()>();
1956    /// // A lookup with zero operands for a known operator succeeds when types match.
1957    /// // This example shows the signature only; real usage requires pushed types.
1958    /// let result = lookup.lookup("+", &mut seg, 2, Span::call_site(), Span::call_site());
1959    /// // result is Err because no operands are on the segment
1960    /// assert!(result.is_err());
1961    /// ```
1962    pub fn lookup(
1963        &self,
1964        name: &str,
1965        segment: &mut DynSegment,
1966        num_operands: usize,
1967        start: proc_macro2::Span,
1968        end: proc_macro2::Span,
1969    ) -> std::result::Result<(), crate::ParseError> {
1970        self.lookup_scoped(
1971            name,
1972            segment,
1973            num_operands,
1974            crate::ExprSpan { start, end },
1975            false,
1976            false,
1977        )
1978    }
1979
1980    /// Applies a direct named operation without looking up a zero-arity value binding.
1981    ///
1982    /// Scopes retain LIFO precedence for nonzero operand counts; registered operations,
1983    /// tuple signatures and built-ins provide the fallbacks. A declined signature
1984    /// leaves the stack unchanged.
1985    ///
1986    /// # Errors
1987    /// Returns a spanned error if no operation matches or an emitter fails.
1988    ///
1989    /// - Complexity: O(k + s * a * d) for k scopes, s signatures, a operands
1990    ///   and descriptor depth d.
1991    ///
1992    /// # Examples
1993    /// ```
1994    /// use cel_parser::OpLookup;
1995    /// use cel_runtime::DynSegment;
1996    /// use proc_macro2::Span;
1997    /// let mut segment = DynSegment::new::<()>();
1998    /// segment.just(1u32);
1999    /// segment.just(2u32);
2000    /// OpLookup::new().lookup_member("+", &mut segment, 2,
2001    ///     Span::call_site(), Span::call_site()).unwrap();
2002    /// assert_eq!(segment.call0::<u32>().unwrap(), 3);
2003    /// ```
2004    pub fn lookup_member(
2005        &self,
2006        name: &str,
2007        segment: &mut DynSegment,
2008        arity: usize,
2009        start: proc_macro2::Span,
2010        end: proc_macro2::Span,
2011    ) -> crate::Result<()> {
2012        self.lookup_scoped(
2013            name,
2014            segment,
2015            arity,
2016            crate::ExprSpan { start, end },
2017            true,
2018            false,
2019        )
2020    }
2021
2022    /// Resolves direct operations with either all scopes or only persistent library scopes.
2023    ///
2024    /// Zero-operand dispatch skips scopes, which cannot distinguish value bindings
2025    /// from direct operations at that arity.
2026    ///
2027    /// # Errors
2028    /// Returns the selected scope or registered emitter error.
2029    /// - Complexity: O(k + s * a * d) for k scopes, s signatures, a operands and depth d.
2030    pub(crate) fn lookup_direct_operation_scoped(
2031        &self,
2032        name: &str,
2033        segment: &mut DynSegment,
2034        arity: usize,
2035        span: SourceSpan,
2036        library_only: bool,
2037    ) -> Result<bool> {
2038        if arity > 0 {
2039            let scopes = if library_only {
2040                &self.scopes[..self.library_scope_count]
2041            } else {
2042                &self.scopes
2043            };
2044            for scope in scopes.iter().rev() {
2045                if scope(name, segment, arity, span)? {
2046                    return Ok(true);
2047                }
2048            }
2049        }
2050        self.lookup_registered_operation(name, segment, arity, span)
2051    }
2052
2053    /// Applies scoped lookup with either ordinary callable or operation-only dispatch.
2054    ///
2055    /// - Complexity: O(k + s * a * d) for k scopes, s signatures, a operands
2056    ///   and descriptor depth d.
2057    pub(crate) fn lookup_scoped(
2058        &self,
2059        name: &str,
2060        segment: &mut DynSegment,
2061        num_operands: usize,
2062        span: crate::ExprSpan,
2063        member: bool,
2064        library_only: bool,
2065    ) -> crate::Result<()> {
2066        let crate::ExprSpan { start, end } = span;
2067        let source_span = SourceSpan::from_proc_macro2_range(start, end);
2068        let scopes = if library_only {
2069            &self.scopes[..self.library_scope_count]
2070        } else {
2071            &self.scopes
2072        };
2073        for scope in scopes.iter().rev().filter(|_| !member) {
2074            match scope(name, segment, num_operands, source_span) {
2075                Ok(true) => return Ok(()),
2076                Ok(false) => {}
2077                Err(e) => return Err(crate::ParseError::new_range(e.to_string(), start, end)),
2078            }
2079        }
2080
2081        let registered = if member {
2082            self.lookup_direct_operation_scoped(
2083                name,
2084                segment,
2085                num_operands,
2086                source_span,
2087                library_only,
2088            )
2089        } else {
2090            self.lookup_registered(name, segment, num_operands, source_span, library_only)
2091        };
2092        match registered {
2093            Ok(true) => return Ok(()),
2094            Ok(false) => {}
2095            Err(error) => return Err(crate::ParseError::new_range(error.to_string(), start, end)),
2096        }
2097
2098        match self.lookup_tuple_signature(name, segment, num_operands, source_span) {
2099            Ok(true) => return Ok(()),
2100            Ok(false) => {}
2101            Err(e) => {
2102                return Err(crate::ParseError::new_range(
2103                    format!("operation error: {}", e),
2104                    start,
2105                    end,
2106                ));
2107            }
2108        }
2109
2110        match self
2111            .builtin_scope
2112            .lookup(name, segment, num_operands, source_span)
2113        {
2114            Ok(true) => return Ok(()),
2115            Ok(false) => {}
2116            Err(e) => {
2117                return Err(crate::ParseError::new_range(
2118                    format!("operation error: {}", e),
2119                    start,
2120                    end,
2121                ));
2122            }
2123        }
2124
2125        if !member
2126            && name == "()"
2127            && num_operands > 0
2128            && segment
2129                .peek_stack_infos(num_operands)
2130                .first()
2131                .is_some_and(|info| {
2132                    info.value_type.type_id() == TypeId::of::<cel_runtime::DynClosure>()
2133                })
2134        {
2135            return segment
2136                .invoke_closure(num_operands - 1)
2137                .map_err(|error| crate::ParseError::new_range(error.to_string(), start, end));
2138        }
2139
2140        if num_operands == 0 {
2141            return Err(crate::ParseError::new(
2142                format!("undefined identifier: `{name}`"),
2143                start,
2144            ));
2145        }
2146        let infos = segment.peek_stack_infos(num_operands);
2147        let mut type_names = String::new();
2148        for (i, info) in infos.iter().enumerate() {
2149            if i > 0 {
2150                type_names.push_str(", ");
2151            }
2152            type_names.push('`');
2153            type_names.push_str(info.value_type.type_name());
2154            type_names.push('`');
2155        }
2156        Err(crate::ParseError::new_range(
2157            format!("no operation `{name}` for types [{type_names}]"),
2158            start,
2159            end,
2160        ))
2161    }
2162
2163    /// Looks up and applies a cast (`expr as Type`), attaching the expression span to any error.
2164    ///
2165    /// - Postcondition: on success, an operation is queued onto `segment` that replaces its
2166    ///   top-of-stack operand with the converted value once the segment is run (e.g. via
2167    ///   [`DynSegment::call0`]) - like every other op this crate queues rather than executes
2168    ///   immediately.
2169    ///
2170    /// # Errors
2171    ///
2172    /// Returns a [`crate::ParseError`] spanning `start..=end` if `segment`'s stack is empty, if
2173    /// `type_name` isn't a recognized cast-target type, or if no cast from the operand's type to
2174    /// it is registered. A registered conversion's own value-range failure (e.g. an out-of-range
2175    /// or non-finite value) is deferred to execution - it surfaces from running the segment, not
2176    /// from this function (see `cast_errors_when_the_value_does_not_fit_in_the_target_type`
2177    /// below).
2178    ///
2179    /// - Complexity: O(s) in the number of registered sources for `type_name`.
2180    ///
2181    /// # Examples
2182    ///
2183    /// ```rust
2184    /// use proc_macro2::Span;
2185    /// use cel_parser::OpLookup;
2186    /// use cel_runtime::DynSegment;
2187    ///
2188    /// let lookup = OpLookup::new();
2189    /// let mut seg = DynSegment::new::<()>();
2190    /// seg.just(1024i32);
2191    /// lookup
2192    ///     .lookup_cast("f64", &mut seg, Span::call_site(), Span::call_site())
2193    ///     .unwrap();
2194    /// assert_eq!(seg.call0::<f64>().unwrap(), 1024.0);
2195    /// ```
2196    pub fn lookup_cast(
2197        &self,
2198        type_name: &str,
2199        segment: &mut DynSegment,
2200        start: proc_macro2::Span,
2201        end: proc_macro2::Span,
2202    ) -> std::result::Result<(), crate::ParseError> {
2203        let source_span = SourceSpan::from_proc_macro2_range(start, end);
2204        let Some(signatures) = signatures_for_cast(type_name) else {
2205            return Err(crate::ParseError::new_range(
2206                format!("unknown type `{type_name}`"),
2207                start,
2208                end,
2209            ));
2210        };
2211        let Some(operand) = segment.peek_stack_infos(1).first() else {
2212            return Err(crate::ParseError::new_range(
2213                "cast requires an operand on the stack".to_string(),
2214                start,
2215                end,
2216            ));
2217        };
2218        let source_type_id = operand.value_type.type_id();
2219        for sig in signatures {
2220            if sig.source_type_id() == source_type_id {
2221                (sig.op_fn)(segment, source_span).map_err(|e| {
2222                    crate::ParseError::new_range(format!("cast error: {}", e), start, end)
2223                })?;
2224                return Ok(());
2225            }
2226        }
2227        Err(crate::ParseError::new_range(
2228            format!(
2229                "no cast from `{}` to `{type_name}`",
2230                operand.value_type.type_name()
2231            ),
2232            start,
2233            end,
2234        ))
2235    }
2236}
2237
2238impl Default for OpLookup {
2239    fn default() -> Self {
2240        Self::new()
2241    }
2242}
2243
2244#[cfg(test)]
2245mod tests {
2246    use super::*;
2247    use proc_macro2::Span;
2248
2249    type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
2250
2251    #[test]
2252    fn test_addition_u32() -> Result<()> {
2253        let lookup = OpLookup::new();
2254        let mut segment = DynSegment::new::<()>();
2255        segment.just(10u32);
2256        segment.just(20u32);
2257        lookup.lookup("+", &mut segment, 2, Span::call_site(), Span::call_site())?;
2258        assert_eq!(segment.call0::<u32>()?, 30);
2259        Ok(())
2260    }
2261
2262    #[test]
2263    fn test_subtraction_i32() -> Result<()> {
2264        let lookup = OpLookup::new();
2265        let mut segment = DynSegment::new::<()>();
2266        segment.just(50i32);
2267        segment.just(20i32);
2268        lookup.lookup("-", &mut segment, 2, Span::call_site(), Span::call_site())?;
2269        assert_eq!(segment.call0::<i32>()?, 30);
2270        Ok(())
2271    }
2272
2273    #[test]
2274    fn test_arithmetic_overflow() -> Result<()> {
2275        let lookup = OpLookup::new();
2276        let mut segment = DynSegment::new::<()>();
2277        segment.just(i32::MAX);
2278        segment.just(1i32);
2279        lookup.lookup("+", &mut segment, 2, Span::call_site(), Span::call_site())?;
2280        let result = segment.call0::<i32>();
2281        assert!(result.is_err());
2282        let err = result.unwrap_err();
2283        let message = format!("{:#}", err);
2284        assert!(
2285            message.contains("arithmetic overflow"),
2286            "error message should mention arithmetic overflow, got: {message}"
2287        );
2288        Ok(())
2289    }
2290
2291    #[test]
2292    fn test_division_by_zero() -> Result<()> {
2293        let lookup = OpLookup::new();
2294        let mut segment = DynSegment::new::<()>();
2295        segment.just(10i32);
2296        segment.just(0i32);
2297        lookup.lookup("/", &mut segment, 2, Span::call_site(), Span::call_site())?;
2298        let result = segment.call0::<i32>();
2299        assert!(result.is_err());
2300        let err = result.unwrap_err();
2301        let message = format!("{:#}", err);
2302        assert!(
2303            message.contains("division by zero"),
2304            "error message should mention division by zero, got: {message}"
2305        );
2306        Ok(())
2307    }
2308
2309    #[test]
2310    fn test_modulo_by_zero() -> Result<()> {
2311        let lookup = OpLookup::new();
2312        let mut segment = DynSegment::new::<()>();
2313        segment.just(10u32);
2314        segment.just(0u32);
2315        lookup.lookup("%", &mut segment, 2, Span::call_site(), Span::call_site())?;
2316        let result = segment.call0::<u32>();
2317        assert!(result.is_err());
2318        let err = result.unwrap_err();
2319        let message = format!("{:#}", err);
2320        assert!(
2321            message.contains("division by zero"),
2322            "error message should mention division by zero, got: {message}"
2323        );
2324        Ok(())
2325    }
2326
2327    #[test]
2328    fn test_multiplication_f64() -> Result<()> {
2329        let lookup = OpLookup::new();
2330        let mut segment = DynSegment::new::<()>();
2331        segment.just(3.5f64);
2332        segment.just(2.0f64);
2333        lookup.lookup("*", &mut segment, 2, Span::call_site(), Span::call_site())?;
2334        assert_eq!(segment.call0::<f64>()?, 7.0);
2335        Ok(())
2336    }
2337
2338    #[test]
2339    fn test_comparison_less_than() -> Result<()> {
2340        let lookup = OpLookup::new();
2341        let mut segment = DynSegment::new::<()>();
2342        segment.just(10u32);
2343        segment.just(20u32);
2344        lookup.lookup("<", &mut segment, 2, Span::call_site(), Span::call_site())?;
2345        assert!(segment.call0::<bool>()?);
2346        Ok(())
2347    }
2348
2349    #[test]
2350    fn test_bitwise_and() -> Result<()> {
2351        let lookup = OpLookup::new();
2352        let mut segment = DynSegment::new::<()>();
2353        segment.just(0b1010u32);
2354        segment.just(0b1100u32);
2355        lookup.lookup("&", &mut segment, 2, Span::call_site(), Span::call_site())?;
2356        assert_eq!(segment.call0::<u32>()?, 0b1000);
2357        Ok(())
2358    }
2359
2360    #[test]
2361    fn test_unary_negation() -> Result<()> {
2362        let lookup = OpLookup::new();
2363        let mut segment = DynSegment::new::<()>();
2364        segment.just(42i32);
2365        lookup.lookup("-", &mut segment, 1, Span::call_site(), Span::call_site())?;
2366        assert_eq!(segment.call0::<i32>()?, -42);
2367        Ok(())
2368    }
2369
2370    #[test]
2371    fn test_logical_not() -> Result<()> {
2372        let lookup = OpLookup::new();
2373        let mut segment = DynSegment::new::<()>();
2374        segment.just(true);
2375        lookup.lookup("!", &mut segment, 1, Span::call_site(), Span::call_site())?;
2376        assert!(!segment.call0::<bool>()?);
2377        Ok(())
2378    }
2379
2380    #[test]
2381    fn test_unregistered_operation() {
2382        let lookup = OpLookup::new();
2383        let mut segment = DynSegment::new::<()>();
2384        segment.just(10u32);
2385        segment.just(20u32);
2386        let result = lookup.lookup(
2387            "unknown_op",
2388            &mut segment,
2389            2,
2390            Span::call_site(),
2391            Span::call_site(),
2392        );
2393        assert!(result.is_err());
2394    }
2395
2396    #[test]
2397    fn test_custom_scope() -> Result<()> {
2398        let mut lookup = OpLookup::new();
2399
2400        lookup.push_scope(|name, segment, num_operands, _span| {
2401            let matches = {
2402                let top = segment.peek_stack_infos(num_operands);
2403                name == "double"
2404                    && top.len() == 1
2405                    && top[0].value_type.type_id() == TypeId::of::<u32>()
2406            };
2407            if matches {
2408                segment.op1(|a: u32| a * 2)?;
2409                Ok(true)
2410            } else {
2411                Ok(false)
2412            }
2413        });
2414
2415        let mut segment = DynSegment::new::<()>();
2416        segment.just(21u32);
2417        lookup.lookup(
2418            "double",
2419            &mut segment,
2420            1,
2421            Span::call_site(),
2422            Span::call_site(),
2423        )?;
2424        assert_eq!(segment.call0::<u32>()?, 42);
2425
2426        Ok(())
2427    }
2428
2429    #[test]
2430    fn test_scope_override() -> Result<()> {
2431        let mut lookup = OpLookup::new();
2432
2433        lookup.push_scope(|name, segment, num_operands, _span| {
2434            let matches = {
2435                let top = segment.peek_stack_infos(num_operands);
2436                name == "+" && top.len() == 2 && top[0].value_type.type_id() == TypeId::of::<u32>()
2437            };
2438            if matches {
2439                segment.op2(|_a: u32, _b: u32| 100u32)?;
2440                Ok(true)
2441            } else {
2442                Ok(false)
2443            }
2444        });
2445
2446        let mut segment = DynSegment::new::<()>();
2447        segment.just(10u32);
2448        segment.just(20u32);
2449        lookup.lookup("+", &mut segment, 2, Span::call_site(), Span::call_site())?;
2450        assert_eq!(segment.call0::<u32>()?, 100);
2451
2452        Ok(())
2453    }
2454
2455    #[test]
2456    fn lookup_cast_errors_rather_than_panics_on_an_empty_stack() -> Result<()> {
2457        // Regression test: `lookup_cast` used to index `peek_stack_infos(1)[0]` unconditionally,
2458        // which panicked (rather than returning a `ParseError`) when the stack was empty -
2459        // reachable directly through this public API without going through the grammar, which
2460        // always pushes the operand first.
2461        let lookup = OpLookup::new();
2462        let mut segment = DynSegment::new::<()>();
2463        let result = lookup.lookup_cast("i32", &mut segment, Span::call_site(), Span::call_site());
2464        assert!(result.is_err());
2465        Ok(())
2466    }
2467
2468    #[test]
2469    fn cast_widens_i32_to_f64_exactly() -> Result<()> {
2470        let lookup = OpLookup::new();
2471        let mut segment = DynSegment::new::<()>();
2472        segment.just(1024i32);
2473        lookup.lookup_cast("f64", &mut segment, Span::call_site(), Span::call_site())?;
2474        assert_eq!(segment.call0::<f64>()?, 1024.0);
2475        Ok(())
2476    }
2477
2478    #[test]
2479    fn cast_narrows_f64_to_i32_when_the_value_fits() -> Result<()> {
2480        let lookup = OpLookup::new();
2481        let mut segment = DynSegment::new::<()>();
2482        segment.just(1024.0f64);
2483        lookup.lookup_cast("i32", &mut segment, Span::call_site(), Span::call_site())?;
2484        assert_eq!(segment.call0::<i32>()?, 1024);
2485        Ok(())
2486    }
2487
2488    #[test]
2489    fn cast_errors_when_the_value_does_not_fit_in_the_target_type() -> Result<()> {
2490        // lookup_cast's own Result only reports type-checking failures (unknown type, no
2491        // registered source) - a checked cast's own out-of-range failure is deferred to
2492        // execution, same as any other fallible op (see DynSegment::op1r's doc comment), so
2493        // it's asserted via call0.
2494        let lookup = OpLookup::new();
2495        let mut segment = DynSegment::new::<()>();
2496        segment.just(1.0e20f64);
2497        lookup.lookup_cast("i32", &mut segment, Span::call_site(), Span::call_site())?;
2498        assert!(segment.call0::<i32>().is_err());
2499        Ok(())
2500    }
2501
2502    #[test]
2503    fn cast_errors_just_past_the_target_types_max_even_when_max_is_not_exactly_representable()
2504    -> Result<()> {
2505        // Regression test: `i32::MAX as f32` isn't exactly representable (f32's 24-bit mantissa
2506        // can't hold all 31 value bits) and rounds *up* to exactly `i32::MAX as i64 + 1`
2507        // (2147483648.0, a power of two). A bound check comparing against that rounded value
2508        // with `>` would let this value silently pass, then Rust's saturating `as` would turn it
2509        // into `i32::MAX` instead of the `Err` a genuinely out-of-range value should produce.
2510        let lookup = OpLookup::new();
2511        let mut segment = DynSegment::new::<()>();
2512        segment.just(2147483648.0f32); // i32::MAX + 1, exactly representable in f32
2513        lookup.lookup_cast("i32", &mut segment, Span::call_site(), Span::call_site())?;
2514        assert!(segment.call0::<i32>().is_err());
2515        Ok(())
2516    }
2517
2518    #[test]
2519    fn cast_accepts_the_target_types_actual_max_value() -> Result<()> {
2520        let lookup = OpLookup::new();
2521        let mut segment = DynSegment::new::<()>();
2522        segment.just(2147483520.0f32); // largest f32 value below i32::MAX + 1
2523        lookup.lookup_cast("i32", &mut segment, Span::call_site(), Span::call_site())?;
2524        assert_eq!(segment.call0::<i32>()?, 2147483520);
2525        Ok(())
2526    }
2527
2528    #[test]
2529    fn cast_errors_just_past_i64_max_from_f64() -> Result<()> {
2530        // Same rounding pitfall as the f32 -> i32 case above, but for the f64 -> i64 pair (f64's
2531        // 53-bit mantissa can't hold i64's 63 value bits either).
2532        let lookup = OpLookup::new();
2533        let mut segment = DynSegment::new::<()>();
2534        segment.just(9223372036854775808.0f64); // i64::MAX + 1, exactly representable in f64
2535        lookup.lookup_cast("i64", &mut segment, Span::call_site(), Span::call_site())?;
2536        assert!(segment.call0::<i64>().is_err());
2537        Ok(())
2538    }
2539
2540    #[test]
2541    fn cast_errors_for_an_unknown_target_type() -> Result<()> {
2542        let lookup = OpLookup::new();
2543        let mut segment = DynSegment::new::<()>();
2544        segment.just(1i32);
2545        let result = lookup.lookup_cast(
2546            "nonsense",
2547            &mut segment,
2548            Span::call_site(),
2549            Span::call_site(),
2550        );
2551        assert!(result.is_err());
2552        Ok(())
2553    }
2554
2555    #[test]
2556    fn cast_errors_for_an_unregistered_source_type() -> Result<()> {
2557        // String -> i32 has no registered cast: Rust's own `as` has no conversion from `String`
2558        // to anything (see the "Casts" section's doc comment).
2559        let lookup = OpLookup::new();
2560        let mut segment = DynSegment::new::<()>();
2561        segment.just("1".to_string());
2562        let result = lookup.lookup_cast("i32", &mut segment, Span::call_site(), Span::call_site());
2563        assert!(result.is_err());
2564        Ok(())
2565    }
2566
2567    #[test]
2568    fn cast_from_bool_is_registered_for_every_integer_width() -> Result<()> {
2569        // Matches Rust's own `bool as <int>`: infallible for all 12 integer widths.
2570        let lookup = OpLookup::new();
2571        for target in [
2572            "u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128", "isize",
2573        ] {
2574            let mut segment = DynSegment::new::<()>();
2575            segment.just(true);
2576            lookup.lookup_cast(target, &mut segment, Span::call_site(), Span::call_site())?;
2577        }
2578        Ok(())
2579    }
2580
2581    #[test]
2582    fn cast_widens_bool_to_a_specific_integer_value() -> Result<()> {
2583        // Matches Rust's own `bool as <int>`: `true` -> 1, `false` -> 0.
2584        let lookup = OpLookup::new();
2585        let mut segment = DynSegment::new::<()>();
2586        segment.just(true);
2587        lookup.lookup_cast("i32", &mut segment, Span::call_site(), Span::call_site())?;
2588        assert_eq!(segment.call0::<i32>()?, 1);
2589
2590        let mut segment = DynSegment::new::<()>();
2591        segment.just(false);
2592        lookup.lookup_cast("u64", &mut segment, Span::call_site(), Span::call_site())?;
2593        assert_eq!(segment.call0::<u64>()?, 0);
2594        Ok(())
2595    }
2596
2597    #[test]
2598    fn cast_bool_to_bool_is_a_no_op_identity_conversion() -> Result<()> {
2599        let lookup = OpLookup::new();
2600        let mut segment = DynSegment::new::<()>();
2601        segment.just(true);
2602        lookup.lookup_cast("bool", &mut segment, Span::call_site(), Span::call_site())?;
2603        assert!(segment.call0::<bool>()?);
2604        Ok(())
2605    }
2606
2607    #[test]
2608    fn cast_string_to_string_is_a_no_op_identity_conversion() -> Result<()> {
2609        let lookup = OpLookup::new();
2610        let mut segment = DynSegment::new::<()>();
2611        segment.just("hello".to_string());
2612        lookup.lookup_cast("String", &mut segment, Span::call_site(), Span::call_site())?;
2613        assert_eq!(segment.call0::<String>()?, "hello");
2614        Ok(())
2615    }
2616
2617    #[test]
2618    fn cast_errors_from_a_number_to_bool() -> Result<()> {
2619        // Rust's own `as` rejects `<int> as bool` (E0054) - no int/float -> bool conversion is
2620        // registered, matching Rust exactly, not just "unknown type".
2621        let lookup = OpLookup::new();
2622        let mut segment = DynSegment::new::<()>();
2623        segment.just(1i32);
2624        let result = lookup.lookup_cast("bool", &mut segment, Span::call_site(), Span::call_site());
2625        assert!(result.is_err());
2626        Ok(())
2627    }
2628
2629    #[test]
2630    fn cast_errors_from_bool_to_a_float() -> Result<()> {
2631        // Rust's own `as` rejects `bool as f64` (E0606: "cast through an integer first").
2632        let lookup = OpLookup::new();
2633        let mut segment = DynSegment::new::<()>();
2634        segment.just(true);
2635        let result = lookup.lookup_cast("f64", &mut segment, Span::call_site(), Span::call_site());
2636        assert!(result.is_err());
2637        Ok(())
2638    }
2639
2640    #[test]
2641    fn cast_errors_between_string_and_bool_in_both_directions() -> Result<()> {
2642        let lookup = OpLookup::new();
2643        let mut segment = DynSegment::new::<()>();
2644        segment.just(true);
2645        assert!(
2646            lookup
2647                .lookup_cast("String", &mut segment, Span::call_site(), Span::call_site())
2648                .is_err()
2649        );
2650
2651        let mut segment = DynSegment::new::<()>();
2652        segment.just("true".to_string());
2653        assert!(
2654            lookup
2655                .lookup_cast("bool", &mut segment, Span::call_site(), Span::call_site())
2656                .is_err()
2657        );
2658        Ok(())
2659    }
2660
2661    #[test]
2662    fn cast_errors_between_string_and_numbers_in_both_directions() -> Result<()> {
2663        let lookup = OpLookup::new();
2664        let mut segment = DynSegment::new::<()>();
2665        segment.just(1i32);
2666        assert!(
2667            lookup
2668                .lookup_cast("String", &mut segment, Span::call_site(), Span::call_site())
2669                .is_err()
2670        );
2671
2672        let mut segment = DynSegment::new::<()>();
2673        segment.just("1".to_string());
2674        assert!(
2675            lookup
2676                .lookup_cast("i32", &mut segment, Span::call_site(), Span::call_site())
2677                .is_err()
2678        );
2679        Ok(())
2680    }
2681
2682    #[test]
2683    fn test_left_shift_u64() -> Result<()> {
2684        let lookup = OpLookup::new();
2685        let mut segment = DynSegment::new::<()>();
2686        segment.just(1u64);
2687        segment.just(3u32);
2688        lookup.lookup("<<", &mut segment, 2, Span::call_site(), Span::call_site())?;
2689        assert_eq!(segment.call0::<u64>()?, 8);
2690        Ok(())
2691    }
2692
2693    #[test]
2694    fn test_right_shift_i32() -> Result<()> {
2695        let lookup = OpLookup::new();
2696        let mut segment = DynSegment::new::<()>();
2697        segment.just(16i32);
2698        segment.just(2u32);
2699        lookup.lookup(">>", &mut segment, 2, Span::call_site(), Span::call_site())?;
2700        assert_eq!(segment.call0::<i32>()?, 4);
2701        Ok(())
2702    }
2703
2704    #[test]
2705    fn test_shift_overflow() -> Result<()> {
2706        let lookup = OpLookup::new();
2707        let mut segment = DynSegment::new::<()>();
2708        segment.just(1u32);
2709        segment.just(32u32);
2710        lookup.lookup("<<", &mut segment, 2, Span::call_site(), Span::call_site())?;
2711        let result = segment.call0::<u32>();
2712        assert!(result.is_err());
2713        let err = result.unwrap_err();
2714        let message = format!("{:#}", err);
2715        assert!(
2716            message.contains("shift overflow"),
2717            "error message should mention shift overflow, got: {message}"
2718        );
2719        Ok(())
2720    }
2721
2722    #[test]
2723    fn test_shift_i32_rhs() -> Result<()> {
2724        let lookup = OpLookup::new();
2725        let mut segment = DynSegment::new::<()>();
2726        segment.just(1u32);
2727        segment.just(3i32);
2728        lookup.lookup("<<", &mut segment, 2, Span::call_site(), Span::call_site())?;
2729        assert_eq!(segment.call0::<u32>()?, 8);
2730        Ok(())
2731    }
2732
2733    #[test]
2734    fn test_shift_negative_rhs_errors() -> Result<()> {
2735        let lookup = OpLookup::new();
2736        let mut segment = DynSegment::new::<()>();
2737        segment.just(1u32);
2738        segment.just(-1i32);
2739        lookup.lookup("<<", &mut segment, 2, Span::call_site(), Span::call_site())?;
2740        let result = segment.call0::<u32>();
2741        assert!(result.is_err());
2742        let err = result.unwrap_err();
2743        let message = format!("{:#}", err);
2744        assert!(message.contains("shift overflow"), "got: {message}");
2745        Ok(())
2746    }
2747
2748    #[test]
2749    fn test_shift_wide_rhs_overflow_errors() -> Result<()> {
2750        let lookup = OpLookup::new();
2751        let mut segment = DynSegment::new::<()>();
2752        segment.just(1u32);
2753        segment.just(u32::MAX as u64 + 1);
2754        lookup.lookup("<<", &mut segment, 2, Span::call_site(), Span::call_site())?;
2755        let result = segment.call0::<u32>();
2756        assert!(result.is_err());
2757        let err = result.unwrap_err();
2758        let message = format!("{:#}", err);
2759        assert!(message.contains("shift overflow"), "got: {message}");
2760        Ok(())
2761    }
2762
2763    #[test]
2764    fn test_shift_rejects_float_rhs() {
2765        let lookup = OpLookup::new();
2766        let mut segment = DynSegment::new::<()>();
2767        segment.just(1u32);
2768        segment.just(3.0f64);
2769        assert!(
2770            lookup
2771                .lookup("<<", &mut segment, 2, Span::call_site(), Span::call_site())
2772                .is_err()
2773        );
2774    }
2775
2776    #[test]
2777    fn test_scope_pop() -> Result<()> {
2778        let mut lookup = OpLookup::new();
2779
2780        lookup.push_scope(|name, segment, num_operands, _span| {
2781            let matches = {
2782                let top = segment.peek_stack_infos(num_operands);
2783                name == "+" && top.len() == 2 && top[0].value_type.type_id() == TypeId::of::<u32>()
2784            };
2785            if matches {
2786                segment.op2(|_a: u32, _b: u32| 100u32)?;
2787                Ok(true)
2788            } else {
2789                Ok(false)
2790            }
2791        });
2792
2793        let mut segment = DynSegment::new::<()>();
2794        segment.just(10u32);
2795        segment.just(20u32);
2796        lookup.lookup("+", &mut segment, 2, Span::call_site(), Span::call_site())?;
2797        assert_eq!(segment.call0::<u32>()?, 100);
2798
2799        lookup.pop_scope();
2800        let mut segment = DynSegment::new::<()>();
2801        segment.just(10u32);
2802        segment.just(20u32);
2803        lookup.lookup("+", &mut segment, 2, Span::call_site(), Span::call_site())?;
2804        assert_eq!(segment.call0::<u32>()?, 30);
2805
2806        Ok(())
2807    }
2808
2809    #[test]
2810    fn lookup_not_found_error_carries_span() {
2811        let lookup = OpLookup::new();
2812        let mut segment = DynSegment::new::<()>();
2813        segment.just(10u32);
2814        segment.just(20.0f64);
2815        let err = lookup
2816            .lookup("+", &mut segment, 2, Span::call_site(), Span::call_site())
2817            .unwrap_err();
2818        assert!(
2819            err.message().starts_with("no operation"),
2820            "expected 'no operation' prefix, got: {}",
2821            err.message()
2822        );
2823        assert!(err.message().contains("`+`"));
2824        assert!(err.message().contains("`u32`"));
2825        assert!(err.message().contains("`f64`"));
2826    }
2827
2828    #[test]
2829    fn lookup_not_found_error_has_range() {
2830        let lookup = OpLookup::new();
2831        let mut segment = DynSegment::new::<()>();
2832        segment.just(10u32);
2833        segment.just(20.0f64);
2834        let err = lookup
2835            .lookup("+", &mut segment, 2, Span::call_site(), Span::call_site())
2836            .unwrap_err();
2837        assert!(
2838            err.end_span().is_some(),
2839            "op-lookup errors should carry an end span"
2840        );
2841    }
2842
2843    /// Verifies that `ScopeFn` closures compile when written with an explicit `SourceSpan`
2844    /// parameter, confirming the type alias signature is correct.
2845    #[cfg(feature = "span-diagnostics")]
2846    #[test]
2847    fn scope_fn_accepts_source_span_parameter() {
2848        let mut lookup = OpLookup::new();
2849        lookup.push_scope(
2850            |name: &str, segment: &mut DynSegment, arity: usize, span: crate::SourceSpan| {
2851                if name != "value" || arity != 0 {
2852                    return Ok(false);
2853                }
2854                assert_eq!(span, crate::SourceSpan::new(1, 4, 1, 9));
2855                segment.just(span.start.column);
2856                Ok(true)
2857            },
2858        );
2859        let span = crate::ExprSpan::for_text("    value");
2860        let mut segment = DynSegment::new::<()>();
2861        lookup
2862            .lookup("value", &mut segment, 0, span.start, span.end)
2863            .unwrap();
2864        assert_eq!(segment.call0::<usize>().unwrap(), 4);
2865    }
2866
2867    /// Verifies that `FormatRustcStyle` on an `anyhow::Error` without a `SpanContext`
2868    /// falls back to the plain error message — the expected behavior for errors from
2869    /// client-added ops that do not attach span context.
2870    #[test]
2871    fn format_rustc_style_falls_back_for_client_added_op_error() {
2872        use crate::FormatRustcStyle;
2873        use annotate_snippets::Renderer;
2874
2875        let mut lookup = OpLookup::new();
2876        lookup.push_scope(|name, segment, arity, _| {
2877            if name != "failure" || arity != 0 {
2878                return Ok(false);
2879            }
2880            segment.just(0i32);
2881            segment
2882                .op1r(|_: i32| -> anyhow::Result<i32> { anyhow::bail!("custom domain error") })?;
2883            Ok(true)
2884        });
2885        let mut segment = crate::CELParser::new(lookup).parse_str("failure").unwrap();
2886        let err = segment.call0::<i32>().unwrap_err();
2887        let output = err.format_rustc_style("unused source", "unused.cel", 1, &Renderer::plain());
2888        assert_eq!(output, "custom domain error");
2889    }
2890
2891    #[cfg(feature = "span-diagnostics")]
2892    #[test]
2893    fn runtime_error_carries_span_context() {
2894        use crate::{CELParser, FormatRustcStyle, SpanContext};
2895        use annotate_snippets::Renderer;
2896
2897        let mut parser = CELParser::new(OpLookup::new());
2898        let source = "1i32 + 2147483647i32"; // i32::MAX + 1 → overflow
2899        let mut segment = parser.parse_str(source).expect("should parse");
2900        let err = segment.call0::<i32>().expect_err("should overflow");
2901        let ctx = err
2902            .downcast_ref::<SpanContext>()
2903            .expect("expected SpanContext on runtime error");
2904        // The span is on line 1 (1-indexed). In test mode, proc_macro2 with
2905        // span-locations assigns spans relative to the parsed string, starting
2906        // at column 0 for the first token on each line.
2907        assert_eq!(ctx.span().start.line, 1);
2908        // End-to-end rendering must mention the error and mark the source location.
2909        let rendered = err.format_rustc_style(source, "test.cel", 1, &Renderer::plain());
2910        assert!(
2911            rendered.contains("arithmetic overflow"),
2912            "expected 'arithmetic overflow' in rendered output, got: {rendered}"
2913        );
2914        assert!(
2915            rendered.contains('^'),
2916            "expected caret marker in rendered output, got: {rendered}"
2917        );
2918    }
2919
2920    #[test]
2921    fn tuple_shaped_signature_matches_and_dispatches() -> Result<()> {
2922        let mut lookup = OpLookup::new();
2923        lookup.register_tuple_op(TupleOpSignature {
2924            name: "greet".to_string(),
2925            shape: vec![TypeId::of::<String>(), TypeId::of::<i32>()],
2926            tuple_operand_index: 0,
2927            operand_type_ids: vec![],
2928            op_fn: |seg, _span| {
2929                seg.tuple_index(1)?;
2930                seg.op1(|_ignored: i32| true)
2931            },
2932        });
2933
2934        let mut segment = DynSegment::new::<()>();
2935        let ambient_start = segment.current_stack_offset();
2936        segment.op0(|| "hi".to_string());
2937        segment.op0(|| 7i32);
2938        segment.make_tuple(2, ambient_start)?;
2939
2940        lookup.lookup(
2941            "greet",
2942            &mut segment,
2943            1,
2944            Span::call_site(),
2945            Span::call_site(),
2946        )?;
2947        assert!(segment.call0::<bool>()?);
2948        Ok(())
2949    }
2950
2951    #[test]
2952    fn tuple_shaped_signature_does_not_match_wrong_shape() {
2953        let mut lookup = OpLookup::new();
2954        lookup.register_tuple_op(TupleOpSignature {
2955            name: "greet".to_string(),
2956            shape: vec![TypeId::of::<String>(), TypeId::of::<i32>()],
2957            tuple_operand_index: 0,
2958            operand_type_ids: vec![],
2959            op_fn: |seg, _span| {
2960                seg.tuple_index(1)?;
2961                seg.op1(|_ignored: i32| true)
2962            },
2963        });
2964
2965        let mut segment = DynSegment::new::<()>();
2966        let ambient_start = segment.current_stack_offset();
2967        segment.op0(|| 1i32);
2968        segment.op0(|| 2i32);
2969        segment.make_tuple(2, ambient_start).unwrap();
2970
2971        let result = lookup.lookup(
2972            "greet",
2973            &mut segment,
2974            1,
2975            Span::call_site(),
2976            Span::call_site(),
2977        );
2978        assert!(
2979            result.is_err(),
2980            "shape (i32, i32) should not match (String, i32)"
2981        );
2982    }
2983
2984    #[test]
2985    fn tuple_shaped_signature_with_empty_shape_does_not_match_non_tuple() {
2986        // Regression test: a 0-element `shape` must only match an actual
2987        // 0-arity tuple, not any non-tuple operand (which also reports an
2988        // empty `associated` list).
2989        let mut lookup = OpLookup::new();
2990        lookup.register_tuple_op(TupleOpSignature {
2991            name: "unit_greet".to_string(),
2992            shape: vec![],
2993            tuple_operand_index: 0,
2994            operand_type_ids: vec![],
2995            op_fn: |seg, _span| seg.op1(|_ignored: i32| true),
2996        });
2997
2998        let mut segment = DynSegment::new::<()>();
2999        segment.op0(|| 42i32);
3000
3001        let result = lookup.lookup(
3002            "unit_greet",
3003            &mut segment,
3004            1,
3005            Span::call_site(),
3006            Span::call_site(),
3007        );
3008        assert!(
3009            result.is_err(),
3010            "empty-shape tuple signature must not match a non-tuple operand"
3011        );
3012    }
3013
3014    #[test]
3015    fn builtin_operand_types_reports_a_binary_arithmetic_overload() {
3016        let sigs = builtin_operand_types("+");
3017        assert!(
3018            sigs.iter().any(|s| s.arity == 2
3019                && s.lhs == TypeId::of::<i32>()
3020                && s.rhs == TypeId::of::<i32>())
3021        );
3022    }
3023
3024    #[test]
3025    fn builtin_operand_types_reports_a_unary_overload() {
3026        let sigs = builtin_operand_types("!");
3027        assert!(
3028            sigs.iter()
3029                .any(|s| s.arity == 1 && s.lhs == TypeId::of::<bool>())
3030        );
3031    }
3032
3033    #[test]
3034    fn builtin_operand_types_includes_unary_negation_but_only_for_signed_and_float_types() {
3035        let sigs = builtin_operand_types("-");
3036        assert!(
3037            sigs.iter()
3038                .any(|s| s.arity == 1 && s.lhs == TypeId::of::<i32>())
3039        );
3040        assert!(
3041            sigs.iter()
3042                .any(|s| s.arity == 2 && s.lhs == TypeId::of::<i32>())
3043        );
3044        assert!(
3045            !sigs
3046                .iter()
3047                .any(|s| s.arity == 1 && s.lhs == TypeId::of::<u32>()),
3048            "unsigned types have no unary negation overload"
3049        );
3050    }
3051
3052    #[test]
3053    fn builtin_operand_types_covers_heterogeneous_shift_signatures() {
3054        let integers = [
3055            TypeId::of::<u8>(),
3056            TypeId::of::<u16>(),
3057            TypeId::of::<u32>(),
3058            TypeId::of::<u64>(),
3059            TypeId::of::<u128>(),
3060            TypeId::of::<usize>(),
3061            TypeId::of::<i8>(),
3062            TypeId::of::<i16>(),
3063            TypeId::of::<i32>(),
3064            TypeId::of::<i64>(),
3065            TypeId::of::<i128>(),
3066            TypeId::of::<isize>(),
3067        ];
3068        for operator in ["<<", ">>"] {
3069            let signatures = builtin_operand_types(operator);
3070            assert_eq!(signatures.len(), integers.len() * integers.len());
3071            for lhs in integers {
3072                for rhs in integers {
3073                    assert_eq!(
3074                        signatures
3075                            .iter()
3076                            .filter(|s| s.arity == 2 && s.lhs == lhs && s.rhs == rhs)
3077                            .count(),
3078                        1
3079                    );
3080                }
3081            }
3082        }
3083    }
3084
3085    #[test]
3086    fn builtin_operand_types_is_empty_for_an_unregistered_name() {
3087        assert!(builtin_operand_types("not_an_operator").is_empty());
3088    }
3089
3090    #[test]
3091    fn builtin_operand_types_is_empty_for_a_runtime_only_tuple_op() {
3092        // Tuple-shaped ops are registered on an OpLookup instance at runtime
3093        // (OpLookup::register_tuple_op), never in the static BUILTINS table this function reads —
3094        // confirming they're invisible here, not an oversight.
3095        assert!(builtin_operand_types("greet").is_empty());
3096    }
3097
3098    #[test]
3099    fn builtin_scalar_type_resolves_every_documented_name() {
3100        for name in BUILTIN_SCALAR_NAMES {
3101            let scalar =
3102                builtin_scalar_type(name).unwrap_or_else(|| panic!("expected `{name}` to resolve"));
3103            assert_eq!(scalar.type_name, *name);
3104        }
3105        assert!(builtin_scalar_type("not_a_type").is_none());
3106    }
3107
3108    #[test]
3109    fn builtin_scalar_type_i32_matches_std_any_type_id() {
3110        let scalar = builtin_scalar_type("i32").unwrap();
3111        assert_eq!((scalar.value_type)().type_id(), TypeId::of::<i32>());
3112        assert_eq!((scalar.value_type)().size(), std::mem::size_of::<i32>());
3113        assert_eq!((scalar.value_type)().align(), std::mem::align_of::<i32>());
3114    }
3115
3116    #[test]
3117    fn builtin_scalar_type_push_arg_declares_a_readable_argument() {
3118        let scalar = builtin_scalar_type("i32").unwrap();
3119        let mut segment = DynSegment::new::<()>();
3120        (scalar.value_type)()
3121            .push_argument(&mut segment, 0)
3122            .unwrap();
3123        let value = 42i32;
3124        let result: i32 = segment.call_dyn(&[&value]).unwrap();
3125        assert_eq!(result, 42);
3126    }
3127
3128    #[test]
3129    fn builtin_generic_type_resolves_every_range_family_member_for_every_numeric_type() {
3130        macro_rules! check {
3131            ($($ty:ty),* $(,)?) => {$(
3132                for (generic, identity, size, align) in [
3133                    ("Range", TypeId::of::<std::ops::Range<$ty>>(), std::mem::size_of::<std::ops::Range<$ty>>(), std::mem::align_of::<std::ops::Range<$ty>>()),
3134                    ("RangeInclusive", TypeId::of::<std::ops::RangeInclusive<$ty>>(), std::mem::size_of::<std::ops::RangeInclusive<$ty>>(), std::mem::align_of::<std::ops::RangeInclusive<$ty>>()),
3135                    ("RangeFrom", TypeId::of::<std::ops::RangeFrom<$ty>>(), std::mem::size_of::<std::ops::RangeFrom<$ty>>(), std::mem::align_of::<std::ops::RangeFrom<$ty>>()),
3136                    ("RangeTo", TypeId::of::<std::ops::RangeTo<$ty>>(), std::mem::size_of::<std::ops::RangeTo<$ty>>(), std::mem::align_of::<std::ops::RangeTo<$ty>>()),
3137                    ("RangeToInclusive", TypeId::of::<std::ops::RangeToInclusive<$ty>>(), std::mem::size_of::<std::ops::RangeToInclusive<$ty>>(), std::mem::align_of::<std::ops::RangeToInclusive<$ty>>()),
3138                ] {
3139                    let resolved = builtin_generic_type(generic, stringify!($ty)).unwrap();
3140                    let descriptor = (resolved.value_type)();
3141                    assert_eq!(resolved.type_name, format!("{generic}({})", stringify!($ty)));
3142                    assert_eq!(descriptor.type_id(), identity);
3143                    assert_eq!(descriptor.size(), size);
3144                    assert_eq!(descriptor.align(), align);
3145                }
3146            )*};
3147        }
3148        check!(
3149            u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64
3150        );
3151    }
3152
3153    #[test]
3154    fn builtin_generic_type_range_inclusive_f64_matches_std_any_type_id() {
3155        let resolved = builtin_generic_type("RangeInclusive", "f64").unwrap();
3156        assert_eq!(
3157            (resolved.value_type)().type_id(),
3158            TypeId::of::<std::ops::RangeInclusive<f64>>()
3159        );
3160        assert_eq!(
3161            (resolved.value_type)().size(),
3162            std::mem::size_of::<std::ops::RangeInclusive<f64>>()
3163        );
3164    }
3165
3166    #[test]
3167    fn builtin_generic_type_is_none_for_unknown_generic_or_argument() {
3168        assert!(builtin_generic_type("NotAGeneric", "f64").is_none());
3169        assert!(builtin_generic_type("Range", "not_a_type").is_none());
3170        assert!(builtin_generic_type("Range", "bool").is_none());
3171        assert!(builtin_generic_type("Range", "String").is_none());
3172    }
3173
3174    #[test]
3175    fn builtin_generic_type_0_resolves_range_full() {
3176        let resolved = builtin_generic_type_0("RangeFull").unwrap();
3177        assert_eq!(resolved.type_name, "RangeFull");
3178        assert_eq!(
3179            (resolved.value_type)().type_id(),
3180            TypeId::of::<std::ops::RangeFull>()
3181        );
3182        assert!(builtin_generic_type_0("NotAGeneric").is_none());
3183    }
3184
3185    #[test]
3186    fn builtin_generic_type_push_arg_declares_a_readable_argument() {
3187        let resolved = builtin_generic_type("RangeInclusive", "i32").unwrap();
3188        let mut segment = DynSegment::new::<()>();
3189        (resolved.value_type)()
3190            .push_argument(&mut segment, 0)
3191            .unwrap();
3192        let value = 1i32..=5i32;
3193        let result: std::ops::RangeInclusive<i32> = segment.call_dyn(&[&value]).unwrap();
3194        assert_eq!(result, 1..=5);
3195    }
3196
3197    #[test]
3198    fn isolate_scopes_removes_pushed_scopes_until_restored() {
3199        let mut lookup = OpLookup::new();
3200        lookup.push_scope(|name, segment, arity, _span| {
3201            if name == "custom" && arity == 0 {
3202                segment.just(1i32);
3203                Ok(true)
3204            } else {
3205                Ok(false)
3206            }
3207        });
3208
3209        let mut segment = DynSegment::new::<()>();
3210        let isolated = lookup.isolate_scopes();
3211        let err = lookup.lookup(
3212            "custom",
3213            &mut segment,
3214            0,
3215            proc_macro2::Span::call_site(),
3216            proc_macro2::Span::call_site(),
3217        );
3218        assert!(
3219            err.is_err(),
3220            "custom scope must not be reachable while isolated"
3221        );
3222
3223        lookup.restore_scopes(isolated);
3224        let mut segment = DynSegment::new::<()>();
3225        lookup
3226            .lookup(
3227                "custom",
3228                &mut segment,
3229                0,
3230                proc_macro2::Span::call_site(),
3231                proc_macro2::Span::call_site(),
3232            )
3233            .unwrap();
3234        assert_eq!(segment.call0::<i32>().unwrap(), 1);
3235    }
3236
3237    #[test]
3238    fn isolate_scopes_leaves_library_scopes_reachable() {
3239        let mut lookup = OpLookup::new();
3240        lookup.push_library_scope(|name, segment, arity, _| {
3241            if name != "persistent" || arity != 0 {
3242                return Ok(false);
3243            }
3244            segment.just(42i32);
3245            Ok(true)
3246        });
3247        let mut segment = DynSegment::new::<()>();
3248        let isolated = lookup.isolate_scopes();
3249        lookup
3250            .lookup(
3251                "persistent",
3252                &mut segment,
3253                0,
3254                proc_macro2::Span::call_site(),
3255                proc_macro2::Span::call_site(),
3256            )
3257            .expect("installed library callbacks survive isolation");
3258        assert_eq!(segment.call0::<i32>().unwrap(), 42);
3259        lookup.restore_scopes(isolated);
3260        let mut segment = DynSegment::new::<()>();
3261        lookup
3262            .lookup(
3263                "persistent",
3264                &mut segment,
3265                0,
3266                Span::call_site(),
3267                Span::call_site(),
3268            )
3269            .unwrap();
3270        assert_eq!(segment.call0::<i32>().unwrap(), 42);
3271    }
3272
3273    #[test]
3274    fn range_i32_constructs_a_range() -> Result<()> {
3275        let lookup = OpLookup::new();
3276        let mut segment = DynSegment::new::<()>();
3277        segment.just(1i32);
3278        segment.just(5i32);
3279        lookup.lookup(
3280            "range",
3281            &mut segment,
3282            2,
3283            Span::call_site(),
3284            Span::call_site(),
3285        )?;
3286        assert_eq!(segment.call0::<std::ops::Range<i32>>()?, 1i32..5i32);
3287        Ok(())
3288    }
3289
3290    #[test]
3291    fn range_f64_constructs_a_range() -> Result<()> {
3292        let lookup = OpLookup::new();
3293        let mut segment = DynSegment::new::<()>();
3294        segment.just(1.5f64);
3295        segment.just(5.5f64);
3296        lookup.lookup(
3297            "range",
3298            &mut segment,
3299            2,
3300            Span::call_site(),
3301            Span::call_site(),
3302        )?;
3303        assert_eq!(segment.call0::<std::ops::Range<f64>>()?, 1.5f64..5.5f64);
3304        Ok(())
3305    }
3306
3307    #[test]
3308    fn range_inclusive_i32_constructs_a_range_inclusive() -> Result<()> {
3309        let lookup = OpLookup::new();
3310        let mut segment = DynSegment::new::<()>();
3311        segment.just(1i32);
3312        segment.just(5i32);
3313        lookup.lookup(
3314            "range_inclusive",
3315            &mut segment,
3316            2,
3317            Span::call_site(),
3318            Span::call_site(),
3319        )?;
3320        assert_eq!(
3321            segment.call0::<std::ops::RangeInclusive<i32>>()?,
3322            1i32..=5i32
3323        );
3324        Ok(())
3325    }
3326
3327    #[test]
3328    fn range_rejects_mismatched_operand_types() {
3329        let lookup = OpLookup::new();
3330        let mut segment = DynSegment::new::<()>();
3331        segment.just(1i32);
3332        segment.just(5.0f64);
3333        let result = lookup.lookup(
3334            "range",
3335            &mut segment,
3336            2,
3337            Span::call_site(),
3338            Span::call_site(),
3339        );
3340        assert!(result.is_err(), "expected a type-mismatch error, got Ok");
3341    }
3342
3343    #[test]
3344    fn range_from_i32_constructs_a_range_from() -> Result<()> {
3345        let lookup = OpLookup::new();
3346        let mut segment = DynSegment::new::<()>();
3347        segment.just(3i32);
3348        lookup.lookup(
3349            "range_from",
3350            &mut segment,
3351            1,
3352            Span::call_site(),
3353            Span::call_site(),
3354        )?;
3355        assert_eq!(segment.call0::<std::ops::RangeFrom<i32>>()?, 3i32..);
3356        Ok(())
3357    }
3358
3359    #[test]
3360    fn range_to_i32_constructs_a_range_to() -> Result<()> {
3361        let lookup = OpLookup::new();
3362        let mut segment = DynSegment::new::<()>();
3363        segment.just(7i32);
3364        lookup.lookup(
3365            "range_to",
3366            &mut segment,
3367            1,
3368            Span::call_site(),
3369            Span::call_site(),
3370        )?;
3371        assert_eq!(segment.call0::<std::ops::RangeTo<i32>>()?, ..7i32);
3372        Ok(())
3373    }
3374
3375    #[test]
3376    fn range_to_inclusive_i32_constructs_a_range_to_inclusive() -> Result<()> {
3377        let lookup = OpLookup::new();
3378        let mut segment = DynSegment::new::<()>();
3379        segment.just(7i32);
3380        lookup.lookup(
3381            "range_to_inclusive",
3382            &mut segment,
3383            1,
3384            Span::call_site(),
3385            Span::call_site(),
3386        )?;
3387        assert_eq!(segment.call0::<std::ops::RangeToInclusive<i32>>()?, ..=7i32);
3388        Ok(())
3389    }
3390
3391    #[test]
3392    fn range_full_constructs_a_range_full() -> Result<()> {
3393        let lookup = OpLookup::new();
3394        let mut segment = DynSegment::new::<()>();
3395        lookup.lookup(
3396            "range_full",
3397            &mut segment,
3398            0,
3399            Span::call_site(),
3400            Span::call_site(),
3401        )?;
3402        segment.call0::<std::ops::RangeFull>()?;
3403        Ok(())
3404    }
3405}