1use rustc_hash::FxHashSet;
23
24use super::ops::Op;
25use radixdb_core::CompactArc;
26use radixdb_core::{Error, Result, Value};
27
28#[derive(Debug, Clone)]
30pub enum Constant {
31 Value(Value),
33 String(CompactArc<str>),
35}
36
37#[derive(Clone)]
44pub struct Program {
45 ops: Vec<Op>,
47
48 max_stack_depth: usize,
50
51 needs_outer_context: bool,
53
54 needs_second_row: bool,
56
57 #[cfg(debug_assertions)]
59 source: Option<String>,
60}
61
62impl Program {
63 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 pub fn try_new_unoptimized(ops: Vec<Op>) -> Result<Self> {
73 Self::validate(&ops)?;
74 Ok(Self::new_unoptimized(ops))
75 }
76
77 pub(crate) fn new(ops: Vec<Op>) -> Self {
80 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 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 pub fn null() -> Self {
114 Self::new(vec![Op::LoadNull(radixdb_core::DataType::Null), Op::Return])
115 }
116
117 pub fn constant(value: Value) -> Self {
119 Self::new(vec![Op::LoadConst(value), Op::Return])
120 }
121
122 pub fn always_true() -> Self {
124 Self::new(vec![Op::ReturnTrue])
125 }
126
127 pub fn always_false() -> Self {
129 Self::new(vec![Op::ReturnFalse])
130 }
131
132 #[inline]
134 pub fn ops(&self) -> &[Op] {
135 &self.ops
136 }
137
138 #[inline]
140 pub fn max_stack_depth(&self) -> usize {
141 self.max_stack_depth
142 }
143
144 #[inline]
146 pub fn needs_outer_context(&self) -> bool {
147 self.needs_outer_context
148 }
149
150 #[inline]
152 pub fn needs_second_row(&self) -> bool {
153 self.needs_second_row
154 }
155
156 #[inline]
158 pub fn len(&self) -> usize {
159 self.ops.len()
160 }
161
162 #[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 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 #[cfg(debug_assertions)]
348 pub fn with_source(mut self, source: String) -> Self {
349 self.source = Some(source);
350 self
351 }
352
353 #[cfg(debug_assertions)]
355 pub fn source(&self) -> Option<&str> {
356 self.source.as_deref()
357 }
358
359 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 let effect = match op {
367 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 | Op::EqColumnConst(_, _)
380 | Op::NeColumnConst(_, _)
381 | Op::LtColumnConst(_, _)
382 | Op::LeColumnConst(_, _)
383 | Op::GtColumnConst(_, _)
384 | Op::GeColumnConst(_, _)
385 | Op::IsNullColumn(_)
387 | Op::IsNotNullColumn(_)
388 | Op::LikeColumn(_, _, _)
389 | Op::InSetColumn(_, _, _)
390 | Op::BetweenColumnConst(_, _, _) => 1,
391
392 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 Op::Between | Op::NotBetween => -2,
420
421 Op::LikeDynamic(_)
423 | Op::LikeDynamicEscape(_, _)
424 | Op::GlobDynamic
425 | Op::RegexpDynamic
426 | 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 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 Op::InTupleSet { tuple_size, .. } => 1 - (*tuple_size as i32),
461
462 Op::Pop => -1,
464
465 Op::And(_) | Op::Or(_) => 0,
467
468 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 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 (max_depth as usize).max(1)
503 }
504
505 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 pub fn optimize(mut self) -> Self {
517 self.ops = Self::peephole_optimize(self.ops);
518 self.max_stack_depth = Self::compute_stack_depth(&self.ops);
520 self
521 }
522
523 fn peephole_optimize(mut ops: Vec<Op>) -> Vec<Op> {
525 if ops.len() < 2 {
526 return ops;
527 }
528
529 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 let mut position_map: Vec<usize> = Vec::with_capacity(ops.len());
555 let mut i = 0;
556
557 while i < ops.len() {
558 let new_pos = result.len();
560
561 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 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 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 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 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 position_map.push(new_pos);
647 position_map.push(new_pos);
648 i += 2;
649 continue;
650 }
651 }
652 }
653 }
654
655 result.push(std::mem::replace(&mut ops[i], Op::Nop));
657 position_map.push(new_pos);
658 i += 1;
659 }
660
661 if result.len() != ops.len() {
663 Self::adjust_jump_targets(&mut result, &position_map);
664 }
665
666 result
667 }
668
669 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
706pub 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 #[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 #[inline]
733 pub fn position(&self) -> u16 {
734 u16::try_from(self.ops.len()).unwrap_or(u16::MAX)
735 }
736
737 pub fn is_overflowed(&self) -> bool {
739 self.overflowed
740 }
741
742 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 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 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 #[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 #[test]
848 fn test_stack_depth_simple() {
849 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 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 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 assert!(prog.max_stack_depth() >= 2);
883 }
884
885 #[test]
886 fn test_stack_depth_fused_ops() {
887 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 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 #[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 #[test]
938 fn test_peephole_eq_column_const() {
939 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 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 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 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 assert!(prog.len() >= 3);
1000 }
1001
1002 #[test]
1003 fn test_peephole_preserves_jumps() {
1004 let ops = vec![
1006 Op::LoadColumn(0),
1007 Op::JumpIfFalse(3), Op::LoadConst(Value::Integer(5)),
1009 Op::Eq, Op::Return,
1011 ];
1012 let prog = Program::new(ops);
1013 assert!(prog.len() >= 3);
1015 }
1016
1017 #[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)); 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 let has_jump = prog.ops().iter().any(|op| matches!(op, Op::JumpIfFalse(_)));
1059 assert!(has_jump || prog.len() < 4); }
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 #[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 assert!(disasm.contains("0000:"));
1102 assert!(disasm.contains("0001:"));
1103 }
1104
1105 #[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 #[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}