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