1use std::borrow::Cow;
42use std::fmt::Debug;
43
44#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
46pub struct ExprId(u32);
47
48impl ExprId {
49 #[must_use]
51 pub const fn new(index: u32) -> Self {
52 Self(index)
53 }
54
55 #[must_use]
57 pub const fn index(self) -> usize {
58 self.0 as usize
59 }
60}
61
62#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
64pub struct StmtId(u32);
65
66impl StmtId {
67 #[must_use]
69 pub const fn new(index: u32) -> Self {
70 Self(index)
71 }
72
73 #[must_use]
75 pub const fn index(self) -> usize {
76 self.0 as usize
77 }
78}
79
80#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
82pub struct StrId(u32);
83
84impl StrId {
85 #[must_use]
87 pub const fn new(index: u32) -> Self {
88 Self(index)
89 }
90
91 #[must_use]
93 pub const fn index(self) -> usize {
94 self.0 as usize
95 }
96}
97
98#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
105pub struct HookId(u32);
106
107impl HookId {
108 #[must_use]
110 pub const fn new(index: u32) -> Self {
111 Self(index)
112 }
113
114 #[must_use]
116 pub const fn index(self) -> usize {
117 self.0 as usize
118 }
119}
120
121#[derive(Clone, Copy, Debug, Eq, PartialEq)]
123pub enum CmpOp {
124 Eq,
125 Ne,
126 Lt,
127 Le,
128 Gt,
129 Ge,
130}
131
132#[derive(Clone, Copy, Debug, Eq, PartialEq)]
134pub enum ArithOp {
135 Add,
136 Sub,
137 Mul,
138 Div,
139 Mod,
140}
141
142#[derive(Clone, Debug, Eq, PartialEq)]
149pub enum PExpr {
150 Bool(bool),
152 Int(i64),
154 Str(StrId),
156 La(isize),
158 TokenText(isize),
160 TokenIndexAdjacent,
164 CtxRuleText(usize),
167 Member(usize),
169 LocalArg,
172 Column,
174 TokenStartColumn,
176 TokenTextSoFar,
178 IsNull(ExprId),
181 Not(ExprId),
183 And(Box<[ExprId]>),
185 Or(Box<[ExprId]>),
187 Cmp(CmpOp, ExprId, ExprId),
189 Arith(ArithOp, ExprId, ExprId),
191 Hook(HookId),
193 EvalTrace(bool),
198}
199
200#[derive(Clone, Debug, Eq, PartialEq)]
206pub enum AStmt {
207 SetMember(usize, ExprId),
209 AddMember(usize, ExprId),
211 SetReturn(StrId, ExprId),
213 Seq(Box<[StmtId]>),
215 Hook(HookId),
217}
218
219#[allow(variant_size_differences)]
221#[derive(Clone, Copy, Debug, Eq, PartialEq)]
222pub enum Value {
223 Null,
225 Bool(bool),
226 Int(i64),
227}
228
229impl Value {
230 #[must_use]
232 pub const fn truthy(self) -> bool {
233 match self {
234 Self::Null => false,
235 Self::Bool(value) => value,
236 Self::Int(value) => value != 0,
237 }
238 }
239}
240
241pub trait PredContext {
248 type TokenText<'a>: AsRef<str>
249 where
250 Self: 'a;
251
252 fn la(&mut self, offset: isize) -> i64;
254 fn token_text(&mut self, offset: isize) -> Option<Self::TokenText<'_>>;
256 fn token_index_adjacent(&mut self) -> bool;
258 fn ctx_rule_text(&self, rule_index: usize) -> Option<String>;
260 fn member(&self, member: usize) -> Option<i64>;
262 fn local_arg(&self) -> Option<i64>;
264 fn column(&self) -> Option<i64>;
266 fn token_start_column(&self) -> Option<i64>;
268 fn token_text_so_far(&self) -> Option<String>;
270 fn hook(&mut self, hook: HookId) -> bool;
272 fn trace_bool(&mut self, value: bool) -> bool {
274 value
275 }
276}
277
278pub trait ActContext: PredContext {
280 fn set_member(&mut self, member: usize, value: i64);
282 fn set_return(&mut self, name: &str, value: i64);
284 fn action_hook(&mut self, hook: HookId);
286}
287
288#[derive(Clone, Debug, Default, Eq, PartialEq)]
293pub struct SemIr {
294 exprs: Vec<PExpr>,
295 stmts: Vec<AStmt>,
296 strings: Vec<Box<str>>,
297}
298
299impl SemIr {
300 #[must_use]
301 pub fn new() -> Self {
302 Self::default()
303 }
304
305 pub fn expr(&mut self, node: PExpr) -> ExprId {
307 let id = ExprId(u32::try_from(self.exprs.len()).expect("expression arena fits in u32"));
308 self.exprs.push(node);
309 id
310 }
311
312 pub fn stmt(&mut self, node: AStmt) -> StmtId {
314 let id = StmtId(u32::try_from(self.stmts.len()).expect("statement arena fits in u32"));
315 self.stmts.push(node);
316 id
317 }
318
319 pub fn intern(&mut self, value: &str) -> StrId {
321 if let Some(position) = self.strings.iter().position(|entry| &**entry == value) {
322 return StrId(u32::try_from(position).expect("string pool fits in u32"));
323 }
324 let id = StrId(u32::try_from(self.strings.len()).expect("string pool fits in u32"));
325 self.strings.push(value.into());
326 id
327 }
328
329 #[must_use]
331 pub fn text(&self, id: StrId) -> &str {
332 &self.strings[id.0 as usize]
333 }
334
335 fn node(&self, id: ExprId) -> &PExpr {
336 &self.exprs[id.0 as usize]
337 }
338
339 fn stmt_node(&self, id: StmtId) -> &AStmt {
340 &self.stmts[id.0 as usize]
341 }
342}
343
344pub fn eval_pred<C: PredContext>(ir: &SemIr, expr: ExprId, ctx: &mut C) -> bool {
350 eval_value(ir, expr, ctx).truthy()
351}
352
353pub fn exec_stmt<C: ActContext>(ir: &SemIr, stmt: StmtId, ctx: &mut C) {
355 match ir.stmt_node(stmt) {
356 AStmt::SetMember(member, value) => {
357 let value = int_or_zero(eval_value(ir, *value, ctx));
358 ctx.set_member(*member, value);
359 }
360 AStmt::AddMember(member, delta) => {
361 let delta = int_or_zero(eval_value(ir, *delta, ctx));
362 let current = ctx.member(*member).unwrap_or_default();
363 ctx.set_member(*member, current + delta);
364 }
365 AStmt::SetReturn(name, value) => {
366 let value = int_or_zero(eval_value(ir, *value, ctx));
367 let name = ir.text(*name).to_owned();
368 ctx.set_return(&name, value);
369 }
370 AStmt::Seq(stmts) => {
371 for stmt in stmts {
372 exec_stmt(ir, *stmt, ctx);
373 }
374 }
375 AStmt::Hook(hook) => ctx.action_hook(*hook),
376 }
377}
378
379const fn int_or_zero(value: Value) -> i64 {
380 match value {
381 Value::Int(value) => value,
382 Value::Null | Value::Bool(_) => 0,
383 }
384}
385
386fn eval_value<C: PredContext>(ir: &SemIr, expr: ExprId, ctx: &mut C) -> Value {
387 match ir.node(expr) {
388 PExpr::Str(_) | PExpr::TokenText(_) | PExpr::CtxRuleText(_) | PExpr::TokenTextSoFar => {
391 debug_assert!(false, "text-valued node evaluated outside a comparison");
392 Value::Null
393 }
394 PExpr::Bool(value) => Value::Bool(*value),
395 PExpr::Int(value) => Value::Int(*value),
396 PExpr::La(offset) => Value::Int(ctx.la(*offset)),
397 PExpr::TokenIndexAdjacent => Value::Bool(ctx.token_index_adjacent()),
398 PExpr::Member(member) => ctx.member(*member).map_or(Value::Null, Value::Int),
399 PExpr::LocalArg => ctx.local_arg().map_or(Value::Null, Value::Int),
400 PExpr::Column => ctx.column().map_or(Value::Null, Value::Int),
401 PExpr::TokenStartColumn => ctx.token_start_column().map_or(Value::Null, Value::Int),
402 PExpr::IsNull(inner) => Value::Bool(eval_is_null(ir, *inner, ctx)),
403 PExpr::Not(inner) => Value::Bool(!eval_value(ir, *inner, ctx).truthy()),
404 PExpr::And(children) => Value::Bool(
405 children
406 .iter()
407 .all(|child| eval_value(ir, *child, ctx).truthy()),
408 ),
409 PExpr::Or(children) => Value::Bool(
410 children
411 .iter()
412 .any(|child| eval_value(ir, *child, ctx).truthy()),
413 ),
414 PExpr::Cmp(op, lhs, rhs) => eval_cmp(ir, *op, *lhs, *rhs, ctx),
415 PExpr::Arith(op, lhs, rhs) => eval_arith(ir, *op, *lhs, *rhs, ctx),
416 PExpr::Hook(hook) => Value::Bool(ctx.hook(*hook)),
417 PExpr::EvalTrace(value) => Value::Bool(ctx.trace_bool(*value)),
418 }
419}
420
421fn eval_is_null<C: PredContext>(ir: &SemIr, inner: ExprId, ctx: &mut C) -> bool {
422 if let Some(source) = text_source(ir, inner) {
423 return resolve_owned_text(ir, source, ctx).is_none();
424 }
425 eval_value(ir, inner, ctx) == Value::Null
426}
427
428fn eval_cmp<C: PredContext>(ir: &SemIr, op: CmpOp, lhs: ExprId, rhs: ExprId, ctx: &mut C) -> Value {
429 let left_source = text_source(ir, lhs);
430 let right_source = text_source(ir, rhs);
431 if left_source.is_some() || right_source.is_some() {
432 return eval_text_cmp(ir, op, (lhs, left_source), (rhs, right_source), ctx);
433 }
434 let left = eval_value(ir, lhs, ctx);
435 let right = eval_value(ir, rhs, ctx);
436 Value::Bool(match (left, right) {
437 (Value::Null, Value::Null) => cmp_on_equality(op, true),
438 (Value::Null, _) | (_, Value::Null) => cmp_on_equality(op, false),
439 (Value::Bool(left), Value::Bool(right)) => cmp_on_equality(op, left == right),
440 (Value::Int(left), Value::Int(right)) => cmp_ints(op, left, right),
441 (Value::Bool(_), Value::Int(_)) | (Value::Int(_), Value::Bool(_)) => {
442 cmp_on_equality(op, false)
443 }
444 })
445}
446
447const fn cmp_on_equality(op: CmpOp, equal: bool) -> bool {
450 match op {
451 CmpOp::Eq => equal,
452 CmpOp::Ne => !equal,
453 CmpOp::Lt | CmpOp::Le | CmpOp::Gt | CmpOp::Ge => false,
454 }
455}
456
457const fn cmp_ints(op: CmpOp, left: i64, right: i64) -> bool {
458 match op {
459 CmpOp::Eq => left == right,
460 CmpOp::Ne => left != right,
461 CmpOp::Lt => left < right,
462 CmpOp::Le => left <= right,
463 CmpOp::Gt => left > right,
464 CmpOp::Ge => left >= right,
465 }
466}
467
468#[derive(Clone, Copy, Debug)]
475enum TextSource {
476 Literal(StrId),
477 Lookahead(isize),
478 CtxRule(usize),
479 SoFar,
480}
481
482fn text_source(ir: &SemIr, expr: ExprId) -> Option<TextSource> {
483 match ir.node(expr) {
484 PExpr::Str(id) => Some(TextSource::Literal(*id)),
485 PExpr::TokenText(offset) => Some(TextSource::Lookahead(*offset)),
486 PExpr::CtxRuleText(rule_index) => Some(TextSource::CtxRule(*rule_index)),
487 PExpr::TokenTextSoFar => Some(TextSource::SoFar),
488 _ => None,
489 }
490}
491
492fn resolve_static_text<'ir, C: PredContext>(
494 ir: &'ir SemIr,
495 source: TextSource,
496 ctx: &C,
497) -> Option<Cow<'ir, str>> {
498 match source {
499 TextSource::Literal(id) => Some(Cow::Borrowed(ir.text(id))),
500 TextSource::Lookahead(_) => unreachable!("lookahead operands are resolved last"),
501 TextSource::CtxRule(rule_index) => ctx.ctx_rule_text(rule_index).map(Cow::Owned),
502 TextSource::SoFar => ctx.token_text_so_far().map(Cow::Owned),
503 }
504}
505
506fn resolve_owned_text<C: PredContext>(
508 ir: &SemIr,
509 source: TextSource,
510 ctx: &mut C,
511) -> Option<String> {
512 match source {
513 TextSource::Lookahead(offset) => {
514 ctx.token_text(offset).map(|text| text.as_ref().to_owned())
515 }
516 other => resolve_static_text(ir, other, ctx).map(Cow::into_owned),
517 }
518}
519
520fn eval_text_cmp<C: PredContext>(
521 ir: &SemIr,
522 op: CmpOp,
523 (lhs, left_source): (ExprId, Option<TextSource>),
524 (rhs, right_source): (ExprId, Option<TextSource>),
525 ctx: &mut C,
526) -> Value {
527 let (Some(left_source), Some(right_source)) = (left_source, right_source) else {
530 debug_assert!(false, "text operand compared with non-text operand");
531 let _ = (lhs, rhs);
532 return Value::Bool(cmp_on_equality(op, false));
533 };
534 Value::Bool(match (left_source, right_source) {
535 (TextSource::Lookahead(left), TextSource::Lookahead(right)) => {
536 let left = ctx.token_text(left).map(|text| text.as_ref().to_owned());
539 let right = ctx.token_text(right);
540 cmp_texts(op, left.as_deref(), right.as_ref().map(AsRef::as_ref))
541 }
542 (TextSource::Lookahead(offset), other) => {
543 let right = resolve_static_text(ir, other, ctx);
544 let left = ctx.token_text(offset);
545 cmp_texts(op, left.as_ref().map(AsRef::as_ref), right.as_deref())
546 }
547 (other, TextSource::Lookahead(offset)) => {
548 let left = resolve_static_text(ir, other, ctx);
549 let right = ctx.token_text(offset);
550 cmp_texts(op, left.as_deref(), right.as_ref().map(AsRef::as_ref))
551 }
552 (left, right) => {
553 let left = resolve_static_text(ir, left, ctx);
554 let right = resolve_static_text(ir, right, ctx);
555 cmp_texts(op, left.as_deref(), right.as_deref())
556 }
557 })
558}
559
560fn cmp_texts(op: CmpOp, left: Option<&str>, right: Option<&str>) -> bool {
561 match (left, right) {
562 (None, None) => cmp_on_equality(op, true),
563 (None, Some(_)) | (Some(_), None) => cmp_on_equality(op, false),
564 (Some(left), Some(right)) => match op {
565 CmpOp::Eq => left == right,
566 CmpOp::Ne => left != right,
567 CmpOp::Lt => left < right,
568 CmpOp::Le => left <= right,
569 CmpOp::Gt => left > right,
570 CmpOp::Ge => left >= right,
571 },
572 }
573}
574
575fn eval_arith<C: PredContext>(
576 ir: &SemIr,
577 op: ArithOp,
578 lhs: ExprId,
579 rhs: ExprId,
580 ctx: &mut C,
581) -> Value {
582 let (Value::Int(left), Value::Int(right)) =
583 (eval_value(ir, lhs, ctx), eval_value(ir, rhs, ctx))
584 else {
585 return Value::Null;
586 };
587 let result = match op {
588 ArithOp::Add => left.checked_add(right),
589 ArithOp::Sub => left.checked_sub(right),
590 ArithOp::Mul => left.checked_mul(right),
591 ArithOp::Div => left.checked_div(right),
592 ArithOp::Mod => left.checked_rem(right),
593 };
594 result.map_or(Value::Null, Value::Int)
595}
596
597#[cfg(test)]
598mod tests {
599 use super::{
600 AStmt, ActContext, ArithOp, CmpOp, ExprId, HookId, PExpr, PredContext, SemIr, eval_pred,
601 exec_stmt,
602 };
603 use std::collections::BTreeMap;
604
605 #[derive(Debug, Default)]
607 struct MockCtx {
608 tokens: Vec<(i64, Option<&'static str>)>,
609 adjacent: bool,
610 ctx_rule_texts: BTreeMap<usize, String>,
611 members: BTreeMap<usize, i64>,
612 local_arg: Option<i64>,
613 column: Option<i64>,
614 token_start_column: Option<i64>,
615 text_so_far: Option<String>,
616 hook_results: Vec<bool>,
617 hook_calls: Vec<HookId>,
618 la_calls: usize,
619 returns: BTreeMap<String, i64>,
620 }
621
622 impl PredContext for MockCtx {
623 type TokenText<'a>
624 = &'a str
625 where
626 Self: 'a;
627
628 fn la(&mut self, offset: isize) -> i64 {
629 self.la_calls += 1;
630 self.lookup(offset).map_or(-1, |(token_type, _)| token_type)
631 }
632
633 fn token_text(&mut self, offset: isize) -> Option<Self::TokenText<'_>> {
634 self.lookup(offset).and_then(|(_, text)| text)
635 }
636
637 fn token_index_adjacent(&mut self) -> bool {
638 self.adjacent
639 }
640
641 fn ctx_rule_text(&self, rule_index: usize) -> Option<String> {
642 self.ctx_rule_texts.get(&rule_index).cloned()
643 }
644
645 fn member(&self, member: usize) -> Option<i64> {
646 self.members.get(&member).copied()
647 }
648
649 fn local_arg(&self) -> Option<i64> {
650 self.local_arg
651 }
652
653 fn column(&self) -> Option<i64> {
654 self.column
655 }
656
657 fn token_start_column(&self) -> Option<i64> {
658 self.token_start_column
659 }
660
661 fn token_text_so_far(&self) -> Option<String> {
662 self.text_so_far.clone()
663 }
664
665 fn hook(&mut self, hook: HookId) -> bool {
666 self.hook_calls.push(hook);
667 self.hook_results[hook.index()]
668 }
669 }
670
671 impl ActContext for MockCtx {
672 fn set_member(&mut self, member: usize, value: i64) {
673 self.members.insert(member, value);
674 }
675
676 fn set_return(&mut self, name: &str, value: i64) {
677 self.returns.insert(name.to_owned(), value);
678 }
679
680 fn action_hook(&mut self, hook: HookId) {
681 self.hook_calls.push(hook);
682 }
683 }
684
685 impl MockCtx {
686 fn lookup(&self, offset: isize) -> Option<(i64, Option<&'static str>)> {
687 let index = if offset > 0 {
689 usize::try_from(offset - 1).ok()?
690 } else {
691 self.tokens.len().checked_sub(offset.unsigned_abs())?
692 };
693 self.tokens.get(index).copied()
694 }
695 }
696
697 fn build(build: impl FnOnce(&mut SemIr) -> ExprId) -> (SemIr, ExprId) {
698 let mut ir = SemIr::new();
699 let root = build(&mut ir);
700 (ir, root)
701 }
702
703 #[test]
704 fn literals_and_truthiness() {
705 for (value, expected) in [(true, true), (false, false)] {
706 let (ir, root) = build(|ir| ir.expr(PExpr::Bool(value)));
707 assert_eq!(eval_pred(&ir, root, &mut MockCtx::default()), expected);
708 }
709 let (ir, root) = build(|ir| ir.expr(PExpr::Int(2)));
710 assert!(eval_pred(&ir, root, &mut MockCtx::default()));
711 let (ir, root) = build(|ir| ir.expr(PExpr::Int(0)));
712 assert!(!eval_pred(&ir, root, &mut MockCtx::default()));
713 }
714
715 #[test]
716 fn lookahead_text_equals_literal_and_absent_token_fails() {
717 let (ir, root) = build(|ir| {
718 let text = ir.expr(PExpr::TokenText(1));
719 let literal = ir.intern("of");
720 let literal = ir.expr(PExpr::Str(literal));
721 ir.expr(PExpr::Cmp(CmpOp::Eq, text, literal))
722 });
723
724 let mut ctx = MockCtx {
725 tokens: vec![(7, Some("of"))],
726 ..MockCtx::default()
727 };
728 assert!(eval_pred(&ir, root, &mut ctx));
729
730 ctx.tokens = vec![(7, Some("in"))];
731 assert!(!eval_pred(&ir, root, &mut ctx));
732
733 ctx.tokens = Vec::new();
735 assert!(!eval_pred(&ir, root, &mut ctx));
736 }
737
738 #[test]
739 fn ctx_rule_text_not_equals_passes_when_child_absent() {
740 let (ir, root) = build(|ir| {
741 let child = ir.expr(PExpr::CtxRuleText(4));
742 let literal = ir.intern("static");
743 let literal = ir.expr(PExpr::Str(literal));
744 ir.expr(PExpr::Cmp(CmpOp::Ne, child, literal))
745 });
746
747 assert!(eval_pred(&ir, root, &mut MockCtx::default()));
749
750 let mut ctx = MockCtx {
751 ctx_rule_texts: std::iter::once((4, "static".to_owned())).collect(),
752 ..MockCtx::default()
753 };
754 assert!(!eval_pred(&ir, root, &mut ctx));
755
756 ctx.ctx_rule_texts = std::iter::once((4, "dynamic".to_owned())).collect();
757 assert!(eval_pred(&ir, root, &mut ctx));
758 }
759
760 #[test]
761 fn absent_local_arg_composes_non_restrictive_guard() {
762 let (ir, root) = build(|ir| {
765 let arg = ir.expr(PExpr::LocalArg);
766 let absent = ir.expr(PExpr::IsNull(arg));
767 let value = ir.expr(PExpr::Int(2));
768 let equals = ir.expr(PExpr::Cmp(CmpOp::Eq, arg, value));
769 ir.expr(PExpr::Or([absent, equals].into()))
770 });
771
772 assert!(eval_pred(&ir, root, &mut MockCtx::default()));
773 let mut ctx = MockCtx {
774 local_arg: Some(2),
775 ..MockCtx::default()
776 };
777 assert!(eval_pred(&ir, root, &mut ctx));
778 ctx.local_arg = Some(3);
779 assert!(!eval_pred(&ir, root, &mut ctx));
780 }
781
782 #[test]
783 fn member_modulo_comparison() {
784 let (ir, root) = build(|ir| {
785 let member = ir.expr(PExpr::Member(0));
786 let modulus = ir.expr(PExpr::Int(2));
787 let remainder = ir.expr(PExpr::Arith(ArithOp::Mod, member, modulus));
788 let expected = ir.expr(PExpr::Int(0));
789 ir.expr(PExpr::Cmp(CmpOp::Eq, remainder, expected))
790 });
791
792 let mut ctx = MockCtx {
793 members: std::iter::once((0, 4)).collect(),
794 ..MockCtx::default()
795 };
796 assert!(eval_pred(&ir, root, &mut ctx));
797 ctx.members.insert(0, 5);
798 assert!(!eval_pred(&ir, root, &mut ctx));
799 ctx.members.clear();
801 assert!(!eval_pred(&ir, root, &mut ctx));
802 }
803
804 #[test]
805 fn arithmetic_null_propagation_and_division_by_zero() {
806 let (ir, root) = build(|ir| {
807 let member = ir.expr(PExpr::Member(9));
808 let zero = ir.expr(PExpr::Int(0));
809 let modulo = ir.expr(PExpr::Arith(ArithOp::Mod, member, zero));
810 ir.expr(PExpr::IsNull(modulo))
811 });
812 let mut ctx = MockCtx {
814 members: std::iter::once((9, 3)).collect(),
815 ..MockCtx::default()
816 };
817 assert!(eval_pred(&ir, root, &mut ctx));
818 }
819
820 #[test]
821 fn and_or_short_circuit_left_to_right() {
822 let (ir, root) = build(|ir| {
823 let gate = ir.expr(PExpr::Bool(false));
824 let la = ir.expr(PExpr::La(1));
825 let one = ir.expr(PExpr::Int(1));
826 let la_check = ir.expr(PExpr::Cmp(CmpOp::Eq, la, one));
827 ir.expr(PExpr::And([gate, la_check].into()))
828 });
829 let mut ctx = MockCtx::default();
830 assert!(!eval_pred(&ir, root, &mut ctx));
831 assert_eq!(ctx.la_calls, 0, "false gate must short-circuit la()");
832
833 let (ir, root) = build(|ir| {
834 let gate = ir.expr(PExpr::Bool(true));
835 let la = ir.expr(PExpr::La(1));
836 let one = ir.expr(PExpr::Int(1));
837 let la_check = ir.expr(PExpr::Cmp(CmpOp::Eq, la, one));
838 ir.expr(PExpr::Or([gate, la_check].into()))
839 });
840 let mut ctx = MockCtx::default();
841 assert!(eval_pred(&ir, root, &mut ctx));
842 assert_eq!(ctx.la_calls, 0, "true gate must short-circuit la()");
843 }
844
845 #[test]
846 fn token_index_adjacency_and_lookahead_type() {
847 let (ir, root) = build(|ir| ir.expr(PExpr::TokenIndexAdjacent));
848 let mut ctx = MockCtx {
849 adjacent: true,
850 ..MockCtx::default()
851 };
852 assert!(eval_pred(&ir, root, &mut ctx));
853 ctx.adjacent = false;
854 assert!(!eval_pred(&ir, root, &mut ctx));
855
856 let (ir, root) = build(|ir| {
857 let la = ir.expr(PExpr::La(-1));
858 let expected = ir.expr(PExpr::Int(12));
859 ir.expr(PExpr::Cmp(CmpOp::Ne, la, expected))
860 });
861 let mut ctx = MockCtx {
862 tokens: vec![(12, None)],
863 ..MockCtx::default()
864 };
865 assert!(!eval_pred(&ir, root, &mut ctx));
866 ctx.tokens = vec![(13, None)];
867 assert!(eval_pred(&ir, root, &mut ctx));
868 }
869
870 #[test]
871 fn lexer_column_predicates() {
872 let (ir, root) = build(|ir| {
873 let column = ir.expr(PExpr::Column);
874 let limit = ir.expr(PExpr::Int(4));
875 ir.expr(PExpr::Cmp(CmpOp::Ge, column, limit))
876 });
877 let mut ctx = MockCtx {
878 column: Some(5),
879 ..MockCtx::default()
880 };
881 assert!(eval_pred(&ir, root, &mut ctx));
882 ctx.column = Some(3);
883 assert!(!eval_pred(&ir, root, &mut ctx));
884 ctx.column = None;
886 assert!(!eval_pred(&ir, root, &mut ctx));
887
888 let (ir, root) = build(|ir| {
889 let start = ir.expr(PExpr::TokenStartColumn);
890 let zero = ir.expr(PExpr::Int(0));
891 ir.expr(PExpr::Cmp(CmpOp::Eq, start, zero))
892 });
893 let mut ctx = MockCtx {
894 token_start_column: Some(0),
895 ..MockCtx::default()
896 };
897 assert!(eval_pred(&ir, root, &mut ctx));
898 }
899
900 #[test]
901 fn lexer_text_so_far_comparison() {
902 let (ir, root) = build(|ir| {
903 let text = ir.expr(PExpr::TokenTextSoFar);
904 let literal = ir.intern("aa");
905 let literal = ir.expr(PExpr::Str(literal));
906 ir.expr(PExpr::Cmp(CmpOp::Eq, text, literal))
907 });
908 let mut ctx = MockCtx {
909 text_so_far: Some("aa".to_owned()),
910 ..MockCtx::default()
911 };
912 assert!(eval_pred(&ir, root, &mut ctx));
913 ctx.text_so_far = Some("ab".to_owned());
914 assert!(!eval_pred(&ir, root, &mut ctx));
915 }
916
917 #[test]
918 fn hooks_defer_to_context() {
919 let (ir, root) = build(|ir| ir.expr(PExpr::Hook(HookId(0))));
920 let mut ctx = MockCtx {
921 hook_results: vec![true],
922 ..MockCtx::default()
923 };
924 assert!(eval_pred(&ir, root, &mut ctx));
925 assert_eq!(ctx.hook_calls, vec![HookId(0)]);
926 }
927
928 #[test]
929 fn statements_mutate_members_and_returns() {
930 let mut ir = SemIr::new();
931 let five = ir.expr(PExpr::Int(5));
932 let set = ir.stmt(AStmt::SetMember(1, five));
933 let two = ir.expr(PExpr::Int(2));
934 let add = ir.stmt(AStmt::AddMember(1, two));
935 let member = ir.expr(PExpr::Member(1));
936 let name = ir.intern("y");
937 let ret = ir.stmt(AStmt::SetReturn(name, member));
938 let seq = ir.stmt(AStmt::Seq([set, add, ret].into()));
939
940 let mut ctx = MockCtx::default();
941 exec_stmt(&ir, seq, &mut ctx);
942
943 assert_eq!(ctx.members.get(&1), Some(&7));
944 assert_eq!(ctx.returns.get("y"), Some(&7));
945 }
946
947 #[test]
948 fn string_interning_deduplicates() {
949 let mut ir = SemIr::new();
950 let first = ir.intern("of");
951 let second = ir.intern("of");
952 let third = ir.intern("in");
953 assert_eq!(first, second);
954 assert_ne!(first, third);
955 assert_eq!(ir.text(third), "in");
956 }
957}