1use std::borrow::Cow;
67use std::collections::BTreeMap;
68use std::fmt::Debug;
69
70#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
72pub struct ExprId(u32);
73
74impl ExprId {
75 #[must_use]
77 pub const fn new(index: u32) -> Self {
78 Self(index)
79 }
80
81 #[must_use]
83 pub const fn index(self) -> usize {
84 self.0 as usize
85 }
86}
87
88#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
90pub struct StmtId(u32);
91
92impl StmtId {
93 #[must_use]
95 pub const fn new(index: u32) -> Self {
96 Self(index)
97 }
98
99 #[must_use]
101 pub const fn index(self) -> usize {
102 self.0 as usize
103 }
104}
105
106#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
108pub struct StrId(u32);
109
110impl StrId {
111 #[must_use]
113 pub const fn new(index: u32) -> Self {
114 Self(index)
115 }
116
117 #[must_use]
119 pub const fn index(self) -> usize {
120 self.0 as usize
121 }
122}
123
124#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
131pub struct HookId(u32);
132
133impl HookId {
134 #[must_use]
136 pub const fn new(index: u32) -> Self {
137 Self(index)
138 }
139
140 #[must_use]
142 pub const fn index(self) -> usize {
143 self.0 as usize
144 }
145}
146
147#[derive(Clone, Copy, Debug, Eq, PartialEq)]
149pub enum CmpOp {
150 Eq,
151 Ne,
152 Lt,
153 Le,
154 Gt,
155 Ge,
156}
157
158#[derive(Clone, Copy, Debug, Eq, PartialEq)]
160pub enum ArithOp {
161 Add,
162 Sub,
163 Mul,
164 Div,
165 Mod,
166}
167
168#[derive(Clone, Debug, Eq, PartialEq)]
175pub enum PExpr {
176 Bool(bool),
178 Int(i64),
180 Str(StrId),
182 La(isize),
184 TokenText(isize),
186 TokenIndexAdjacent,
190 CtxRuleText(usize),
193 Member(usize),
195 MemberTop(usize),
198 MemberLen(usize),
200 LocalArg,
203 Column,
205 TokenStartColumn,
207 TokenTextSoFar,
209 IsNull(ExprId),
212 Not(ExprId),
214 And(Box<[ExprId]>),
216 Or(Box<[ExprId]>),
218 Cmp(CmpOp, ExprId, ExprId),
220 Arith(ArithOp, ExprId, ExprId),
222 Hook(HookId),
224 EvalTrace(bool),
229}
230
231#[derive(Clone, Debug, Eq, PartialEq)]
237pub enum AStmt {
238 SetMember(usize, ExprId),
240 AddMember(usize, ExprId),
242 PushMember(usize, ExprId),
244 PopMember(usize),
246 SetReturn(StrId, ExprId),
248 Seq(Box<[StmtId]>),
250 Hook(HookId),
252}
253
254#[allow(variant_size_differences)]
256#[derive(Clone, Copy, Debug, Eq, PartialEq)]
257pub enum Value {
258 Null,
260 Bool(bool),
261 Int(i64),
262}
263
264impl Value {
265 #[must_use]
267 pub const fn truthy(self) -> bool {
268 match self {
269 Self::Null => false,
270 Self::Bool(value) => value,
271 Self::Int(value) => value != 0,
272 }
273 }
274}
275
276pub trait PredContext {
283 type TokenText<'a>: AsRef<str>
284 where
285 Self: 'a;
286
287 fn la(&mut self, offset: isize) -> i64;
289 fn token_text(&mut self, offset: isize) -> Option<Self::TokenText<'_>>;
291 fn token_index_adjacent(&mut self) -> bool;
293 fn ctx_rule_text(&self, rule_index: usize) -> Option<String>;
295 fn member(&self, member: usize) -> Option<i64>;
297 fn member_top(&self, _member: usize) -> Option<i64> {
301 None
302 }
303 fn member_len(&self, _member: usize) -> usize {
305 0
306 }
307 fn local_arg(&self) -> Option<i64>;
309 fn column(&self) -> Option<i64>;
311 fn token_start_column(&self) -> Option<i64>;
313 fn token_text_so_far(&self) -> Option<String>;
315 fn hook(&mut self, hook: HookId) -> bool;
317 fn trace_bool(&mut self, value: bool) -> bool {
319 value
320 }
321}
322
323pub trait ActContext: PredContext {
325 fn set_member(&mut self, member: usize, value: i64);
327 fn push_member(&mut self, _member: usize, _value: i64) {}
333 fn pop_member(&mut self, _member: usize) -> Option<i64> {
336 None
337 }
338 fn set_return(&mut self, name: &str, value: i64);
340 fn action_hook(&mut self, hook: HookId);
342}
343
344#[derive(Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
355pub struct MemberEnv {
356 scalars: BTreeMap<usize, i64>,
357 stacks: BTreeMap<usize, Vec<i64>>,
358}
359
360impl MemberEnv {
361 #[must_use]
362 pub const fn new() -> Self {
363 Self {
364 scalars: BTreeMap::new(),
365 stacks: BTreeMap::new(),
366 }
367 }
368
369 #[must_use]
378 pub fn with_initial_scalars(initial: impl IntoIterator<Item = (usize, i64)>) -> Self {
379 Self {
380 scalars: initial.into_iter().collect(),
381 stacks: BTreeMap::new(),
382 }
383 }
384
385 pub fn reset_to_initial(&mut self, initial: impl IntoIterator<Item = (usize, i64)>) {
391 self.scalars = initial.into_iter().collect();
392 self.stacks.clear();
393 }
394
395 #[must_use]
397 pub fn is_empty(&self) -> bool {
398 self.scalars.is_empty() && self.stacks.is_empty()
399 }
400
401 #[must_use]
403 pub fn scalar(&self, member: usize) -> Option<i64> {
404 self.scalars.get(&member).copied()
405 }
406
407 pub fn set_scalar(&mut self, member: usize, value: i64) {
409 self.scalars.insert(member, value);
410 }
411
412 pub fn add_scalar(&mut self, member: usize, delta: i64) -> i64 {
414 let value = self.scalars.entry(member).or_default();
415 *value = value.saturating_add(delta);
416 *value
417 }
418
419 #[must_use]
421 pub fn stack_top(&self, member: usize) -> Option<i64> {
422 self.stacks.get(&member)?.last().copied()
423 }
424
425 #[must_use]
427 pub fn stack_len(&self, member: usize) -> usize {
428 self.stacks.get(&member).map_or(0, Vec::len)
429 }
430
431 pub fn push_stack(&mut self, member: usize, value: i64) {
433 self.stacks.entry(member).or_default().push(value);
434 }
435
436 pub fn pop_stack(&mut self, member: usize) -> Option<i64> {
442 let stack = self.stacks.get_mut(&member)?;
443 let value = stack.pop();
444 if stack.is_empty() {
445 self.stacks.remove(&member);
446 }
447 value
448 }
449
450 pub fn scalars(&self) -> impl Iterator<Item = (usize, i64)> + '_ {
452 self.scalars.iter().map(|(slot, value)| (*slot, *value))
453 }
454}
455
456#[derive(Clone, Debug, Default, Eq, PartialEq)]
461pub struct SemIr {
462 exprs: Vec<PExpr>,
463 stmts: Vec<AStmt>,
464 strings: Vec<Box<str>>,
465}
466
467impl SemIr {
468 #[must_use]
469 pub fn new() -> Self {
470 Self::default()
471 }
472
473 pub fn expr(&mut self, node: PExpr) -> ExprId {
475 let id = ExprId(u32::try_from(self.exprs.len()).expect("expression arena fits in u32"));
476 self.exprs.push(node);
477 id
478 }
479
480 pub fn stmt(&mut self, node: AStmt) -> StmtId {
482 let id = StmtId(u32::try_from(self.stmts.len()).expect("statement arena fits in u32"));
483 self.stmts.push(node);
484 id
485 }
486
487 pub fn intern(&mut self, value: &str) -> StrId {
489 if let Some(position) = self.strings.iter().position(|entry| &**entry == value) {
490 return StrId(u32::try_from(position).expect("string pool fits in u32"));
491 }
492 let id = StrId(u32::try_from(self.strings.len()).expect("string pool fits in u32"));
493 self.strings.push(value.into());
494 id
495 }
496
497 #[must_use]
499 pub fn text(&self, id: StrId) -> &str {
500 &self.strings[id.0 as usize]
501 }
502
503 fn node(&self, id: ExprId) -> &PExpr {
504 &self.exprs[id.0 as usize]
505 }
506
507 fn stmt_node(&self, id: StmtId) -> &AStmt {
508 &self.stmts[id.0 as usize]
509 }
510}
511
512pub fn eval_pred<C: PredContext>(ir: &SemIr, expr: ExprId, ctx: &mut C) -> bool {
518 eval_value(ir, expr, ctx).truthy()
519}
520
521pub fn exec_stmt<C: ActContext>(ir: &SemIr, stmt: StmtId, ctx: &mut C) {
523 match ir.stmt_node(stmt) {
524 AStmt::SetMember(member, value) => {
525 let value = int_or_zero(eval_value(ir, *value, ctx));
526 ctx.set_member(*member, value);
527 }
528 AStmt::AddMember(member, delta) => {
529 let delta = int_or_zero(eval_value(ir, *delta, ctx));
530 let current = ctx.member(*member).unwrap_or_default();
531 ctx.set_member(*member, current.saturating_add(delta));
532 }
533 AStmt::PushMember(member, value) => {
534 let value = int_or_zero(eval_value(ir, *value, ctx));
535 ctx.push_member(*member, value);
536 }
537 AStmt::PopMember(member) => {
538 let _ = ctx.pop_member(*member);
541 }
542 AStmt::SetReturn(name, value) => {
543 let value = int_or_zero(eval_value(ir, *value, ctx));
544 let name = ir.text(*name).to_owned();
545 ctx.set_return(&name, value);
546 }
547 AStmt::Seq(stmts) => {
548 for stmt in stmts {
549 exec_stmt(ir, *stmt, ctx);
550 }
551 }
552 AStmt::Hook(hook) => ctx.action_hook(*hook),
553 }
554}
555
556const fn int_or_zero(value: Value) -> i64 {
563 match value {
564 Value::Int(value) => value,
565 Value::Bool(value) => value as i64,
566 Value::Null => 0,
567 }
568}
569
570fn eval_value<C: PredContext>(ir: &SemIr, expr: ExprId, ctx: &mut C) -> Value {
571 match ir.node(expr) {
572 PExpr::Str(_) | PExpr::TokenText(_) | PExpr::CtxRuleText(_) | PExpr::TokenTextSoFar => {
575 debug_assert!(false, "text-valued node evaluated outside a comparison");
576 Value::Null
577 }
578 PExpr::Bool(value) => Value::Bool(*value),
579 PExpr::Int(value) => Value::Int(*value),
580 PExpr::La(offset) => Value::Int(ctx.la(*offset)),
581 PExpr::TokenIndexAdjacent => Value::Bool(ctx.token_index_adjacent()),
582 PExpr::Member(member) => ctx.member(*member).map_or(Value::Null, Value::Int),
583 PExpr::MemberTop(member) => ctx.member_top(*member).map_or(Value::Null, Value::Int),
586 PExpr::MemberLen(member) => {
588 Value::Int(i64::try_from(ctx.member_len(*member)).unwrap_or(i64::MAX))
589 }
590 PExpr::LocalArg => ctx.local_arg().map_or(Value::Null, Value::Int),
591 PExpr::Column => ctx.column().map_or(Value::Null, Value::Int),
592 PExpr::TokenStartColumn => ctx.token_start_column().map_or(Value::Null, Value::Int),
593 PExpr::IsNull(inner) => Value::Bool(eval_is_null(ir, *inner, ctx)),
594 PExpr::Not(inner) => Value::Bool(!eval_value(ir, *inner, ctx).truthy()),
595 PExpr::And(children) => Value::Bool(
596 children
597 .iter()
598 .all(|child| eval_value(ir, *child, ctx).truthy()),
599 ),
600 PExpr::Or(children) => Value::Bool(
601 children
602 .iter()
603 .any(|child| eval_value(ir, *child, ctx).truthy()),
604 ),
605 PExpr::Cmp(op, lhs, rhs) => eval_cmp(ir, *op, *lhs, *rhs, ctx),
606 PExpr::Arith(op, lhs, rhs) => eval_arith(ir, *op, *lhs, *rhs, ctx),
607 PExpr::Hook(hook) => Value::Bool(ctx.hook(*hook)),
608 PExpr::EvalTrace(value) => Value::Bool(ctx.trace_bool(*value)),
609 }
610}
611
612fn eval_is_null<C: PredContext>(ir: &SemIr, inner: ExprId, ctx: &mut C) -> bool {
613 if let Some(source) = text_source(ir, inner) {
614 return resolve_owned_text(ir, source, ctx).is_none();
615 }
616 eval_value(ir, inner, ctx) == Value::Null
617}
618
619fn eval_cmp<C: PredContext>(ir: &SemIr, op: CmpOp, lhs: ExprId, rhs: ExprId, ctx: &mut C) -> Value {
620 let left_source = text_source(ir, lhs);
621 let right_source = text_source(ir, rhs);
622 if left_source.is_some() || right_source.is_some() {
623 return eval_text_cmp(ir, op, (lhs, left_source), (rhs, right_source), ctx);
624 }
625 let left = eval_value(ir, lhs, ctx);
626 let right = eval_value(ir, rhs, ctx);
627 Value::Bool(match (left, right) {
628 (Value::Null, Value::Null) => cmp_on_equality(op, true),
629 (Value::Null, _) | (_, Value::Null) => cmp_on_equality(op, false),
630 (Value::Bool(left), Value::Bool(right)) => cmp_on_equality(op, left == right),
631 (Value::Int(left), Value::Int(right)) => cmp_ints(op, left, right),
632 (Value::Bool(_), Value::Int(_)) | (Value::Int(_), Value::Bool(_)) => {
633 cmp_on_equality(op, false)
634 }
635 })
636}
637
638const fn cmp_on_equality(op: CmpOp, equal: bool) -> bool {
641 match op {
642 CmpOp::Eq => equal,
643 CmpOp::Ne => !equal,
644 CmpOp::Lt | CmpOp::Le | CmpOp::Gt | CmpOp::Ge => false,
645 }
646}
647
648const fn cmp_ints(op: CmpOp, left: i64, right: i64) -> bool {
649 match op {
650 CmpOp::Eq => left == right,
651 CmpOp::Ne => left != right,
652 CmpOp::Lt => left < right,
653 CmpOp::Le => left <= right,
654 CmpOp::Gt => left > right,
655 CmpOp::Ge => left >= right,
656 }
657}
658
659#[derive(Clone, Copy, Debug)]
666enum TextSource {
667 Literal(StrId),
668 Lookahead(isize),
669 CtxRule(usize),
670 SoFar,
671}
672
673fn text_source(ir: &SemIr, expr: ExprId) -> Option<TextSource> {
674 match ir.node(expr) {
675 PExpr::Str(id) => Some(TextSource::Literal(*id)),
676 PExpr::TokenText(offset) => Some(TextSource::Lookahead(*offset)),
677 PExpr::CtxRuleText(rule_index) => Some(TextSource::CtxRule(*rule_index)),
678 PExpr::TokenTextSoFar => Some(TextSource::SoFar),
679 _ => None,
680 }
681}
682
683fn resolve_static_text<'ir, C: PredContext>(
685 ir: &'ir SemIr,
686 source: TextSource,
687 ctx: &C,
688) -> Option<Cow<'ir, str>> {
689 match source {
690 TextSource::Literal(id) => Some(Cow::Borrowed(ir.text(id))),
691 TextSource::Lookahead(_) => unreachable!("lookahead operands are resolved last"),
692 TextSource::CtxRule(rule_index) => ctx.ctx_rule_text(rule_index).map(Cow::Owned),
693 TextSource::SoFar => ctx.token_text_so_far().map(Cow::Owned),
694 }
695}
696
697fn resolve_owned_text<C: PredContext>(
699 ir: &SemIr,
700 source: TextSource,
701 ctx: &mut C,
702) -> Option<String> {
703 match source {
704 TextSource::Lookahead(offset) => {
705 ctx.token_text(offset).map(|text| text.as_ref().to_owned())
706 }
707 other => resolve_static_text(ir, other, ctx).map(Cow::into_owned),
708 }
709}
710
711fn eval_text_cmp<C: PredContext>(
712 ir: &SemIr,
713 op: CmpOp,
714 (lhs, left_source): (ExprId, Option<TextSource>),
715 (rhs, right_source): (ExprId, Option<TextSource>),
716 ctx: &mut C,
717) -> Value {
718 let (Some(left_source), Some(right_source)) = (left_source, right_source) else {
721 debug_assert!(false, "text operand compared with non-text operand");
722 let _ = (lhs, rhs);
723 return Value::Bool(cmp_on_equality(op, false));
724 };
725 Value::Bool(match (left_source, right_source) {
726 (TextSource::Lookahead(left), TextSource::Lookahead(right)) => {
727 let left = ctx.token_text(left).map(|text| text.as_ref().to_owned());
730 let right = ctx.token_text(right);
731 cmp_texts(op, left.as_deref(), right.as_ref().map(AsRef::as_ref))
732 }
733 (TextSource::Lookahead(offset), other) => {
734 let right = resolve_static_text(ir, other, ctx);
735 let left = ctx.token_text(offset);
736 cmp_texts(op, left.as_ref().map(AsRef::as_ref), right.as_deref())
737 }
738 (other, TextSource::Lookahead(offset)) => {
739 let left = resolve_static_text(ir, other, ctx);
740 let right = ctx.token_text(offset);
741 cmp_texts(op, left.as_deref(), right.as_ref().map(AsRef::as_ref))
742 }
743 (left, right) => {
744 let left = resolve_static_text(ir, left, ctx);
745 let right = resolve_static_text(ir, right, ctx);
746 cmp_texts(op, left.as_deref(), right.as_deref())
747 }
748 })
749}
750
751fn cmp_texts(op: CmpOp, left: Option<&str>, right: Option<&str>) -> bool {
752 match (left, right) {
753 (None, None) => cmp_on_equality(op, true),
754 (None, Some(_)) | (Some(_), None) => cmp_on_equality(op, false),
755 (Some(left), Some(right)) => match op {
756 CmpOp::Eq => left == right,
757 CmpOp::Ne => left != right,
758 CmpOp::Lt => left < right,
759 CmpOp::Le => left <= right,
760 CmpOp::Gt => left > right,
761 CmpOp::Ge => left >= right,
762 },
763 }
764}
765
766fn eval_arith<C: PredContext>(
767 ir: &SemIr,
768 op: ArithOp,
769 lhs: ExprId,
770 rhs: ExprId,
771 ctx: &mut C,
772) -> Value {
773 let (Value::Int(left), Value::Int(right)) =
774 (eval_value(ir, lhs, ctx), eval_value(ir, rhs, ctx))
775 else {
776 return Value::Null;
777 };
778 let result = match op {
779 ArithOp::Add => left.checked_add(right),
780 ArithOp::Sub => left.checked_sub(right),
781 ArithOp::Mul => left.checked_mul(right),
782 ArithOp::Div => left.checked_div(right),
783 ArithOp::Mod => left.checked_rem(right),
784 };
785 result.map_or(Value::Null, Value::Int)
786}
787
788#[cfg(test)]
789mod tests {
790 use super::{
791 AStmt, ActContext, ArithOp, CmpOp, ExprId, HookId, MemberEnv, PExpr, PredContext, SemIr,
792 Value, eval_pred, eval_value, exec_stmt,
793 };
794 use std::collections::BTreeMap;
795
796 #[derive(Debug, Default)]
798 struct MockCtx {
799 tokens: Vec<(i64, Option<&'static str>)>,
800 adjacent: bool,
801 ctx_rule_texts: BTreeMap<usize, String>,
802 members: BTreeMap<usize, i64>,
803 stacks: MemberEnv,
804 local_arg: Option<i64>,
805 column: Option<i64>,
806 token_start_column: Option<i64>,
807 text_so_far: Option<String>,
808 hook_results: Vec<bool>,
809 hook_calls: Vec<HookId>,
810 la_calls: usize,
811 returns: BTreeMap<String, i64>,
812 }
813
814 impl PredContext for MockCtx {
815 type TokenText<'a>
816 = &'a str
817 where
818 Self: 'a;
819
820 fn la(&mut self, offset: isize) -> i64 {
821 self.la_calls += 1;
822 self.lookup(offset).map_or(-1, |(token_type, _)| token_type)
823 }
824
825 fn token_text(&mut self, offset: isize) -> Option<Self::TokenText<'_>> {
826 self.lookup(offset).and_then(|(_, text)| text)
827 }
828
829 fn token_index_adjacent(&mut self) -> bool {
830 self.adjacent
831 }
832
833 fn ctx_rule_text(&self, rule_index: usize) -> Option<String> {
834 self.ctx_rule_texts.get(&rule_index).cloned()
835 }
836
837 fn member(&self, member: usize) -> Option<i64> {
838 self.members.get(&member).copied()
839 }
840
841 fn member_top(&self, member: usize) -> Option<i64> {
842 self.stacks.stack_top(member)
843 }
844
845 fn member_len(&self, member: usize) -> usize {
846 self.stacks.stack_len(member)
847 }
848
849 fn local_arg(&self) -> Option<i64> {
850 self.local_arg
851 }
852
853 fn column(&self) -> Option<i64> {
854 self.column
855 }
856
857 fn token_start_column(&self) -> Option<i64> {
858 self.token_start_column
859 }
860
861 fn token_text_so_far(&self) -> Option<String> {
862 self.text_so_far.clone()
863 }
864
865 fn hook(&mut self, hook: HookId) -> bool {
866 self.hook_calls.push(hook);
867 self.hook_results[hook.index()]
868 }
869 }
870
871 impl ActContext for MockCtx {
872 fn set_member(&mut self, member: usize, value: i64) {
873 self.members.insert(member, value);
874 }
875
876 fn push_member(&mut self, member: usize, value: i64) {
877 self.stacks.push_stack(member, value);
878 }
879
880 fn pop_member(&mut self, member: usize) -> Option<i64> {
881 self.stacks.pop_stack(member)
882 }
883
884 fn set_return(&mut self, name: &str, value: i64) {
885 self.returns.insert(name.to_owned(), value);
886 }
887
888 fn action_hook(&mut self, hook: HookId) {
889 self.hook_calls.push(hook);
890 }
891 }
892
893 impl MockCtx {
894 fn lookup(&self, offset: isize) -> Option<(i64, Option<&'static str>)> {
895 let index = if offset > 0 {
897 usize::try_from(offset - 1).ok()?
898 } else {
899 self.tokens.len().checked_sub(offset.unsigned_abs())?
900 };
901 self.tokens.get(index).copied()
902 }
903 }
904
905 fn build(build: impl FnOnce(&mut SemIr) -> ExprId) -> (SemIr, ExprId) {
906 let mut ir = SemIr::new();
907 let root = build(&mut ir);
908 (ir, root)
909 }
910
911 #[test]
912 fn literals_and_truthiness() {
913 for (value, expected) in [(true, true), (false, false)] {
914 let (ir, root) = build(|ir| ir.expr(PExpr::Bool(value)));
915 assert_eq!(eval_pred(&ir, root, &mut MockCtx::default()), expected);
916 }
917 let (ir, root) = build(|ir| ir.expr(PExpr::Int(2)));
918 assert!(eval_pred(&ir, root, &mut MockCtx::default()));
919 let (ir, root) = build(|ir| ir.expr(PExpr::Int(0)));
920 assert!(!eval_pred(&ir, root, &mut MockCtx::default()));
921 }
922
923 #[test]
924 fn lookahead_text_equals_literal_and_absent_token_fails() {
925 let (ir, root) = build(|ir| {
926 let text = ir.expr(PExpr::TokenText(1));
927 let literal = ir.intern("of");
928 let literal = ir.expr(PExpr::Str(literal));
929 ir.expr(PExpr::Cmp(CmpOp::Eq, text, literal))
930 });
931
932 let mut ctx = MockCtx {
933 tokens: vec![(7, Some("of"))],
934 ..MockCtx::default()
935 };
936 assert!(eval_pred(&ir, root, &mut ctx));
937
938 ctx.tokens = vec![(7, Some("in"))];
939 assert!(!eval_pred(&ir, root, &mut ctx));
940
941 ctx.tokens = Vec::new();
943 assert!(!eval_pred(&ir, root, &mut ctx));
944 }
945
946 #[test]
947 fn ctx_rule_text_not_equals_passes_when_child_absent() {
948 let (ir, root) = build(|ir| {
949 let child = ir.expr(PExpr::CtxRuleText(4));
950 let literal = ir.intern("static");
951 let literal = ir.expr(PExpr::Str(literal));
952 ir.expr(PExpr::Cmp(CmpOp::Ne, child, literal))
953 });
954
955 assert!(eval_pred(&ir, root, &mut MockCtx::default()));
957
958 let mut ctx = MockCtx {
959 ctx_rule_texts: std::iter::once((4, "static".to_owned())).collect(),
960 ..MockCtx::default()
961 };
962 assert!(!eval_pred(&ir, root, &mut ctx));
963
964 ctx.ctx_rule_texts = std::iter::once((4, "dynamic".to_owned())).collect();
965 assert!(eval_pred(&ir, root, &mut ctx));
966 }
967
968 #[test]
969 fn absent_local_arg_composes_non_restrictive_guard() {
970 let (ir, root) = build(|ir| {
973 let arg = ir.expr(PExpr::LocalArg);
974 let absent = ir.expr(PExpr::IsNull(arg));
975 let value = ir.expr(PExpr::Int(2));
976 let equals = ir.expr(PExpr::Cmp(CmpOp::Eq, arg, value));
977 ir.expr(PExpr::Or([absent, equals].into()))
978 });
979
980 assert!(eval_pred(&ir, root, &mut MockCtx::default()));
981 let mut ctx = MockCtx {
982 local_arg: Some(2),
983 ..MockCtx::default()
984 };
985 assert!(eval_pred(&ir, root, &mut ctx));
986 ctx.local_arg = Some(3);
987 assert!(!eval_pred(&ir, root, &mut ctx));
988 }
989
990 #[test]
991 fn member_modulo_comparison() {
992 let (ir, root) = build(|ir| {
993 let member = ir.expr(PExpr::Member(0));
994 let modulus = ir.expr(PExpr::Int(2));
995 let remainder = ir.expr(PExpr::Arith(ArithOp::Mod, member, modulus));
996 let expected = ir.expr(PExpr::Int(0));
997 ir.expr(PExpr::Cmp(CmpOp::Eq, remainder, expected))
998 });
999
1000 let mut ctx = MockCtx {
1001 members: std::iter::once((0, 4)).collect(),
1002 ..MockCtx::default()
1003 };
1004 assert!(eval_pred(&ir, root, &mut ctx));
1005 ctx.members.insert(0, 5);
1006 assert!(!eval_pred(&ir, root, &mut ctx));
1007 ctx.members.clear();
1009 assert!(!eval_pred(&ir, root, &mut ctx));
1010 }
1011
1012 #[test]
1013 fn arithmetic_null_propagation_and_division_by_zero() {
1014 let (ir, root) = build(|ir| {
1015 let member = ir.expr(PExpr::Member(9));
1016 let zero = ir.expr(PExpr::Int(0));
1017 let modulo = ir.expr(PExpr::Arith(ArithOp::Mod, member, zero));
1018 ir.expr(PExpr::IsNull(modulo))
1019 });
1020 let mut ctx = MockCtx {
1022 members: std::iter::once((9, 3)).collect(),
1023 ..MockCtx::default()
1024 };
1025 assert!(eval_pred(&ir, root, &mut ctx));
1026 }
1027
1028 #[test]
1029 fn and_or_short_circuit_left_to_right() {
1030 let (ir, root) = build(|ir| {
1031 let gate = ir.expr(PExpr::Bool(false));
1032 let la = ir.expr(PExpr::La(1));
1033 let one = ir.expr(PExpr::Int(1));
1034 let la_check = ir.expr(PExpr::Cmp(CmpOp::Eq, la, one));
1035 ir.expr(PExpr::And([gate, la_check].into()))
1036 });
1037 let mut ctx = MockCtx::default();
1038 assert!(!eval_pred(&ir, root, &mut ctx));
1039 assert_eq!(ctx.la_calls, 0, "false gate must short-circuit la()");
1040
1041 let (ir, root) = build(|ir| {
1042 let gate = ir.expr(PExpr::Bool(true));
1043 let la = ir.expr(PExpr::La(1));
1044 let one = ir.expr(PExpr::Int(1));
1045 let la_check = ir.expr(PExpr::Cmp(CmpOp::Eq, la, one));
1046 ir.expr(PExpr::Or([gate, la_check].into()))
1047 });
1048 let mut ctx = MockCtx::default();
1049 assert!(eval_pred(&ir, root, &mut ctx));
1050 assert_eq!(ctx.la_calls, 0, "true gate must short-circuit la()");
1051 }
1052
1053 #[test]
1054 fn token_index_adjacency_and_lookahead_type() {
1055 let (ir, root) = build(|ir| ir.expr(PExpr::TokenIndexAdjacent));
1056 let mut ctx = MockCtx {
1057 adjacent: true,
1058 ..MockCtx::default()
1059 };
1060 assert!(eval_pred(&ir, root, &mut ctx));
1061 ctx.adjacent = false;
1062 assert!(!eval_pred(&ir, root, &mut ctx));
1063
1064 let (ir, root) = build(|ir| {
1065 let la = ir.expr(PExpr::La(-1));
1066 let expected = ir.expr(PExpr::Int(12));
1067 ir.expr(PExpr::Cmp(CmpOp::Ne, la, expected))
1068 });
1069 let mut ctx = MockCtx {
1070 tokens: vec![(12, None)],
1071 ..MockCtx::default()
1072 };
1073 assert!(!eval_pred(&ir, root, &mut ctx));
1074 ctx.tokens = vec![(13, None)];
1075 assert!(eval_pred(&ir, root, &mut ctx));
1076 }
1077
1078 #[test]
1079 fn lexer_column_predicates() {
1080 let (ir, root) = build(|ir| {
1081 let column = ir.expr(PExpr::Column);
1082 let limit = ir.expr(PExpr::Int(4));
1083 ir.expr(PExpr::Cmp(CmpOp::Ge, column, limit))
1084 });
1085 let mut ctx = MockCtx {
1086 column: Some(5),
1087 ..MockCtx::default()
1088 };
1089 assert!(eval_pred(&ir, root, &mut ctx));
1090 ctx.column = Some(3);
1091 assert!(!eval_pred(&ir, root, &mut ctx));
1092 ctx.column = None;
1094 assert!(!eval_pred(&ir, root, &mut ctx));
1095
1096 let (ir, root) = build(|ir| {
1097 let start = ir.expr(PExpr::TokenStartColumn);
1098 let zero = ir.expr(PExpr::Int(0));
1099 ir.expr(PExpr::Cmp(CmpOp::Eq, start, zero))
1100 });
1101 let mut ctx = MockCtx {
1102 token_start_column: Some(0),
1103 ..MockCtx::default()
1104 };
1105 assert!(eval_pred(&ir, root, &mut ctx));
1106 }
1107
1108 #[test]
1109 fn lexer_text_so_far_comparison() {
1110 let (ir, root) = build(|ir| {
1111 let text = ir.expr(PExpr::TokenTextSoFar);
1112 let literal = ir.intern("aa");
1113 let literal = ir.expr(PExpr::Str(literal));
1114 ir.expr(PExpr::Cmp(CmpOp::Eq, text, literal))
1115 });
1116 let mut ctx = MockCtx {
1117 text_so_far: Some("aa".to_owned()),
1118 ..MockCtx::default()
1119 };
1120 assert!(eval_pred(&ir, root, &mut ctx));
1121 ctx.text_so_far = Some("ab".to_owned());
1122 assert!(!eval_pred(&ir, root, &mut ctx));
1123 }
1124
1125 #[test]
1126 fn hooks_defer_to_context() {
1127 let (ir, root) = build(|ir| ir.expr(PExpr::Hook(HookId(0))));
1128 let mut ctx = MockCtx {
1129 hook_results: vec![true],
1130 ..MockCtx::default()
1131 };
1132 assert!(eval_pred(&ir, root, &mut ctx));
1133 assert_eq!(ctx.hook_calls, vec![HookId(0)]);
1134 }
1135
1136 #[test]
1137 fn statements_mutate_members_and_returns() {
1138 let mut ir = SemIr::new();
1139 let five = ir.expr(PExpr::Int(5));
1140 let set = ir.stmt(AStmt::SetMember(1, five));
1141 let two = ir.expr(PExpr::Int(2));
1142 let add = ir.stmt(AStmt::AddMember(1, two));
1143 let member = ir.expr(PExpr::Member(1));
1144 let name = ir.intern("y");
1145 let ret = ir.stmt(AStmt::SetReturn(name, member));
1146 let seq = ir.stmt(AStmt::Seq([set, add, ret].into()));
1147
1148 let mut ctx = MockCtx::default();
1149 exec_stmt(&ir, seq, &mut ctx);
1150
1151 assert_eq!(ctx.members.get(&1), Some(&7));
1152 assert_eq!(ctx.returns.get("y"), Some(&7));
1153 }
1154
1155 #[test]
1160 fn stack_member_push_pop_and_empty_reads() {
1161 let mut ir = SemIr::new();
1162 let verbatium = ir.expr(PExpr::Bool(true));
1163 let push_true = ir.stmt(AStmt::PushMember(0, verbatium));
1164 let regular = ir.expr(PExpr::Bool(false));
1165 let push_false = ir.stmt(AStmt::PushMember(0, regular));
1166 let pop = ir.stmt(AStmt::PopMember(0));
1167 let top = ir.expr(PExpr::MemberTop(0));
1168 let depth = ir.expr(PExpr::MemberLen(0));
1169
1170 let mut ctx = MockCtx::default();
1171
1172 assert!(!eval_pred(&ir, top, &mut ctx));
1174 assert_eq!(eval_value(&ir, depth, &mut ctx), Value::Int(0));
1175
1176 exec_stmt(&ir, push_true, &mut ctx);
1177 assert!(
1178 eval_pred(&ir, top, &mut ctx),
1179 "pushed true reads back truthy"
1180 );
1181 assert_eq!(eval_value(&ir, depth, &mut ctx), Value::Int(1));
1182
1183 exec_stmt(&ir, push_false, &mut ctx);
1185 assert!(!eval_pred(&ir, top, &mut ctx));
1186 assert_eq!(eval_value(&ir, depth, &mut ctx), Value::Int(2));
1187
1188 exec_stmt(&ir, pop, &mut ctx);
1190 assert!(eval_pred(&ir, top, &mut ctx));
1191 assert_eq!(eval_value(&ir, depth, &mut ctx), Value::Int(1));
1192
1193 exec_stmt(&ir, pop, &mut ctx);
1194 assert_eq!(eval_value(&ir, top, &mut ctx), Value::Null);
1195 assert_eq!(eval_value(&ir, depth, &mut ctx), Value::Int(0));
1196
1197 exec_stmt(&ir, pop, &mut ctx);
1199 assert_eq!(eval_value(&ir, top, &mut ctx), Value::Null);
1200 assert_eq!(eval_value(&ir, depth, &mut ctx), Value::Int(0));
1201 }
1202
1203 #[test]
1206 fn bool_member_assignment_round_trips_through_truthiness() {
1207 let mut ir = SemIr::new();
1208 let yes = ir.expr(PExpr::Bool(true));
1209 let set_true = ir.stmt(AStmt::SetMember(3, yes));
1210 let no = ir.expr(PExpr::Bool(false));
1211 let set_false = ir.stmt(AStmt::SetMember(3, no));
1212 let read = ir.expr(PExpr::Member(3));
1213
1214 let mut ctx = MockCtx::default();
1215 exec_stmt(&ir, set_true, &mut ctx);
1216 assert!(eval_pred(&ir, read, &mut ctx));
1217 exec_stmt(&ir, set_false, &mut ctx);
1218 assert!(!eval_pred(&ir, read, &mut ctx));
1219 }
1220
1221 #[test]
1225 fn emptied_stack_slot_compares_equal_to_untouched_env() {
1226 let mut env = MemberEnv::new();
1227 env.push_stack(1, 7);
1228 assert_ne!(env, MemberEnv::new());
1229 assert_eq!(env.pop_stack(1), Some(7));
1230 assert_eq!(env, MemberEnv::new(), "emptied stack must canonicalize");
1231 assert!(env.is_empty());
1232 assert_eq!(env.pop_stack(1), None);
1234 assert_eq!(env, MemberEnv::new());
1235 }
1236
1237 #[test]
1240 fn scalar_and_stack_slots_do_not_alias() {
1241 let mut env = MemberEnv::new();
1242 env.set_scalar(0, 5);
1243 env.push_stack(0, 9);
1244 assert_eq!(env.scalar(0), Some(5));
1245 assert_eq!(env.stack_top(0), Some(9));
1246 assert_eq!(env.pop_stack(0), Some(9));
1247 assert_eq!(env.scalar(0), Some(5), "popping a stack leaves scalars");
1248 }
1249
1250 #[test]
1251 fn string_interning_deduplicates() {
1252 let mut ir = SemIr::new();
1253 let first = ir.intern("of");
1254 let second = ir.intern("of");
1255 let third = ir.intern("in");
1256 assert_eq!(first, second);
1257 assert_ne!(first, third);
1258 assert_eq!(ir.text(third), "in");
1259 }
1260}