Skip to main content

radixdb_executor/expression/
program.rs

1// Copyright 2026 RadixDB Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Compiled Expression Program
16//
17// A Program is the compiled form of an AST Expression.
18// It contains:
19// - A sequence of operations (the "bytecode")
20// - Metadata for efficient execution
21
22use rustc_hash::FxHashSet;
23
24use super::ops::Op;
25use radixdb_core::CompactArc;
26use radixdb_core::{Error, Result, Value};
27
28/// Constant value stored in the program
29#[derive(Debug, Clone)]
30pub enum Constant {
31    /// A literal value
32    Value(Value),
33    /// A string (for patterns, column names, etc.)
34    String(CompactArc<str>),
35}
36
37/// Compiled expression program
38///
39/// This is the executable form of an expression. It's designed to be:
40/// - Cheap to clone (uses Arc internally for large data)
41/// - Fast to execute (linear operation sequence)
42/// - Self-contained (no external lookups needed)
43#[derive(Clone)]
44pub struct Program {
45    /// The operation sequence
46    ops: Vec<Op>,
47
48    /// Maximum stack depth needed (for pre-allocation)
49    max_stack_depth: usize,
50
51    /// Whether this program needs outer row context (correlated subquery)
52    needs_outer_context: bool,
53
54    /// Whether this program needs second row (join evaluation)
55    needs_second_row: bool,
56
57    /// Source expression string (for debugging)
58    #[cfg(debug_assertions)]
59    source: Option<String>,
60}
61
62impl Program {
63    /// Validate and create a program from a public bytecode sequence.
64    pub fn try_new(ops: Vec<Op>) -> Result<Self> {
65        Self::validate(&ops)?;
66        let program = Self::new(ops);
67        Self::validate(&program.ops)?;
68        Ok(program)
69    }
70
71    /// Validate and create an unoptimized program.
72    pub fn try_new_unoptimized(ops: Vec<Op>) -> Result<Self> {
73        Self::validate(&ops)?;
74        Ok(Self::new_unoptimized(ops))
75    }
76
77    /// Create a new program from operations.
78    /// Automatically applies peephole optimizations (instruction fusion).
79    pub(crate) fn new(ops: Vec<Op>) -> Self {
80        // Apply peephole optimizations
81        let ops = Self::peephole_optimize(ops);
82
83        let max_stack_depth = Self::compute_stack_depth(&ops);
84        let needs_outer_context = ops.iter().any(|op| matches!(op, Op::LoadOuterColumn(_)));
85        let needs_second_row = ops.iter().any(|op| matches!(op, Op::LoadColumn2(_)));
86        Self {
87            ops,
88            max_stack_depth,
89            needs_outer_context,
90            needs_second_row,
91            #[cfg(debug_assertions)]
92            source: None,
93        }
94    }
95
96    /// Create a new program without peephole optimization.
97    /// Use this for testing or when optimization is not desired.
98    pub(crate) fn new_unoptimized(ops: Vec<Op>) -> Self {
99        let max_stack_depth = Self::compute_stack_depth(&ops);
100        let needs_outer_context = ops.iter().any(|op| matches!(op, Op::LoadOuterColumn(_)));
101        let needs_second_row = ops.iter().any(|op| matches!(op, Op::LoadColumn2(_)));
102        Self {
103            ops,
104            max_stack_depth,
105            needs_outer_context,
106            needs_second_row,
107            #[cfg(debug_assertions)]
108            source: None,
109        }
110    }
111
112    /// Create an empty program that returns NULL
113    pub fn null() -> Self {
114        Self::new(vec![Op::LoadNull(radixdb_core::DataType::Null), Op::Return])
115    }
116
117    /// Create a program that returns a constant value
118    pub fn constant(value: Value) -> Self {
119        Self::new(vec![Op::LoadConst(value), Op::Return])
120    }
121
122    /// Create a program that returns true
123    pub fn always_true() -> Self {
124        Self::new(vec![Op::ReturnTrue])
125    }
126
127    /// Create a program that returns false
128    pub fn always_false() -> Self {
129        Self::new(vec![Op::ReturnFalse])
130    }
131
132    /// Get the operations
133    #[inline]
134    pub fn ops(&self) -> &[Op] {
135        &self.ops
136    }
137
138    /// Get the maximum stack depth needed
139    #[inline]
140    pub fn max_stack_depth(&self) -> usize {
141        self.max_stack_depth
142    }
143
144    /// Check if this program needs outer row context
145    #[inline]
146    pub fn needs_outer_context(&self) -> bool {
147        self.needs_outer_context
148    }
149
150    /// Check if this program needs a second row (for joins)
151    #[inline]
152    pub fn needs_second_row(&self) -> bool {
153        self.needs_second_row
154    }
155
156    /// Get the number of operations
157    #[inline]
158    pub fn len(&self) -> usize {
159        self.ops.len()
160    }
161
162    /// Check if program is empty
163    #[inline]
164    pub fn is_empty(&self) -> bool {
165        self.ops.is_empty()
166    }
167
168    fn validate(ops: &[Op]) -> Result<()> {
169        use std::collections::VecDeque;
170
171        if ops.is_empty() {
172            return Err(Error::invalid_argument("expression program is empty"));
173        }
174        if ops.len() > u16::MAX as usize {
175            return Err(Error::invalid_argument(
176                "expression program exceeds the u16 instruction limit",
177            ));
178        }
179
180        let mut depths = vec![None; ops.len()];
181        let mut queue = VecDeque::from([(0usize, 0usize)]);
182        while let Some((pc, depth)) = queue.pop_front() {
183            if pc >= ops.len() {
184                return Err(Error::invalid_argument(
185                    "expression program falls through without a return",
186                ));
187            }
188            if let Some(existing) = depths[pc] {
189                if existing != depth {
190                    return Err(Error::invalid_argument(format!(
191                        "expression program reaches instruction {pc} with incompatible stack depths {existing} and {depth}"
192                    )));
193                }
194                continue;
195            }
196            depths[pc] = Some(depth);
197
198            let (required, pushed) = Self::stack_contract(&ops[pc]);
199            if depth < required {
200                return Err(Error::invalid_argument(format!(
201                    "expression program stack underflow at instruction {pc}"
202                )));
203            }
204            let next_depth = depth - required + pushed;
205
206            let mut enqueue_target = |target: u16| -> Result<()> {
207                let target = target as usize;
208                if target <= pc || target >= ops.len() {
209                    return Err(Error::invalid_argument(format!(
210                        "expression program has invalid or backward jump from {pc} to {target}"
211                    )));
212                }
213                queue.push_back((target, next_depth));
214                Ok(())
215            };
216
217            match &ops[pc] {
218                Op::Return | Op::ReturnTrue | Op::ReturnFalse | Op::ReturnNull(_) => {}
219                Op::Jump(target) | Op::CaseThen(target) => enqueue_target(*target)?,
220                Op::And(target)
221                | Op::Or(target)
222                | Op::JumpIfTrue(target)
223                | Op::JumpIfFalse(target)
224                | Op::JumpIfNull(target)
225                | Op::JumpIfNotNull(target)
226                | Op::PopJumpIfTrue(target)
227                | Op::PopJumpIfFalse(target)
228                | Op::CaseWhen(target) => {
229                    enqueue_target(*target)?;
230                    queue.push_back((pc + 1, next_depth));
231                }
232                _ => queue.push_back((pc + 1, next_depth)),
233            }
234        }
235        Ok(())
236    }
237
238    /// Number of values consumed and produced by one instruction.
239    fn stack_contract(op: &Op) -> (usize, usize) {
240        match op {
241            Op::LoadColumn(_)
242            | Op::LoadColumn2(_)
243            | Op::LoadOuterColumn(_)
244            | Op::LoadConst(_)
245            | Op::LoadParam(_)
246            | Op::LoadNamedParam(_)
247            | Op::LoadNull(_)
248            | Op::LoadAggregateResult(_)
249            | Op::LoadTransactionId
250            | Op::EqColumnConst(_, _)
251            | Op::NeColumnConst(_, _)
252            | Op::LtColumnConst(_, _)
253            | Op::LeColumnConst(_, _)
254            | Op::GtColumnConst(_, _)
255            | Op::GeColumnConst(_, _)
256            | Op::IsNullColumn(_)
257            | Op::IsNotNullColumn(_)
258            | Op::LikeColumn(_, _, _)
259            | Op::InSetColumn(_, _, _)
260            | Op::BetweenColumnConst(_, _, _) => (0, 1),
261            Op::Dup => (1, 2),
262            Op::Eq
263            | Op::Ne
264            | Op::Lt
265            | Op::Le
266            | Op::Gt
267            | Op::Ge
268            | Op::IsDistinctFrom
269            | Op::IsNotDistinctFrom
270            | Op::AndFinalize
271            | Op::OrFinalize
272            | Op::Add
273            | Op::Sub
274            | Op::Mul
275            | Op::Div
276            | Op::Mod
277            | Op::BitAnd
278            | Op::BitOr
279            | Op::BitXor
280            | Op::Shl
281            | Op::Shr
282            | Op::Concat
283            | Op::Xor
284            | Op::NullIf
285            | Op::LikeDynamic(_)
286            | Op::LikeDynamicEscape(_, _)
287            | Op::GlobDynamic
288            | Op::RegexpDynamic
289            | Op::JsonAccess
290            | Op::JsonAccessText
291            | Op::TimestampAddInterval
292            | Op::TimestampSubInterval
293            | Op::TimestampDiff
294            | Op::TimestampAddDays
295            | Op::TimestampSubDays
296            | Op::VectorDistanceL2
297            | Op::VectorDistanceCosine
298            | Op::VectorDistanceIP => (2, 1),
299            Op::Between | Op::NotBetween => (3, 1),
300            Op::IsNull
301            | Op::IsNotNull
302            | Op::IsTrue
303            | Op::IsNotTrue
304            | Op::IsFalse
305            | Op::IsNotFalse
306            | Op::Not
307            | Op::Neg
308            | Op::BitNot
309            | Op::Like(_, _)
310            | Op::LikeEscape(_, _, _)
311            | Op::Glob(_)
312            | Op::Regexp(_)
313            | Op::InSet(_, _)
314            | Op::NotInSet(_, _)
315            | Op::Cast(_)
316            | Op::CastExternal(_)
317            | Op::TruncateToDate
318            | Op::NativeFn1(_) => (1, 1),
319            Op::InTupleSet { tuple_size, .. } => (*tuple_size as usize, 1),
320            Op::CallScalar { arg_count, .. } | Op::CallStored { arg_count, .. } => {
321                (*arg_count as usize, 1)
322            }
323            Op::Coalesce(n) | Op::Greatest(n) | Op::Least(n) | Op::ConcatN(n) => (*n as usize, 1),
324            Op::Pop | Op::PopJumpIfTrue(_) | Op::PopJumpIfFalse(_) | Op::CaseWhen(_) => (1, 0),
325            Op::Swap => (2, 2),
326            Op::And(_)
327            | Op::Or(_)
328            | Op::JumpIfTrue(_)
329            | Op::JumpIfFalse(_)
330            | Op::JumpIfNull(_)
331            | Op::JumpIfNotNull(_)
332            | Op::CaseThen(_) => (1, 1),
333            Op::CaseCompare => (2, 1),
334            Op::Return => (1, 0),
335            Op::Jump(_)
336            | Op::Nop
337            | Op::CaseStart
338            | Op::CaseElse
339            | Op::CaseEnd
340            | Op::ReturnTrue
341            | Op::ReturnFalse
342            | Op::ReturnNull(_) => (0, 0),
343        }
344    }
345
346    /// Set source string for debugging
347    #[cfg(debug_assertions)]
348    pub fn with_source(mut self, source: String) -> Self {
349        self.source = Some(source);
350        self
351    }
352
353    /// Get source string
354    #[cfg(debug_assertions)]
355    pub fn source(&self) -> Option<&str> {
356        self.source.as_deref()
357    }
358
359    /// Compute the maximum stack depth needed for a sequence of operations
360    fn compute_stack_depth(ops: &[Op]) -> usize {
361        let mut depth: i32 = 0;
362        let mut max_depth: i32 = 0;
363
364        for op in ops {
365            // Calculate stack effect of each operation
366            let effect = match op {
367                // Push operations (+1)
368                Op::LoadColumn(_)
369                | Op::LoadColumn2(_)
370                | Op::LoadOuterColumn(_)
371                | Op::LoadConst(_)
372                | Op::LoadParam(_)
373                | Op::LoadNamedParam(_)
374                | Op::LoadNull(_)
375                | Op::LoadAggregateResult(_)
376                | Op::LoadTransactionId
377                | Op::Dup
378                // Fused compare ops: push 1 (load + compare in single op)
379                | Op::EqColumnConst(_, _)
380                | Op::NeColumnConst(_, _)
381                | Op::LtColumnConst(_, _)
382                | Op::LeColumnConst(_, _)
383                | Op::GtColumnConst(_, _)
384                | Op::GeColumnConst(_, _)
385                // More fused ops: push 1
386                | Op::IsNullColumn(_)
387                | Op::IsNotNullColumn(_)
388                | Op::LikeColumn(_, _, _)
389                | Op::InSetColumn(_, _, _)
390                | Op::BetweenColumnConst(_, _, _) => 1,
391
392                // Pop 2, push 1 (-1)
393                Op::Eq
394                | Op::Ne
395                | Op::Lt
396                | Op::Le
397                | Op::Gt
398                | Op::Ge
399                | Op::IsDistinctFrom
400                | Op::IsNotDistinctFrom
401                | Op::AndFinalize
402                | Op::OrFinalize
403                | Op::Add
404                | Op::Sub
405                | Op::Mul
406                | Op::Div
407                | Op::Mod
408                | Op::BitAnd
409                | Op::BitOr
410                | Op::BitXor
411                | Op::Shl
412                | Op::Shr
413                | Op::Concat
414                | Op::Xor
415                | Op::NullIf
416                | Op::CaseCompare => -1,
417
418                // Pop 3, push 1 (-2)
419                Op::Between | Op::NotBetween => -2,
420
421                // Dynamic pattern ops: Pop 2, push 1 (-1)
422                Op::LikeDynamic(_)
423                | Op::LikeDynamicEscape(_, _)
424                | Op::GlobDynamic
425                | Op::RegexpDynamic
426                // JSON/Timestamp binary ops: Pop 2, push 1 (-1)
427                | Op::JsonAccess
428                | Op::JsonAccessText
429                | Op::TimestampAddInterval
430                | Op::TimestampSubInterval
431                | Op::TimestampDiff
432                | Op::TimestampAddDays
433                | Op::TimestampSubDays
434                | Op::VectorDistanceL2
435                | Op::VectorDistanceCosine
436                | Op::VectorDistanceIP => -1,
437
438                // Transform (0) - pop 1, push 1
439                Op::IsNull
440                | Op::IsNotNull
441                | Op::IsTrue
442                | Op::IsNotTrue
443                | Op::IsFalse
444                | Op::IsNotFalse
445                | Op::Not
446                | Op::Neg
447                | Op::BitNot
448                | Op::Like(_, _)
449                | Op::LikeEscape(_, _, _)
450                | Op::Glob(_)
451                | Op::Regexp(_)
452                | Op::InSet(_, _)
453                | Op::NotInSet(_, _)
454                | Op::Cast(_)
455                | Op::CastExternal(_)
456                | Op::TruncateToDate
457                | Op::NativeFn1(_) => 0,
458
459                // Multi-column IN: pop N, push 1
460                Op::InTupleSet { tuple_size, .. } => 1 - (*tuple_size as i32),
461
462                // Pop only (-1)
463                Op::Pop => -1,
464
465                // Conditionals (no change to depth calculation)
466                Op::And(_) | Op::Or(_) => 0,
467
468                // Function calls: pop N, push 1
469                Op::CallScalar { arg_count, .. } | Op::CallStored { arg_count, .. } => {
470                    1 - (*arg_count as i32)
471                }
472                Op::Coalesce(n) | Op::Greatest(n) | Op::Least(n) | Op::ConcatN(n) => {
473                    1 - (*n as i32)
474                }
475
476                // Control flow (no stack effect for depth calculation)
477                Op::Jump(_)
478                | Op::JumpIfTrue(_)
479                | Op::JumpIfFalse(_)
480                | Op::JumpIfNull(_)
481                | Op::JumpIfNotNull(_)
482                | Op::PopJumpIfTrue(_)
483                | Op::PopJumpIfFalse(_)
484                | Op::Swap
485                | Op::Nop
486                | Op::Return
487                | Op::ReturnTrue
488                | Op::ReturnFalse
489                | Op::ReturnNull(_)
490                | Op::CaseStart
491                | Op::CaseWhen(_)
492                | Op::CaseThen(_)
493                | Op::CaseElse
494                | Op::CaseEnd => 0,
495            };
496
497            depth += effect;
498            max_depth = max_depth.max(depth);
499        }
500
501        // Ensure at least 1 for safety
502        (max_depth as usize).max(1)
503    }
504
505    /// Disassemble the program for debugging
506    pub fn disassemble(&self) -> String {
507        let mut result = String::new();
508        for (i, op) in self.ops.iter().enumerate() {
509            result.push_str(&format!("{:04}: {:?}\n", i, op));
510        }
511        result
512    }
513
514    /// Apply peephole optimizations to fuse common instruction patterns.
515    /// This is called automatically when creating a program via `new()`.
516    pub fn optimize(mut self) -> Self {
517        self.ops = Self::peephole_optimize(self.ops);
518        // Recalculate metadata after optimization
519        self.max_stack_depth = Self::compute_stack_depth(&self.ops);
520        self
521    }
522
523    /// Peephole optimizer: fuse common instruction patterns into single ops
524    fn peephole_optimize(mut ops: Vec<Op>) -> Vec<Op> {
525        if ops.len() < 2 {
526            return ops;
527        }
528
529        // Build a set of positions that are jump targets - we can't fuse instructions
530        // that are jump targets because that would make the jump land in the middle
531        // of what becomes a single instruction.
532        let mut jump_targets = FxHashSet::default();
533        for op in &ops {
534            match op {
535                Op::And(t)
536                | Op::Or(t)
537                | Op::Jump(t)
538                | Op::JumpIfTrue(t)
539                | Op::JumpIfFalse(t)
540                | Op::JumpIfNull(t)
541                | Op::JumpIfNotNull(t)
542                | Op::PopJumpIfTrue(t)
543                | Op::PopJumpIfFalse(t)
544                | Op::CaseWhen(t)
545                | Op::CaseThen(t) => {
546                    jump_targets.insert(*t as usize);
547                }
548                _ => {}
549            }
550        }
551
552        let mut result = Vec::with_capacity(ops.len());
553        // Map from old instruction position to new instruction position
554        let mut position_map: Vec<usize> = Vec::with_capacity(ops.len());
555        let mut i = 0;
556
557        while i < ops.len() {
558            // Record the new position for this old position
559            let new_pos = result.len();
560
561            // Pattern 1: LoadColumn + LoadConst + LoadConst + Between → BetweenColumnConst (4 ops → 1)
562            if i + 3 < ops.len() {
563                let is_safe = !jump_targets.contains(&i)
564                    && !jump_targets.contains(&(i + 1))
565                    && !jump_targets.contains(&(i + 2))
566                    && !jump_targets.contains(&(i + 3));
567
568                if is_safe {
569                    if let (
570                        Op::LoadColumn(col_idx),
571                        Op::LoadConst(low_val),
572                        Op::LoadConst(high_val),
573                        Op::Between,
574                    ) = (&ops[i], &ops[i + 1], &ops[i + 2], &ops[i + 3])
575                    {
576                        result.push(Op::BetweenColumnConst(
577                            *col_idx,
578                            low_val.clone(),
579                            high_val.clone(),
580                        ));
581                        // All 4 positions map to this single new position
582                        position_map.push(new_pos);
583                        position_map.push(new_pos);
584                        position_map.push(new_pos);
585                        position_map.push(new_pos);
586                        i += 4;
587                        continue;
588                    }
589                }
590            }
591
592            // Pattern 2: LoadColumn + LoadConst + Compare → XxColumnConst (3 ops → 1)
593            if i + 2 < ops.len() {
594                let is_safe = !jump_targets.contains(&i)
595                    && !jump_targets.contains(&(i + 1))
596                    && !jump_targets.contains(&(i + 2));
597
598                if is_safe {
599                    if let (Op::LoadColumn(col_idx), Op::LoadConst(const_val)) =
600                        (&ops[i], &ops[i + 1])
601                    {
602                        let fused = match &ops[i + 2] {
603                            Op::Eq => Some(Op::EqColumnConst(*col_idx, const_val.clone())),
604                            Op::Ne => Some(Op::NeColumnConst(*col_idx, const_val.clone())),
605                            Op::Lt => Some(Op::LtColumnConst(*col_idx, const_val.clone())),
606                            Op::Le => Some(Op::LeColumnConst(*col_idx, const_val.clone())),
607                            Op::Gt => Some(Op::GtColumnConst(*col_idx, const_val.clone())),
608                            Op::Ge => Some(Op::GeColumnConst(*col_idx, const_val.clone())),
609                            _ => None,
610                        };
611
612                        if let Some(fused_op) = fused {
613                            result.push(fused_op);
614                            // All 3 positions map to this single new position
615                            position_map.push(new_pos);
616                            position_map.push(new_pos);
617                            position_map.push(new_pos);
618                            i += 3;
619                            continue;
620                        }
621                    }
622                }
623            }
624
625            // Pattern 3: LoadColumn + IsNull/IsNotNull/Like/InSet (2 ops → 1)
626            if i + 1 < ops.len() {
627                let is_safe = !jump_targets.contains(&i) && !jump_targets.contains(&(i + 1));
628
629                if is_safe {
630                    if let Op::LoadColumn(col_idx) = &ops[i] {
631                        let fused = match &ops[i + 1] {
632                            Op::IsNull => Some(Op::IsNullColumn(*col_idx)),
633                            Op::IsNotNull => Some(Op::IsNotNullColumn(*col_idx)),
634                            Op::Like(pattern, case_insensitive) => {
635                                Some(Op::LikeColumn(*col_idx, pattern.clone(), *case_insensitive))
636                            }
637                            Op::InSet(set, has_null) => {
638                                Some(Op::InSetColumn(*col_idx, set.clone(), *has_null))
639                            }
640                            _ => None,
641                        };
642
643                        if let Some(fused_op) = fused {
644                            result.push(fused_op);
645                            // Both positions map to this single new position
646                            position_map.push(new_pos);
647                            position_map.push(new_pos);
648                            i += 2;
649                            continue;
650                        }
651                    }
652                }
653            }
654
655            // No fusion, copy the op
656            result.push(std::mem::replace(&mut ops[i], Op::Nop));
657            position_map.push(new_pos);
658            i += 1;
659        }
660
661        // If we fused anything, we need to adjust jump targets
662        if result.len() != ops.len() {
663            Self::adjust_jump_targets(&mut result, &position_map);
664        }
665
666        result
667    }
668
669    /// Adjust jump targets after peephole optimization using the position map.
670    fn adjust_jump_targets(ops: &mut [Op], position_map: &[usize]) {
671        for op in ops.iter_mut() {
672            match op {
673                Op::And(t)
674                | Op::Or(t)
675                | Op::Jump(t)
676                | Op::JumpIfTrue(t)
677                | Op::JumpIfFalse(t)
678                | Op::JumpIfNull(t)
679                | Op::JumpIfNotNull(t)
680                | Op::PopJumpIfTrue(t)
681                | Op::PopJumpIfFalse(t)
682                | Op::CaseWhen(t)
683                | Op::CaseThen(t) => {
684                    let old_target = *t as usize;
685                    if old_target < position_map.len() {
686                        *t = u16::try_from(position_map[old_target]).unwrap_or(u16::MAX);
687                    }
688                }
689                _ => {}
690            }
691        }
692    }
693}
694
695impl std::fmt::Debug for Program {
696    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
697        f.debug_struct("Program")
698            .field("ops_count", &self.ops.len())
699            .field("max_stack_depth", &self.max_stack_depth)
700            .field("needs_outer_context", &self.needs_outer_context)
701            .field("needs_second_row", &self.needs_second_row)
702            .finish()
703    }
704}
705
706/// Builder for constructing programs
707pub struct ProgramBuilder {
708    ops: Vec<Op>,
709    overflowed: bool,
710    invalid_patch: Option<String>,
711}
712
713impl ProgramBuilder {
714    pub fn new() -> Self {
715        Self {
716            ops: Vec::with_capacity(32),
717            overflowed: false,
718            invalid_patch: None,
719        }
720    }
721
722    /// Emit an operation
723    #[inline]
724    pub fn emit(&mut self, op: Op) {
725        if self.ops.len() >= u16::MAX as usize {
726            self.overflowed = true;
727        }
728        self.ops.push(op);
729    }
730
731    /// Get current position (for jump targets)
732    #[inline]
733    pub fn position(&self) -> u16 {
734        u16::try_from(self.ops.len()).unwrap_or(u16::MAX)
735    }
736
737    /// Whether an instruction or jump target exceeded the bytecode ABI.
738    pub fn is_overflowed(&self) -> bool {
739        self.overflowed
740    }
741
742    /// Patch a jump target at a specific position
743    pub fn patch_jump(&mut self, pos: usize, target: u16) {
744        let Some(op) = self.ops.get_mut(pos) else {
745            self.invalid_patch = Some(format!(
746                "cannot patch expression jump at instruction {pos}: program has {} instructions",
747                self.ops.len()
748            ));
749            return;
750        };
751
752        match op {
753            Op::And(t)
754            | Op::Or(t)
755            | Op::Jump(t)
756            | Op::JumpIfTrue(t)
757            | Op::JumpIfFalse(t)
758            | Op::JumpIfNull(t)
759            | Op::JumpIfNotNull(t)
760            | Op::PopJumpIfTrue(t)
761            | Op::PopJumpIfFalse(t)
762            | Op::CaseWhen(t)
763            | Op::CaseThen(t) => *t = target,
764            other => {
765                self.invalid_patch = Some(format!(
766                    "cannot patch non-jump expression instruction at {pos}: {other:?}"
767                ));
768            }
769        }
770    }
771
772    /// Validate and build the final program.
773    pub fn build(self) -> Result<Program> {
774        if let Some(message) = self.invalid_patch {
775            return Err(Error::invalid_argument(message));
776        }
777        if self.overflowed {
778            return Err(Error::invalid_argument(
779                "expression bytecode exceeds the u16 instruction limit",
780            ));
781        }
782        Program::try_new(self.ops)
783    }
784
785    /// Validate and build without peephole optimization (for tiny fold programs).
786    pub fn build_unoptimized(self) -> Result<Program> {
787        if let Some(message) = self.invalid_patch {
788            return Err(Error::invalid_argument(message));
789        }
790        if self.overflowed {
791            return Err(Error::invalid_argument(
792                "expression bytecode exceeds the u16 instruction limit",
793            ));
794        }
795        Program::try_new_unoptimized(self.ops)
796    }
797}
798
799impl Default for ProgramBuilder {
800    fn default() -> Self {
801        Self::new()
802    }
803}
804
805#[cfg(test)]
806mod tests {
807    use super::*;
808
809    // =========================================================================
810    // Program factory methods
811    // =========================================================================
812
813    #[test]
814    fn test_program_null() {
815        let prog = Program::null();
816        assert!(!prog.is_empty());
817        assert!(!prog.needs_outer_context());
818        assert!(!prog.needs_second_row());
819    }
820
821    #[test]
822    fn test_program_constant() {
823        let prog = Program::constant(Value::Integer(42));
824        assert!(!prog.is_empty());
825        assert!(!prog.needs_outer_context());
826        assert!(!prog.needs_second_row());
827    }
828
829    #[test]
830    fn test_program_always_true() {
831        let prog = Program::always_true();
832        assert_eq!(prog.len(), 1);
833        assert!(matches!(prog.ops()[0], Op::ReturnTrue));
834    }
835
836    #[test]
837    fn test_program_always_false() {
838        let prog = Program::always_false();
839        assert_eq!(prog.len(), 1);
840        assert!(matches!(prog.ops()[0], Op::ReturnFalse));
841    }
842
843    // =========================================================================
844    // Stack depth calculation
845    // =========================================================================
846
847    #[test]
848    fn test_stack_depth_simple() {
849        // LoadConst pushes 1, Return pops and returns
850        let prog = Program::new_unoptimized(vec![Op::LoadConst(Value::Integer(1)), Op::Return]);
851        assert_eq!(prog.max_stack_depth(), 1);
852    }
853
854    #[test]
855    fn test_stack_depth_binary_op() {
856        // LoadConst (+1), LoadConst (+1), Add (-1) = max 2
857        let prog = Program::new_unoptimized(vec![
858            Op::LoadConst(Value::Integer(1)),
859            Op::LoadConst(Value::Integer(2)),
860            Op::Add,
861            Op::Return,
862        ]);
863        assert_eq!(prog.max_stack_depth(), 2);
864    }
865
866    #[test]
867    fn test_stack_depth_nested() {
868        // (1 + 2) * (3 + 4)
869        // Load 1, Load 2, Add, Load 3, Load 4, Add, Mul
870        let prog = Program::new_unoptimized(vec![
871            Op::LoadConst(Value::Integer(1)),
872            Op::LoadConst(Value::Integer(2)),
873            Op::Add,
874            Op::LoadConst(Value::Integer(3)),
875            Op::LoadConst(Value::Integer(4)),
876            Op::Add,
877            Op::Mul,
878            Op::Return,
879        ]);
880        // Stack trace: 1, 2, 1, 2, 3, 2, 1
881        // Max is 3 (after loading 3rd value before second add)
882        assert!(prog.max_stack_depth() >= 2);
883    }
884
885    #[test]
886    fn test_stack_depth_fused_ops() {
887        // Fused ops push 1
888        let prog =
889            Program::new_unoptimized(vec![Op::EqColumnConst(0, Value::Integer(5)), Op::Return]);
890        assert_eq!(prog.max_stack_depth(), 1);
891    }
892
893    #[test]
894    fn test_stack_depth_between() {
895        // Between pops 3, pushes 1
896        let prog = Program::new_unoptimized(vec![
897            Op::LoadColumn(0),
898            Op::LoadConst(Value::Integer(1)),
899            Op::LoadConst(Value::Integer(10)),
900            Op::Between,
901            Op::Return,
902        ]);
903        assert_eq!(prog.max_stack_depth(), 3);
904    }
905
906    // =========================================================================
907    // Metadata detection
908    // =========================================================================
909
910    #[test]
911    fn test_needs_outer_context() {
912        let prog = Program::new_unoptimized(vec![Op::LoadOuterColumn("col".into()), Op::Return]);
913        assert!(prog.needs_outer_context());
914
915        let prog2 = Program::new_unoptimized(vec![Op::LoadColumn(0), Op::Return]);
916        assert!(!prog2.needs_outer_context());
917    }
918
919    #[test]
920    fn test_needs_second_row() {
921        let prog = Program::new_unoptimized(vec![
922            Op::LoadColumn(0),
923            Op::LoadColumn2(1),
924            Op::Eq,
925            Op::Return,
926        ]);
927        assert!(prog.needs_second_row());
928
929        let prog2 = Program::new_unoptimized(vec![Op::LoadColumn(0), Op::Return]);
930        assert!(!prog2.needs_second_row());
931    }
932
933    // =========================================================================
934    // Peephole optimization
935    // =========================================================================
936
937    #[test]
938    fn test_peephole_eq_column_const() {
939        // LoadColumn + LoadConst + Eq should fuse to EqColumnConst
940        let ops = vec![
941            Op::LoadColumn(0),
942            Op::LoadConst(Value::Integer(5)),
943            Op::Eq,
944            Op::Return,
945        ];
946        let prog = Program::new(ops);
947        // Should be fused to 2 ops: EqColumnConst + Return
948        assert_eq!(prog.len(), 2);
949        assert!(matches!(prog.ops()[0], Op::EqColumnConst(0, _)));
950    }
951
952    #[test]
953    fn test_peephole_lt_column_const() {
954        let ops = vec![
955            Op::LoadColumn(1),
956            Op::LoadConst(Value::Integer(10)),
957            Op::Lt,
958            Op::Return,
959        ];
960        let prog = Program::new(ops);
961        assert_eq!(prog.len(), 2);
962        assert!(matches!(prog.ops()[0], Op::LtColumnConst(1, _)));
963    }
964
965    #[test]
966    fn test_peephole_is_null_column() {
967        let ops = vec![Op::LoadColumn(2), Op::IsNull, Op::Return];
968        let prog = Program::new(ops);
969        assert_eq!(prog.len(), 2);
970        assert!(matches!(prog.ops()[0], Op::IsNullColumn(2)));
971    }
972
973    #[test]
974    fn test_peephole_between_column_const() {
975        let ops = vec![
976            Op::LoadColumn(0),
977            Op::LoadConst(Value::Integer(1)),
978            Op::LoadConst(Value::Integer(100)),
979            Op::Between,
980            Op::Return,
981        ];
982        let prog = Program::new(ops);
983        // Should fuse 4 ops to 1 BetweenColumnConst
984        assert_eq!(prog.len(), 2);
985        assert!(matches!(prog.ops()[0], Op::BetweenColumnConst(0, _, _)));
986    }
987
988    #[test]
989    fn test_peephole_no_fusion_when_not_applicable() {
990        // Can't fuse when pattern doesn't match
991        let ops = vec![
992            Op::LoadConst(Value::Integer(5)),
993            Op::LoadColumn(0),
994            Op::Eq,
995            Op::Return,
996        ];
997        let prog = Program::new(ops);
998        // LoadConst + LoadColumn + Eq doesn't match (order is wrong)
999        assert!(prog.len() >= 3);
1000    }
1001
1002    #[test]
1003    fn test_peephole_preserves_jumps() {
1004        // Ensure jump targets aren't fused over
1005        let ops = vec![
1006            Op::LoadColumn(0),
1007            Op::JumpIfFalse(3), // Jump to position 3
1008            Op::LoadConst(Value::Integer(5)),
1009            Op::Eq, // This is position 3 - a jump target
1010            Op::Return,
1011        ];
1012        let prog = Program::new(ops);
1013        // The fusion should not break jump semantics
1014        assert!(prog.len() >= 3);
1015    }
1016
1017    // =========================================================================
1018    // ProgramBuilder
1019    // =========================================================================
1020
1021    #[test]
1022    fn test_builder_basic() {
1023        let mut builder = ProgramBuilder::new();
1024        builder.emit(Op::LoadConst(Value::Integer(42)));
1025        builder.emit(Op::Return);
1026
1027        let prog = builder.build().unwrap();
1028        assert!(!prog.is_empty());
1029    }
1030
1031    #[test]
1032    fn test_builder_position() {
1033        let mut builder = ProgramBuilder::new();
1034        assert_eq!(builder.position(), 0);
1035
1036        builder.emit(Op::LoadColumn(0));
1037        assert_eq!(builder.position(), 1);
1038
1039        builder.emit(Op::LoadColumn(1));
1040        assert_eq!(builder.position(), 2);
1041    }
1042
1043    #[test]
1044    fn test_builder_patch_jump() {
1045        let mut builder = ProgramBuilder::new();
1046        builder.emit(Op::LoadColumn(0));
1047        builder.emit(Op::JumpIfFalse(0)); // Placeholder target
1048        let jump_pos = 1;
1049        builder.emit(Op::Pop);
1050        builder.emit(Op::LoadConst(Value::Integer(1)));
1051        let return_pos = builder.position();
1052        builder.emit(Op::Return);
1053
1054        builder.patch_jump(jump_pos, return_pos);
1055
1056        let prog = builder.build().unwrap();
1057        // After optimization, check that jump exists
1058        let has_jump = prog.ops().iter().any(|op| matches!(op, Op::JumpIfFalse(_)));
1059        assert!(has_jump || prog.len() < 4); // Either has jump or was optimized away
1060    }
1061
1062    #[test]
1063    fn test_builder_rejects_invalid_jump_patch() {
1064        let mut missing = ProgramBuilder::new();
1065        missing.emit(Op::LoadConst(Value::Integer(1)));
1066        missing.emit(Op::Return);
1067        missing.patch_jump(7, 1);
1068        assert!(missing.build().is_err());
1069
1070        let mut not_a_jump = ProgramBuilder::new();
1071        not_a_jump.emit(Op::LoadConst(Value::Integer(1)));
1072        not_a_jump.emit(Op::Return);
1073        not_a_jump.patch_jump(0, 1);
1074        assert!(not_a_jump.build_unoptimized().is_err());
1075    }
1076
1077    #[test]
1078    fn test_builder_default() {
1079        let builder: ProgramBuilder = Default::default();
1080        assert!(builder.build().is_err());
1081    }
1082
1083    // =========================================================================
1084    // Disassemble
1085    // =========================================================================
1086
1087    #[test]
1088    fn test_disassemble() {
1089        let prog = Program::new_unoptimized(vec![
1090            Op::LoadColumn(0),
1091            Op::LoadConst(Value::Integer(5)),
1092            Op::Eq,
1093            Op::Return,
1094        ]);
1095        let disasm = prog.disassemble();
1096        assert!(disasm.contains("LoadColumn"));
1097        assert!(disasm.contains("LoadConst"));
1098        assert!(disasm.contains("Eq"));
1099        assert!(disasm.contains("Return"));
1100        // Check format has line numbers
1101        assert!(disasm.contains("0000:"));
1102        assert!(disasm.contains("0001:"));
1103    }
1104
1105    // =========================================================================
1106    // Debug formatting
1107    // =========================================================================
1108
1109    #[test]
1110    fn test_program_debug() {
1111        let prog = Program::constant(Value::Integer(42));
1112        let debug_str = format!("{:?}", prog);
1113        assert!(debug_str.contains("Program"));
1114        assert!(debug_str.contains("ops_count"));
1115        assert!(debug_str.contains("max_stack_depth"));
1116    }
1117
1118    // =========================================================================
1119    // Constant enum
1120    // =========================================================================
1121
1122    #[test]
1123    fn test_constant_value() {
1124        let c = Constant::Value(Value::Integer(42));
1125        assert!(matches!(c, Constant::Value(Value::Integer(42))));
1126    }
1127
1128    #[test]
1129    fn test_constant_string() {
1130        let c = Constant::String("test".into());
1131        assert!(matches!(c, Constant::String(_)));
1132    }
1133
1134    #[test]
1135    fn test_constant_clone() {
1136        let c1 = Constant::Value(Value::Text("hello".into()));
1137        let c2 = c1.clone();
1138        assert!(matches!(c2, Constant::Value(Value::Text(_))));
1139    }
1140
1141    #[test]
1142    fn test_public_constructors_reject_malformed_programs() {
1143        assert!(Program::try_new(vec![]).is_err());
1144        assert!(Program::try_new_unoptimized(vec![Op::Add, Op::Return]).is_err());
1145        assert!(
1146            Program::try_new_unoptimized(vec![Op::LoadConst(Value::Integer(1)), Op::Jump(0),])
1147                .is_err()
1148        );
1149        assert!(Program::try_new_unoptimized(vec![Op::LoadConst(Value::Integer(1))]).is_err());
1150    }
1151}