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