Skip to main content

p3_air/symbolic/
expression_ext.rs

1use alloc::sync::Arc;
2
3use p3_field::extension::{
4    BinomialExtensionField, Complex, CubicTrinomialExtensionField, QuinticTrinomialExtensionField,
5};
6use p3_field::{Algebra, ExtensionField, Field, PrimeCharacteristicRing};
7use serde::{Deserialize, Serialize};
8
9use crate::symbolic::expression::BaseLeaf;
10use crate::symbolic::variable::SymbolicVariableExt;
11use crate::symbolic::{SymLeaf, SymbolicExpr, SymbolicExpression, SymbolicVariable};
12
13/// Leaf nodes for extension-field symbolic expressions.
14///
15/// These represent the atomic building blocks of extension-field AIR constraints:
16/// lifted base-field sub-trees, extension-field variables, and extension-field constants.
17#[derive(Clone, Debug, Serialize, Deserialize)]
18pub enum ExtLeaf<F, EF> {
19    /// A lifted base-field expression (entire base sub-tree preserved).
20    Base(SymbolicExpression<F>),
21
22    /// An extension-field variable (permutation column or challenge).
23    ExtVariable(SymbolicVariableExt<F, EF>),
24
25    /// An extension-field constant.
26    ExtConstant(EF),
27}
28
29/// A symbolic expression tree for extension-field AIR constraints.
30///
31/// This is a type alias for the generic [`SymbolicExpr`] parameterized with
32/// extension-field [`ExtLeaf`] nodes.
33pub type SymbolicExpressionExt<F, EF> = SymbolicExpr<ExtLeaf<F, EF>>;
34
35impl<F: Field, EF: ExtensionField<F>> SymLeaf for ExtLeaf<F, EF> {
36    type F = F;
37
38    const ZERO: Self = Self::Base(SymbolicExpression::ZERO);
39    const ONE: Self = Self::Base(SymbolicExpression::ONE);
40    const TWO: Self = Self::Base(SymbolicExpression::TWO);
41    const NEG_ONE: Self = Self::Base(SymbolicExpression::NEG_ONE);
42
43    fn degree_multiple(&self) -> usize {
44        match self {
45            Self::Base(e) => e.degree_multiple(),
46            Self::ExtVariable(v) => v.degree_multiple(),
47            Self::ExtConstant(_) => 0,
48        }
49    }
50
51    fn degree_multiple_with_transition(&self, transition_degree: usize) -> usize {
52        match self {
53            Self::Base(e) => e.degree_multiple_with_transition(transition_degree),
54            _ => self.degree_multiple(),
55        }
56    }
57
58    fn poly_degree(&self, trace_len: usize, periodic_periods: &[usize]) -> usize {
59        match self {
60            Self::Base(e) => e.poly_degree(trace_len, periodic_periods),
61            Self::ExtVariable(v) => v.poly_degree(trace_len),
62            Self::ExtConstant(_) => 0,
63        }
64    }
65
66    fn as_const(&self) -> Option<&F> {
67        match self {
68            Self::Base(SymbolicExpression::Leaf(BaseLeaf::Constant(c))) => Some(c),
69            Self::ExtConstant(ef) if ef.is_in_basefield() => {
70                Some(&ef.as_basis_coefficients_slice()[0])
71            }
72            _ => None,
73        }
74    }
75
76    fn from_const(c: F) -> Self {
77        Self::Base(SymbolicExpression::from(c))
78    }
79}
80
81impl<F: Field, EF> SymbolicExpressionExt<F, EF> {
82    /// Try to lower this extension expression to a base-field expression.
83    ///
84    /// Returns `None` if the tree contains any extension-only nodes
85    /// ([`ExtVariable`](ExtLeaf::ExtVariable) or [`ExtConstant`](ExtLeaf::ExtConstant)).
86    pub fn to_base(&self) -> Option<SymbolicExpression<F>> {
87        match self {
88            Self::Leaf(ExtLeaf::Base(e)) => Some(e.clone()),
89            Self::Leaf(ExtLeaf::ExtVariable(_) | ExtLeaf::ExtConstant(_)) => None,
90            Self::Add {
91                x,
92                y,
93                degree_multiple,
94            } => Some(SymbolicExpr::Add {
95                x: Arc::new(x.to_base()?),
96                y: Arc::new(y.to_base()?),
97                degree_multiple: *degree_multiple,
98            }),
99            Self::Sub {
100                x,
101                y,
102                degree_multiple,
103            } => Some(SymbolicExpr::Sub {
104                x: Arc::new(x.to_base()?),
105                y: Arc::new(y.to_base()?),
106                degree_multiple: *degree_multiple,
107            }),
108            Self::Neg { x, degree_multiple } => Some(SymbolicExpr::Neg {
109                x: Arc::new(x.to_base()?),
110                degree_multiple: *degree_multiple,
111            }),
112            Self::Mul {
113                x,
114                y,
115                degree_multiple,
116            } => Some(SymbolicExpr::Mul {
117                x: Arc::new(x.to_base()?),
118                y: Arc::new(y.to_base()?),
119                degree_multiple: *degree_multiple,
120            }),
121        }
122    }
123}
124
125impl<F: Field, EF> From<SymbolicExpression<F>> for SymbolicExpressionExt<F, EF> {
126    fn from(expr: SymbolicExpression<F>) -> Self {
127        Self::Leaf(ExtLeaf::Base(expr))
128    }
129}
130
131impl<F: Field, EF> From<SymbolicVariable<F>> for SymbolicExpressionExt<F, EF> {
132    fn from(var: SymbolicVariable<F>) -> Self {
133        Self::Leaf(ExtLeaf::Base(SymbolicExpr::Leaf(BaseLeaf::Variable(var))))
134    }
135}
136
137impl<F, EF> From<SymbolicVariableExt<F, EF>> for SymbolicExpressionExt<F, EF> {
138    fn from(var: SymbolicVariableExt<F, EF>) -> Self {
139        Self::Leaf(ExtLeaf::ExtVariable(var))
140    }
141}
142
143impl<F: Field, EF> From<F> for SymbolicExpressionExt<F, EF> {
144    fn from(f: F) -> Self {
145        Self::Leaf(ExtLeaf::Base(SymbolicExpression::from(f)))
146    }
147}
148
149/// Concrete [`From`] for [`BinomialExtensionField`] constants.
150///
151/// This avoids overlap with [`From<F>`] when `EF = F`, since
152/// [`BinomialExtensionField<F, D>`] is always a distinct type from `F`.
153impl<F, const D: usize> From<BinomialExtensionField<F, D>>
154    for SymbolicExpressionExt<F, BinomialExtensionField<F, D>>
155where
156    F: Field,
157    BinomialExtensionField<F, D>: ExtensionField<F>,
158{
159    fn from(ef: BinomialExtensionField<F, D>) -> Self {
160        Self::Leaf(ExtLeaf::ExtConstant(ef))
161    }
162}
163
164impl<F: Field, EF: ExtensionField<F>> Algebra<F> for SymbolicExpressionExt<F, EF> {}
165
166impl<F: Field, EF: ExtensionField<F>> Algebra<SymbolicExpression<F>>
167    for SymbolicExpressionExt<F, EF>
168{
169}
170
171impl<F: Field, EF: ExtensionField<F>> Algebra<SymbolicVariable<F>>
172    for SymbolicExpressionExt<F, EF>
173{
174}
175
176impl<F: Field, EF: ExtensionField<F>> Algebra<SymbolicVariableExt<F, EF>>
177    for SymbolicExpressionExt<F, EF>
178{
179}
180
181/// Concrete [`Algebra`] for [`BinomialExtensionField`] — avoids overlap with `Algebra<F>` when `EF = F`.
182impl<F: Field, const D: usize> Algebra<BinomialExtensionField<F, D>>
183    for SymbolicExpressionExt<F, BinomialExtensionField<F, D>>
184where
185    BinomialExtensionField<F, D>: ExtensionField<F>,
186{
187}
188
189impl<F: Field> From<CubicTrinomialExtensionField<F>>
190    for SymbolicExpressionExt<F, CubicTrinomialExtensionField<F>>
191where
192    CubicTrinomialExtensionField<F>: ExtensionField<F>,
193{
194    fn from(ef: CubicTrinomialExtensionField<F>) -> Self {
195        Self::Leaf(ExtLeaf::ExtConstant(ef))
196    }
197}
198
199impl<F: Field> From<QuinticTrinomialExtensionField<F>>
200    for SymbolicExpressionExt<F, QuinticTrinomialExtensionField<F>>
201where
202    QuinticTrinomialExtensionField<F>: ExtensionField<F>,
203{
204    fn from(ef: QuinticTrinomialExtensionField<F>) -> Self {
205        Self::Leaf(ExtLeaf::ExtConstant(ef))
206    }
207}
208
209/// Concrete [`From`] for a degree-4 complex tower `BinomialExtensionField<Complex<F>, 2>`.
210///
211/// The symbolic base is `F` while the binomial's base parameter is `Complex<F>`, so
212/// the generic [`BinomialExtensionField<F, D>`] impl above does not cover it.
213impl<F: Field> From<BinomialExtensionField<Complex<F>, 2>>
214    for SymbolicExpressionExt<F, BinomialExtensionField<Complex<F>, 2>>
215where
216    BinomialExtensionField<Complex<F>, 2>: ExtensionField<F>,
217{
218    fn from(ef: BinomialExtensionField<Complex<F>, 2>) -> Self {
219        Self::Leaf(ExtLeaf::ExtConstant(ef))
220    }
221}
222
223/// Concrete [`Algebra`] for [`CubicTrinomialExtensionField`] — avoids overlap with `Algebra<F>`.
224impl<F: Field> Algebra<CubicTrinomialExtensionField<F>>
225    for SymbolicExpressionExt<F, CubicTrinomialExtensionField<F>>
226where
227    CubicTrinomialExtensionField<F>: ExtensionField<F>,
228{
229}
230
231/// Concrete [`Algebra`] for [`QuinticTrinomialExtensionField`] — avoids overlap with `Algebra<F>`.
232impl<F: Field> Algebra<QuinticTrinomialExtensionField<F>>
233    for SymbolicExpressionExt<F, QuinticTrinomialExtensionField<F>>
234where
235    QuinticTrinomialExtensionField<F>: ExtensionField<F>,
236{
237}
238
239/// Concrete [`Algebra`] for a degree-4 complex tower `BinomialExtensionField<Complex<F>, 2>` —
240/// avoids overlap with `Algebra<F>` and with the generic binomial impl, whose base parameter
241/// matches the symbolic base.
242impl<F: Field> Algebra<BinomialExtensionField<Complex<F>, 2>>
243    for SymbolicExpressionExt<F, BinomialExtensionField<Complex<F>, 2>>
244where
245    BinomialExtensionField<Complex<F>, 2>: ExtensionField<F>,
246{
247}
248
249#[cfg(test)]
250mod tests {
251    use p3_baby_bear::BabyBear;
252    use p3_field::extension::BinomialExtensionField;
253    use p3_field::{BasedVectorSpace, PrimeCharacteristicRing};
254    use p3_mersenne_31::{Mersenne31, QM31};
255
256    use super::*;
257    use crate::symbolic::SymbolicExpr;
258    use crate::symbolic::variable::{BaseEntry, ExtEntry};
259
260    type F = BabyBear;
261    type EF = BinomialExtensionField<BabyBear, 4>;
262
263    #[test]
264    fn ext_leaf_degree_multiple_base_variable() {
265        // A base leaf with a trace variable inside has degree 1.
266        let var = SymbolicVariable::<F>::new(BaseEntry::Main { offset: 0 }, 0);
267        let leaf = ExtLeaf::<F, EF>::Base(SymbolicExpression::from(var));
268        assert_eq!(leaf.degree_multiple(), 1);
269    }
270
271    #[test]
272    fn ext_leaf_degree_multiple_base_constant() {
273        // A base leaf with a constant inside has degree 0.
274        let leaf = ExtLeaf::<F, EF>::Base(SymbolicExpression::from(F::new(42)));
275        assert_eq!(leaf.degree_multiple(), 0);
276    }
277
278    #[test]
279    fn ext_leaf_degree_multiple_ext_variable() {
280        // A permutation variable has degree 1.
281        let var = SymbolicVariableExt::<F, EF>::new(ExtEntry::Permutation { offset: 0 }, 0);
282        let leaf = ExtLeaf::ExtVariable(var);
283        assert_eq!(leaf.degree_multiple(), 1);
284    }
285
286    #[test]
287    fn ext_leaf_degree_multiple_ext_variable_challenge() {
288        // A challenge variable has degree 0.
289        let var = SymbolicVariableExt::<F, EF>::new(ExtEntry::Challenge, 0);
290        let leaf = ExtLeaf::ExtVariable(var);
291        assert_eq!(leaf.degree_multiple(), 0);
292    }
293
294    #[test]
295    fn ext_leaf_degree_multiple_ext_constant() {
296        // An extension constant always has degree 0.
297        let leaf = ExtLeaf::<F, EF>::ExtConstant(EF::ONE);
298        assert_eq!(leaf.degree_multiple(), 0);
299    }
300
301    #[test]
302    fn ext_leaf_as_const_base_constant() {
303        // A base constant leaf can be viewed as a field constant.
304        let leaf = ExtLeaf::<F, EF>::Base(SymbolicExpression::from(F::new(7)));
305        assert_eq!(leaf.as_const(), Some(&F::new(7)));
306    }
307
308    #[test]
309    fn ext_leaf_as_const_base_variable() {
310        // A base variable leaf is not a constant.
311        let var = SymbolicVariable::<F>::new(BaseEntry::Main { offset: 0 }, 0);
312        let leaf = ExtLeaf::<F, EF>::Base(SymbolicExpression::from(var));
313        assert!(leaf.as_const().is_none());
314    }
315
316    #[test]
317    fn ext_leaf_as_const_ext_variable() {
318        // An extension variable leaf is not a constant.
319        let var = SymbolicVariableExt::<F, EF>::new(ExtEntry::Permutation { offset: 0 }, 0);
320        let leaf = ExtLeaf::ExtVariable(var);
321        assert!(leaf.as_const().is_none());
322    }
323
324    #[test]
325    fn ext_leaf_as_const_ext_constant_in_basefield() {
326        // An extension constant that lies in the base field is recognized as a constant.
327        let leaf = ExtLeaf::<F, EF>::ExtConstant(EF::ONE);
328        assert_eq!(leaf.as_const(), Some(&F::ONE));
329    }
330
331    #[test]
332    fn ext_leaf_as_const_ext_constant_zero() {
333        // The extension zero element is recognized as the base zero.
334        let leaf = ExtLeaf::<F, EF>::ExtConstant(EF::ZERO);
335        assert_eq!(leaf.as_const(), Some(&F::ZERO));
336    }
337
338    #[test]
339    fn ext_leaf_as_const_ext_constant_not_in_basefield() {
340        // An extension constant with non-zero higher coefficients is not a base constant.
341        let ef_val = EF::from_basis_coefficients_fn(|i| if i == 1 { F::ONE } else { F::ZERO });
342        let leaf = ExtLeaf::<F, EF>::ExtConstant(ef_val);
343        assert!(leaf.as_const().is_none());
344    }
345
346    #[test]
347    fn ext_leaf_from_const() {
348        // Creating a leaf from a base-field value produces a constant.
349        let leaf = ExtLeaf::<F, EF>::from_const(F::new(13));
350        assert_eq!(leaf.as_const(), Some(&F::new(13)));
351    }
352
353    #[test]
354    fn to_base_leaf_base() {
355        // A base-only leaf can be lowered to a base expression.
356        let base_expr = SymbolicExpression::from(F::new(5));
357        let ext_expr = SymbolicExpressionExt::<F, EF>::from(base_expr);
358        let lowered = ext_expr.to_base();
359
360        assert!(lowered.is_some());
361        assert!(matches!(
362            lowered.unwrap(),
363            SymbolicExpr::Leaf(BaseLeaf::Constant(c)) if c == F::new(5)
364        ));
365    }
366
367    #[test]
368    fn to_base_leaf_ext_variable() {
369        // An extension variable cannot be lowered to base.
370        let var = SymbolicVariableExt::<F, EF>::new(ExtEntry::Permutation { offset: 0 }, 0);
371        let ext_expr = SymbolicExpressionExt::<F, EF>::from(var);
372        assert!(ext_expr.to_base().is_none());
373    }
374
375    #[test]
376    fn to_base_leaf_ext_constant() {
377        // An extension constant cannot be lowered to base.
378        let ext_expr = SymbolicExpressionExt::<F, EF>::Leaf(ExtLeaf::ExtConstant(EF::TWO));
379        assert!(ext_expr.to_base().is_none());
380    }
381
382    #[test]
383    fn to_base_add_of_base_exprs() {
384        // A sum of two base-only expressions can be lowered.
385        let a = SymbolicExpressionExt::<F, EF>::from(F::new(3));
386        let b = SymbolicExpressionExt::<F, EF>::from(SymbolicVariable::<F>::new(
387            BaseEntry::Main { offset: 0 },
388            0,
389        ));
390        let sum = a + b;
391        let lowered = sum.to_base();
392
393        match lowered {
394            Some(SymbolicExpr::Add {
395                x,
396                y,
397                degree_multiple,
398            }) => {
399                assert_eq!(degree_multiple, 1);
400                assert!(matches!(
401                    x.as_ref(),
402                    SymbolicExpr::Leaf(BaseLeaf::Constant(c)) if *c == F::new(3)
403                ));
404                assert!(matches!(
405                    y.as_ref(),
406                    SymbolicExpr::Leaf(BaseLeaf::Variable(v))
407                        if v.index == 0 && v.entry == BaseEntry::Main { offset: 0 }
408                ));
409            }
410            _ => panic!("Expected a lowered Add node"),
411        }
412    }
413
414    #[test]
415    fn to_base_add_with_ext_child_returns_none() {
416        // A sum with one extension-only child cannot be lowered.
417        let base = SymbolicExpressionExt::<F, EF>::from(F::new(3));
418        let ext_var = SymbolicExpressionExt::<F, EF>::from(SymbolicVariableExt::<F, EF>::new(
419            ExtEntry::Permutation { offset: 0 },
420            0,
421        ));
422        let sum = base + ext_var;
423        assert!(sum.to_base().is_none());
424    }
425
426    #[test]
427    fn to_base_sub_of_base_exprs() {
428        // A difference of two base-only expressions can be lowered.
429        let a = SymbolicExpressionExt::<F, EF>::from(SymbolicVariable::<F>::new(
430            BaseEntry::Main { offset: 0 },
431            0,
432        ));
433        let b = SymbolicExpressionExt::<F, EF>::from(SymbolicVariable::<F>::new(
434            BaseEntry::Main { offset: 0 },
435            1,
436        ));
437        let diff = a - b;
438        let lowered = diff.to_base();
439
440        match lowered {
441            Some(SymbolicExpr::Sub {
442                x,
443                y,
444                degree_multiple,
445            }) => {
446                assert_eq!(degree_multiple, 1);
447                assert!(matches!(
448                    x.as_ref(),
449                    SymbolicExpr::Leaf(BaseLeaf::Variable(v))
450                        if v.index == 0 && v.entry == BaseEntry::Main { offset: 0 }
451                ));
452                assert!(matches!(
453                    y.as_ref(),
454                    SymbolicExpr::Leaf(BaseLeaf::Variable(v))
455                        if v.index == 1 && v.entry == BaseEntry::Main { offset: 0 }
456                ));
457            }
458            _ => panic!("Expected a lowered Sub node"),
459        }
460    }
461
462    #[test]
463    fn to_base_sub_with_ext_child_returns_none() {
464        // A difference with an extension-only child cannot be lowered.
465        let base = SymbolicExpressionExt::<F, EF>::from(F::new(5));
466        let ext_var = SymbolicExpressionExt::<F, EF>::from(SymbolicVariableExt::<F, EF>::new(
467            ExtEntry::Challenge,
468            0,
469        ));
470        let diff = base - ext_var;
471        assert!(diff.to_base().is_none());
472    }
473
474    #[test]
475    fn to_base_neg_of_base_expr() {
476        // Negation of a base-only expression can be lowered.
477        let var = SymbolicExpressionExt::<F, EF>::from(SymbolicVariable::<F>::new(
478            BaseEntry::Main { offset: 0 },
479            0,
480        ));
481        let neg = -var;
482        let lowered = neg.to_base();
483
484        match lowered {
485            Some(SymbolicExpr::Neg { x, degree_multiple }) => {
486                assert_eq!(degree_multiple, 1);
487                assert!(matches!(
488                    x.as_ref(),
489                    SymbolicExpr::Leaf(BaseLeaf::Variable(v))
490                        if v.index == 0 && v.entry == BaseEntry::Main { offset: 0 }
491                ));
492            }
493            _ => panic!("Expected a lowered Neg node"),
494        }
495    }
496
497    #[test]
498    fn to_base_mul_of_base_exprs() {
499        // A product of two base-only expressions can be lowered.
500        let a = SymbolicExpressionExt::<F, EF>::from(SymbolicVariable::<F>::new(
501            BaseEntry::Main { offset: 0 },
502            0,
503        ));
504        let b = SymbolicExpressionExt::<F, EF>::from(SymbolicVariable::<F>::new(
505            BaseEntry::Main { offset: 0 },
506            1,
507        ));
508        let prod = a * b;
509        let lowered = prod.to_base();
510
511        match lowered {
512            Some(SymbolicExpr::Mul {
513                x,
514                y,
515                degree_multiple,
516            }) => {
517                assert_eq!(degree_multiple, 2);
518                assert!(matches!(
519                    x.as_ref(),
520                    SymbolicExpr::Leaf(BaseLeaf::Variable(v))
521                        if v.index == 0 && v.entry == BaseEntry::Main { offset: 0 }
522                ));
523                assert!(matches!(
524                    y.as_ref(),
525                    SymbolicExpr::Leaf(BaseLeaf::Variable(v))
526                        if v.index == 1 && v.entry == BaseEntry::Main { offset: 0 }
527                ));
528            }
529            _ => panic!("Expected a lowered Mul node"),
530        }
531    }
532
533    #[test]
534    fn to_base_mul_with_ext_child_returns_none() {
535        // A product with an extension-only child cannot be lowered.
536        let base = SymbolicExpressionExt::<F, EF>::from(SymbolicVariable::<F>::new(
537            BaseEntry::Main { offset: 0 },
538            0,
539        ));
540        let ext_var = SymbolicExpressionExt::<F, EF>::from(SymbolicVariableExt::<F, EF>::new(
541            ExtEntry::Permutation { offset: 0 },
542            0,
543        ));
544        let prod = base * ext_var;
545        assert!(prod.to_base().is_none());
546    }
547
548    #[test]
549    fn from_symbolic_expression() {
550        // Converting a base expression lifts it into a base leaf.
551        let base_expr = SymbolicExpression::from(F::new(99));
552        let ext_expr = SymbolicExpressionExt::<F, EF>::from(base_expr);
553        assert!(matches!(
554            ext_expr,
555            SymbolicExpr::Leaf(ExtLeaf::Base(SymbolicExpr::Leaf(BaseLeaf::Constant(c)))) if c == F::new(99)
556        ));
557    }
558
559    #[test]
560    fn from_symbolic_variable() {
561        // Converting a base variable lifts it into a base leaf.
562        let var = SymbolicVariable::<F>::new(BaseEntry::Main { offset: 0 }, 2);
563        let ext_expr = SymbolicExpressionExt::<F, EF>::from(var);
564        assert!(matches!(
565            ext_expr,
566            SymbolicExpr::Leaf(ExtLeaf::Base(SymbolicExpr::Leaf(BaseLeaf::Variable(v))))
567                if v.index == 2 && v.entry == BaseEntry::Main { offset: 0 }
568        ));
569    }
570
571    #[test]
572    fn from_symbolic_variable_ext() {
573        // Converting an extension variable produces an extension variable leaf.
574        let var = SymbolicVariableExt::<F, EF>::new(ExtEntry::Permutation { offset: 1 }, 3);
575        let ext_expr = SymbolicExpressionExt::<F, EF>::from(var);
576        assert!(matches!(
577            ext_expr,
578            SymbolicExpr::Leaf(ExtLeaf::ExtVariable(v))
579                if v.index == 3 && v.entry == ExtEntry::Permutation { offset: 1 }
580        ));
581    }
582
583    #[test]
584    fn from_base_field() {
585        // Converting a base field element produces a base constant leaf.
586        let ext_expr = SymbolicExpressionExt::<F, EF>::from(F::new(42));
587        assert!(matches!(
588            ext_expr,
589            SymbolicExpr::Leaf(ExtLeaf::Base(SymbolicExpr::Leaf(BaseLeaf::Constant(c)))) if c == F::new(42)
590        ));
591    }
592
593    #[test]
594    fn from_binomial_extension_field() {
595        // Converting an extension field element produces an extension constant leaf.
596        let ef_val = EF::ONE + EF::ONE;
597        let ext_expr = SymbolicExpressionExt::<F, EF>::from(ef_val);
598        assert!(matches!(
599            ext_expr,
600            SymbolicExpr::Leaf(ExtLeaf::ExtConstant(c)) if c == ef_val
601        ));
602    }
603
604    #[test]
605    fn ext_add_constant_folding() {
606        // Two base constants fold into one on addition.
607        let a = SymbolicExpressionExt::<F, EF>::from(F::new(3));
608        let b = SymbolicExpressionExt::<F, EF>::from(F::new(4));
609        let result = a + b;
610        assert!(matches!(
611            result,
612            SymbolicExpr::Leaf(ExtLeaf::Base(SymbolicExpr::Leaf(BaseLeaf::Constant(c)))) if c == F::new(7)
613        ));
614    }
615
616    #[test]
617    fn ext_sub_constant_folding() {
618        // Two base constants fold into one on subtraction.
619        let a = SymbolicExpressionExt::<F, EF>::from(F::new(10));
620        let b = SymbolicExpressionExt::<F, EF>::from(F::new(4));
621        let result = a - b;
622        assert!(matches!(
623            result,
624            SymbolicExpr::Leaf(ExtLeaf::Base(SymbolicExpr::Leaf(BaseLeaf::Constant(c)))) if c == F::new(6)
625        ));
626    }
627
628    #[test]
629    fn ext_mul_constant_folding() {
630        // Two base constants fold into one on multiplication.
631        let a = SymbolicExpressionExt::<F, EF>::from(F::new(3));
632        let b = SymbolicExpressionExt::<F, EF>::from(F::new(5));
633        let result = a * b;
634        assert!(matches!(
635            result,
636            SymbolicExpr::Leaf(ExtLeaf::Base(SymbolicExpr::Leaf(BaseLeaf::Constant(c)))) if c == F::new(15)
637        ));
638    }
639
640    #[test]
641    fn ext_add_variables_degree_tracking() {
642        // Adding two degree-1 variables gives degree 1 (the max).
643        let a = SymbolicExpressionExt::<F, EF>::from(SymbolicVariableExt::<F, EF>::new(
644            ExtEntry::Permutation { offset: 0 },
645            0,
646        ));
647        let b = SymbolicExpressionExt::<F, EF>::from(SymbolicVariableExt::<F, EF>::new(
648            ExtEntry::Permutation { offset: 0 },
649            1,
650        ));
651        let result = a + b;
652
653        match result {
654            SymbolicExpr::Add {
655                x,
656                y,
657                degree_multiple,
658            } => {
659                assert_eq!(degree_multiple, 1);
660                assert!(matches!(
661                    x.as_ref(),
662                    SymbolicExpr::Leaf(ExtLeaf::ExtVariable(v))
663                        if v.index == 0 && v.entry == ExtEntry::Permutation { offset: 0 }
664                ));
665                assert!(matches!(
666                    y.as_ref(),
667                    SymbolicExpr::Leaf(ExtLeaf::ExtVariable(v))
668                        if v.index == 1 && v.entry == ExtEntry::Permutation { offset: 0 }
669                ));
670            }
671            _ => panic!("Expected an Add node"),
672        }
673    }
674
675    #[test]
676    fn ext_mul_variables_degree_tracking() {
677        // Multiplying two degree-1 variables gives degree 2 (the sum).
678        let a = SymbolicExpressionExt::<F, EF>::from(SymbolicVariableExt::<F, EF>::new(
679            ExtEntry::Permutation { offset: 0 },
680            0,
681        ));
682        let b = SymbolicExpressionExt::<F, EF>::from(SymbolicVariableExt::<F, EF>::new(
683            ExtEntry::Permutation { offset: 0 },
684            1,
685        ));
686        let result = a * b;
687
688        match result {
689            SymbolicExpr::Mul {
690                x,
691                y,
692                degree_multiple,
693            } => {
694                assert_eq!(degree_multiple, 2);
695                assert!(matches!(
696                    x.as_ref(),
697                    SymbolicExpr::Leaf(ExtLeaf::ExtVariable(v))
698                        if v.index == 0 && v.entry == ExtEntry::Permutation { offset: 0 }
699                ));
700                assert!(matches!(
701                    y.as_ref(),
702                    SymbolicExpr::Leaf(ExtLeaf::ExtVariable(v))
703                        if v.index == 1 && v.entry == ExtEntry::Permutation { offset: 0 }
704                ));
705            }
706            _ => panic!("Expected a Mul node"),
707        }
708    }
709
710    #[test]
711    fn ext_constant_zero_mul_folds_to_zero() {
712        // Multiplying by the extension zero folds to the zero constant.
713        let var = SymbolicExpressionExt::<F, EF>::from(SymbolicVariableExt::<F, EF>::new(
714            ExtEntry::Permutation { offset: 0 },
715            0,
716        ));
717        let zero = SymbolicExpressionExt::<F, EF>::from(EF::ZERO);
718        let result = var * zero;
719        assert!(matches!(
720            result,
721            SymbolicExpr::Leaf(ExtLeaf::Base(SymbolicExpr::Leaf(BaseLeaf::Constant(c)))) if c == F::ZERO
722        ));
723    }
724
725    #[test]
726    fn ext_constant_one_mul_folds_to_identity() {
727        // Multiplying by the extension one folds to the other operand.
728        let var = SymbolicExpressionExt::<F, EF>::from(SymbolicVariableExt::<F, EF>::new(
729            ExtEntry::Permutation { offset: 0 },
730            0,
731        ));
732        let one = SymbolicExpressionExt::<F, EF>::from(EF::ONE);
733        let result = var * one;
734        assert!(matches!(
735            result,
736            SymbolicExpr::Leaf(ExtLeaf::ExtVariable(v))
737                if v.index == 0 && v.entry == ExtEntry::Permutation { offset: 0 }
738        ));
739    }
740
741    #[test]
742    fn ext_constant_zero_add_folds_to_identity() {
743        // Adding the extension zero folds to the other operand.
744        let var = SymbolicExpressionExt::<F, EF>::from(SymbolicVariableExt::<F, EF>::new(
745            ExtEntry::Permutation { offset: 0 },
746            0,
747        ));
748        let zero = SymbolicExpressionExt::<F, EF>::from(EF::ZERO);
749        let result = zero + var;
750        assert!(matches!(
751            result,
752            SymbolicExpr::Leaf(ExtLeaf::ExtVariable(v))
753                if v.index == 0 && v.entry == ExtEntry::Permutation { offset: 0 }
754        ));
755    }
756
757    #[test]
758    fn ext_constant_zero_sub_folds_to_neg() {
759        // Subtracting from the extension zero folds to negation.
760        let var = SymbolicExpressionExt::<F, EF>::from(SymbolicVariableExt::<F, EF>::new(
761            ExtEntry::Permutation { offset: 0 },
762            0,
763        ));
764        let zero = SymbolicExpressionExt::<F, EF>::from(EF::ZERO);
765        let result = zero - var;
766        match result {
767            SymbolicExpr::Neg { x, degree_multiple } => {
768                assert_eq!(degree_multiple, 1);
769                assert!(matches!(
770                    x.as_ref(),
771                    SymbolicExpr::Leaf(ExtLeaf::ExtVariable(v))
772                        if v.index == 0 && v.entry == ExtEntry::Permutation { offset: 0 }
773                ));
774            }
775            _ => panic!("Expected a Neg node"),
776        }
777    }
778
779    #[test]
780    fn ext_constant_not_in_basefield_no_folding() {
781        // A non-base-field extension constant does not fold with multiplication.
782        let var = SymbolicExpressionExt::<F, EF>::from(SymbolicVariableExt::<F, EF>::new(
783            ExtEntry::Permutation { offset: 0 },
784            0,
785        ));
786        let non_base = SymbolicExpressionExt::<F, EF>::from(EF::from_basis_coefficients_fn(|i| {
787            if i == 1 { F::ONE } else { F::ZERO }
788        }));
789        let result = var * non_base;
790        match result {
791            SymbolicExpr::Mul {
792                x,
793                y,
794                degree_multiple,
795            } => {
796                assert_eq!(degree_multiple, 1);
797                assert!(matches!(
798                    x.as_ref(),
799                    SymbolicExpr::Leaf(ExtLeaf::ExtVariable(v))
800                        if v.index == 0 && v.entry == ExtEntry::Permutation { offset: 0 }
801                ));
802                assert!(matches!(
803                    y.as_ref(),
804                    SymbolicExpr::Leaf(ExtLeaf::ExtConstant(_))
805                ));
806            }
807            _ => panic!("Expected a Mul node since the constant is not in the base field"),
808        }
809    }
810
811    #[test]
812    fn ext_mixed_base_and_ext_arithmetic() {
813        // Mixing a base variable with an extension variable in a sum.
814        let base_var = SymbolicExpressionExt::<F, EF>::from(SymbolicVariable::<F>::new(
815            BaseEntry::Main { offset: 0 },
816            0,
817        ));
818        let ext_var = SymbolicExpressionExt::<F, EF>::from(SymbolicVariableExt::<F, EF>::new(
819            ExtEntry::Permutation { offset: 0 },
820            0,
821        ));
822        let result = base_var + ext_var;
823
824        match &result {
825            SymbolicExpr::Add {
826                x,
827                y,
828                degree_multiple,
829            } => {
830                assert_eq!(*degree_multiple, 1);
831                assert!(matches!(
832                    x.as_ref(),
833                    SymbolicExpr::Leaf(ExtLeaf::Base(SymbolicExpr::Leaf(BaseLeaf::Variable(v))))
834                        if v.index == 0 && v.entry == BaseEntry::Main { offset: 0 }
835                ));
836                assert!(matches!(
837                    y.as_ref(),
838                    SymbolicExpr::Leaf(ExtLeaf::ExtVariable(v))
839                        if v.index == 0 && v.entry == ExtEntry::Permutation { offset: 0 }
840                ));
841            }
842            _ => panic!("Expected an Add node"),
843        }
844
845        // The mixed result cannot be lowered to base.
846        assert!(result.to_base().is_none());
847    }
848
849    #[test]
850    fn serde_round_trip_preserves_extension_constraint() {
851        // A constraint over all extension leaf kinds and a lifted base sub-tree:
852        //   perm[0]·challenge - ext_const + base_var
853        let perm = SymbolicExpressionExt::<F, EF>::from(SymbolicVariableExt::<F, EF>::new(
854            ExtEntry::Permutation { offset: 0 },
855            0,
856        ));
857        let challenge = SymbolicExpressionExt::<F, EF>::from(SymbolicVariableExt::<F, EF>::new(
858            ExtEntry::Challenge,
859            0,
860        ));
861        let ext_const = SymbolicExpressionExt::<F, EF>::from(EF::from_basis_coefficients_fn(|i| {
862            if i == 1 { F::ONE } else { F::ZERO }
863        }));
864        let base_var = SymbolicExpressionExt::<F, EF>::from(SymbolicVariable::<F>::new(
865            BaseEntry::Main { offset: 0 },
866            0,
867        ));
868
869        let expr = perm * challenge - ext_const + base_var;
870
871        let json = serde_json::to_string(&expr).unwrap();
872        let decoded: SymbolicExpressionExt<F, EF> = serde_json::from_str(&json).unwrap();
873
874        // Structural equality: the decoded tree re-serializes identically.
875        assert_eq!(serde_json::to_string(&decoded).unwrap(), json);
876        assert_eq!(decoded.degree_multiple(), expr.degree_multiple());
877    }
878
879    #[test]
880    fn complex_tower_extension_constant_lowers_to_leaf() {
881        // `QM31 = BinomialExtensionField<Complex<Mersenne31>, 2>` is a degree-4 tower whose
882        // binomial base parameter (`Complex<Mersenne31>`) differs from the symbolic base
883        // (`Mersenne31`), so it needs the dedicated complex-tower impls.
884        fn assert_algebra<A: Algebra<B>, B>() {}
885        assert_algebra::<SymbolicExpressionExt<Mersenne31, QM31>, QM31>();
886
887        let expr = SymbolicExpressionExt::<Mersenne31, QM31>::from(QM31::ONE);
888        match expr {
889            SymbolicExpressionExt::Leaf(ExtLeaf::ExtConstant(c)) => assert_eq!(c, QM31::ONE),
890            _ => panic!("Expected an ExtConstant leaf"),
891        }
892    }
893}