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 fn la(&mut self, offset: isize) -> i64;
250 fn token_text(&mut self, offset: isize) -> Option<&str>;
252 fn token_index_adjacent(&mut self) -> bool;
254 fn ctx_rule_text(&self, rule_index: usize) -> Option<String>;
256 fn member(&self, member: usize) -> Option<i64>;
258 fn local_arg(&self) -> Option<i64>;
260 fn column(&self) -> Option<i64>;
262 fn token_start_column(&self) -> Option<i64>;
264 fn token_text_so_far(&self) -> Option<String>;
266 fn hook(&mut self, hook: HookId) -> bool;
268 fn trace_bool(&mut self, value: bool) -> bool {
270 value
271 }
272}
273
274pub trait ActContext: PredContext {
276 fn set_member(&mut self, member: usize, value: i64);
278 fn set_return(&mut self, name: &str, value: i64);
280 fn action_hook(&mut self, hook: HookId);
282}
283
284#[derive(Clone, Debug, Default, Eq, PartialEq)]
289pub struct SemIr {
290 exprs: Vec<PExpr>,
291 stmts: Vec<AStmt>,
292 strings: Vec<Box<str>>,
293}
294
295impl SemIr {
296 #[must_use]
297 pub fn new() -> Self {
298 Self::default()
299 }
300
301 pub fn expr(&mut self, node: PExpr) -> ExprId {
303 let id = ExprId(u32::try_from(self.exprs.len()).expect("expression arena fits in u32"));
304 self.exprs.push(node);
305 id
306 }
307
308 pub fn stmt(&mut self, node: AStmt) -> StmtId {
310 let id = StmtId(u32::try_from(self.stmts.len()).expect("statement arena fits in u32"));
311 self.stmts.push(node);
312 id
313 }
314
315 pub fn intern(&mut self, value: &str) -> StrId {
317 if let Some(position) = self.strings.iter().position(|entry| &**entry == value) {
318 return StrId(u32::try_from(position).expect("string pool fits in u32"));
319 }
320 let id = StrId(u32::try_from(self.strings.len()).expect("string pool fits in u32"));
321 self.strings.push(value.into());
322 id
323 }
324
325 #[must_use]
327 pub fn text(&self, id: StrId) -> &str {
328 &self.strings[id.0 as usize]
329 }
330
331 fn node(&self, id: ExprId) -> &PExpr {
332 &self.exprs[id.0 as usize]
333 }
334
335 fn stmt_node(&self, id: StmtId) -> &AStmt {
336 &self.stmts[id.0 as usize]
337 }
338}
339
340pub fn eval_pred<C: PredContext>(ir: &SemIr, expr: ExprId, ctx: &mut C) -> bool {
346 eval_value(ir, expr, ctx).truthy()
347}
348
349pub fn exec_stmt<C: ActContext>(ir: &SemIr, stmt: StmtId, ctx: &mut C) {
351 match ir.stmt_node(stmt) {
352 AStmt::SetMember(member, value) => {
353 let value = int_or_zero(eval_value(ir, *value, ctx));
354 ctx.set_member(*member, value);
355 }
356 AStmt::AddMember(member, delta) => {
357 let delta = int_or_zero(eval_value(ir, *delta, ctx));
358 let current = ctx.member(*member).unwrap_or_default();
359 ctx.set_member(*member, current + delta);
360 }
361 AStmt::SetReturn(name, value) => {
362 let value = int_or_zero(eval_value(ir, *value, ctx));
363 let name = ir.text(*name).to_owned();
364 ctx.set_return(&name, value);
365 }
366 AStmt::Seq(stmts) => {
367 for stmt in stmts {
368 exec_stmt(ir, *stmt, ctx);
369 }
370 }
371 AStmt::Hook(hook) => ctx.action_hook(*hook),
372 }
373}
374
375const fn int_or_zero(value: Value) -> i64 {
376 match value {
377 Value::Int(value) => value,
378 Value::Null | Value::Bool(_) => 0,
379 }
380}
381
382fn eval_value<C: PredContext>(ir: &SemIr, expr: ExprId, ctx: &mut C) -> Value {
383 match ir.node(expr) {
384 PExpr::Str(_) | PExpr::TokenText(_) | PExpr::CtxRuleText(_) | PExpr::TokenTextSoFar => {
387 debug_assert!(false, "text-valued node evaluated outside a comparison");
388 Value::Null
389 }
390 PExpr::Bool(value) => Value::Bool(*value),
391 PExpr::Int(value) => Value::Int(*value),
392 PExpr::La(offset) => Value::Int(ctx.la(*offset)),
393 PExpr::TokenIndexAdjacent => Value::Bool(ctx.token_index_adjacent()),
394 PExpr::Member(member) => ctx.member(*member).map_or(Value::Null, Value::Int),
395 PExpr::LocalArg => ctx.local_arg().map_or(Value::Null, Value::Int),
396 PExpr::Column => ctx.column().map_or(Value::Null, Value::Int),
397 PExpr::TokenStartColumn => ctx.token_start_column().map_or(Value::Null, Value::Int),
398 PExpr::IsNull(inner) => Value::Bool(eval_is_null(ir, *inner, ctx)),
399 PExpr::Not(inner) => Value::Bool(!eval_value(ir, *inner, ctx).truthy()),
400 PExpr::And(children) => Value::Bool(
401 children
402 .iter()
403 .all(|child| eval_value(ir, *child, ctx).truthy()),
404 ),
405 PExpr::Or(children) => Value::Bool(
406 children
407 .iter()
408 .any(|child| eval_value(ir, *child, ctx).truthy()),
409 ),
410 PExpr::Cmp(op, lhs, rhs) => eval_cmp(ir, *op, *lhs, *rhs, ctx),
411 PExpr::Arith(op, lhs, rhs) => eval_arith(ir, *op, *lhs, *rhs, ctx),
412 PExpr::Hook(hook) => Value::Bool(ctx.hook(*hook)),
413 PExpr::EvalTrace(value) => Value::Bool(ctx.trace_bool(*value)),
414 }
415}
416
417fn eval_is_null<C: PredContext>(ir: &SemIr, inner: ExprId, ctx: &mut C) -> bool {
418 if let Some(source) = text_source(ir, inner) {
419 return resolve_owned_text(ir, source, ctx).is_none();
420 }
421 eval_value(ir, inner, ctx) == Value::Null
422}
423
424fn eval_cmp<C: PredContext>(ir: &SemIr, op: CmpOp, lhs: ExprId, rhs: ExprId, ctx: &mut C) -> Value {
425 let left_source = text_source(ir, lhs);
426 let right_source = text_source(ir, rhs);
427 if left_source.is_some() || right_source.is_some() {
428 return eval_text_cmp(ir, op, (lhs, left_source), (rhs, right_source), ctx);
429 }
430 let left = eval_value(ir, lhs, ctx);
431 let right = eval_value(ir, rhs, ctx);
432 Value::Bool(match (left, right) {
433 (Value::Null, Value::Null) => cmp_on_equality(op, true),
434 (Value::Null, _) | (_, Value::Null) => cmp_on_equality(op, false),
435 (Value::Bool(left), Value::Bool(right)) => cmp_on_equality(op, left == right),
436 (Value::Int(left), Value::Int(right)) => cmp_ints(op, left, right),
437 (Value::Bool(_), Value::Int(_)) | (Value::Int(_), Value::Bool(_)) => {
438 cmp_on_equality(op, false)
439 }
440 })
441}
442
443const fn cmp_on_equality(op: CmpOp, equal: bool) -> bool {
446 match op {
447 CmpOp::Eq => equal,
448 CmpOp::Ne => !equal,
449 CmpOp::Lt | CmpOp::Le | CmpOp::Gt | CmpOp::Ge => false,
450 }
451}
452
453const fn cmp_ints(op: CmpOp, left: i64, right: i64) -> bool {
454 match op {
455 CmpOp::Eq => left == right,
456 CmpOp::Ne => left != right,
457 CmpOp::Lt => left < right,
458 CmpOp::Le => left <= right,
459 CmpOp::Gt => left > right,
460 CmpOp::Ge => left >= right,
461 }
462}
463
464#[derive(Clone, Copy, Debug)]
471enum TextSource {
472 Literal(StrId),
473 Lookahead(isize),
474 CtxRule(usize),
475 SoFar,
476}
477
478fn text_source(ir: &SemIr, expr: ExprId) -> Option<TextSource> {
479 match ir.node(expr) {
480 PExpr::Str(id) => Some(TextSource::Literal(*id)),
481 PExpr::TokenText(offset) => Some(TextSource::Lookahead(*offset)),
482 PExpr::CtxRuleText(rule_index) => Some(TextSource::CtxRule(*rule_index)),
483 PExpr::TokenTextSoFar => Some(TextSource::SoFar),
484 _ => None,
485 }
486}
487
488fn resolve_static_text<'ir, C: PredContext>(
490 ir: &'ir SemIr,
491 source: TextSource,
492 ctx: &C,
493) -> Option<Cow<'ir, str>> {
494 match source {
495 TextSource::Literal(id) => Some(Cow::Borrowed(ir.text(id))),
496 TextSource::Lookahead(_) => unreachable!("lookahead operands are resolved last"),
497 TextSource::CtxRule(rule_index) => ctx.ctx_rule_text(rule_index).map(Cow::Owned),
498 TextSource::SoFar => ctx.token_text_so_far().map(Cow::Owned),
499 }
500}
501
502fn resolve_owned_text<C: PredContext>(
504 ir: &SemIr,
505 source: TextSource,
506 ctx: &mut C,
507) -> Option<String> {
508 match source {
509 TextSource::Lookahead(offset) => ctx.token_text(offset).map(str::to_owned),
510 other => resolve_static_text(ir, other, ctx).map(Cow::into_owned),
511 }
512}
513
514fn eval_text_cmp<C: PredContext>(
515 ir: &SemIr,
516 op: CmpOp,
517 (lhs, left_source): (ExprId, Option<TextSource>),
518 (rhs, right_source): (ExprId, Option<TextSource>),
519 ctx: &mut C,
520) -> Value {
521 let (Some(left_source), Some(right_source)) = (left_source, right_source) else {
524 debug_assert!(false, "text operand compared with non-text operand");
525 let _ = (lhs, rhs);
526 return Value::Bool(cmp_on_equality(op, false));
527 };
528 Value::Bool(match (left_source, right_source) {
529 (TextSource::Lookahead(left), TextSource::Lookahead(right)) => {
530 let left = ctx.token_text(left).map(str::to_owned);
534 let right = ctx.token_text(right);
535 cmp_texts(op, left.as_deref(), right)
536 }
537 (TextSource::Lookahead(offset), other) => {
538 let right = resolve_static_text(ir, other, ctx);
539 let left = ctx.token_text(offset);
540 cmp_texts(op, left, right.as_deref())
541 }
542 (other, TextSource::Lookahead(offset)) => {
543 let left = resolve_static_text(ir, other, ctx);
544 let right = ctx.token_text(offset);
545 cmp_texts(op, left.as_deref(), right)
546 }
547 (left, right) => {
548 let left = resolve_static_text(ir, left, ctx);
549 let right = resolve_static_text(ir, right, ctx);
550 cmp_texts(op, left.as_deref(), right.as_deref())
551 }
552 })
553}
554
555fn cmp_texts(op: CmpOp, left: Option<&str>, right: Option<&str>) -> bool {
556 match (left, right) {
557 (None, None) => cmp_on_equality(op, true),
558 (None, Some(_)) | (Some(_), None) => cmp_on_equality(op, false),
559 (Some(left), Some(right)) => match op {
560 CmpOp::Eq => left == right,
561 CmpOp::Ne => left != right,
562 CmpOp::Lt => left < right,
563 CmpOp::Le => left <= right,
564 CmpOp::Gt => left > right,
565 CmpOp::Ge => left >= right,
566 },
567 }
568}
569
570fn eval_arith<C: PredContext>(
571 ir: &SemIr,
572 op: ArithOp,
573 lhs: ExprId,
574 rhs: ExprId,
575 ctx: &mut C,
576) -> Value {
577 let (Value::Int(left), Value::Int(right)) =
578 (eval_value(ir, lhs, ctx), eval_value(ir, rhs, ctx))
579 else {
580 return Value::Null;
581 };
582 let result = match op {
583 ArithOp::Add => left.checked_add(right),
584 ArithOp::Sub => left.checked_sub(right),
585 ArithOp::Mul => left.checked_mul(right),
586 ArithOp::Div => left.checked_div(right),
587 ArithOp::Mod => left.checked_rem(right),
588 };
589 result.map_or(Value::Null, Value::Int)
590}
591
592#[cfg(test)]
593mod tests {
594 use super::{
595 AStmt, ActContext, ArithOp, CmpOp, ExprId, HookId, PExpr, PredContext, SemIr, eval_pred,
596 exec_stmt,
597 };
598 use std::collections::BTreeMap;
599
600 #[derive(Debug, Default)]
602 struct MockCtx {
603 tokens: Vec<(i64, Option<&'static str>)>,
604 adjacent: bool,
605 ctx_rule_texts: BTreeMap<usize, String>,
606 members: BTreeMap<usize, i64>,
607 local_arg: Option<i64>,
608 column: Option<i64>,
609 token_start_column: Option<i64>,
610 text_so_far: Option<String>,
611 hook_results: Vec<bool>,
612 hook_calls: Vec<HookId>,
613 la_calls: usize,
614 returns: BTreeMap<String, i64>,
615 }
616
617 impl PredContext for MockCtx {
618 fn la(&mut self, offset: isize) -> i64 {
619 self.la_calls += 1;
620 self.lookup(offset).map_or(-1, |(token_type, _)| token_type)
621 }
622
623 fn token_text(&mut self, offset: isize) -> Option<&str> {
624 self.lookup(offset).and_then(|(_, text)| text)
625 }
626
627 fn token_index_adjacent(&mut self) -> bool {
628 self.adjacent
629 }
630
631 fn ctx_rule_text(&self, rule_index: usize) -> Option<String> {
632 self.ctx_rule_texts.get(&rule_index).cloned()
633 }
634
635 fn member(&self, member: usize) -> Option<i64> {
636 self.members.get(&member).copied()
637 }
638
639 fn local_arg(&self) -> Option<i64> {
640 self.local_arg
641 }
642
643 fn column(&self) -> Option<i64> {
644 self.column
645 }
646
647 fn token_start_column(&self) -> Option<i64> {
648 self.token_start_column
649 }
650
651 fn token_text_so_far(&self) -> Option<String> {
652 self.text_so_far.clone()
653 }
654
655 fn hook(&mut self, hook: HookId) -> bool {
656 self.hook_calls.push(hook);
657 self.hook_results[hook.index()]
658 }
659 }
660
661 impl ActContext for MockCtx {
662 fn set_member(&mut self, member: usize, value: i64) {
663 self.members.insert(member, value);
664 }
665
666 fn set_return(&mut self, name: &str, value: i64) {
667 self.returns.insert(name.to_owned(), value);
668 }
669
670 fn action_hook(&mut self, hook: HookId) {
671 self.hook_calls.push(hook);
672 }
673 }
674
675 impl MockCtx {
676 fn lookup(&self, offset: isize) -> Option<(i64, Option<&'static str>)> {
677 let index = if offset > 0 {
679 usize::try_from(offset - 1).ok()?
680 } else {
681 self.tokens.len().checked_sub(offset.unsigned_abs())?
682 };
683 self.tokens.get(index).copied()
684 }
685 }
686
687 fn build(build: impl FnOnce(&mut SemIr) -> ExprId) -> (SemIr, ExprId) {
688 let mut ir = SemIr::new();
689 let root = build(&mut ir);
690 (ir, root)
691 }
692
693 #[test]
694 fn literals_and_truthiness() {
695 for (value, expected) in [(true, true), (false, false)] {
696 let (ir, root) = build(|ir| ir.expr(PExpr::Bool(value)));
697 assert_eq!(eval_pred(&ir, root, &mut MockCtx::default()), expected);
698 }
699 let (ir, root) = build(|ir| ir.expr(PExpr::Int(2)));
700 assert!(eval_pred(&ir, root, &mut MockCtx::default()));
701 let (ir, root) = build(|ir| ir.expr(PExpr::Int(0)));
702 assert!(!eval_pred(&ir, root, &mut MockCtx::default()));
703 }
704
705 #[test]
706 fn lookahead_text_equals_literal_and_absent_token_fails() {
707 let (ir, root) = build(|ir| {
708 let text = ir.expr(PExpr::TokenText(1));
709 let literal = ir.intern("of");
710 let literal = ir.expr(PExpr::Str(literal));
711 ir.expr(PExpr::Cmp(CmpOp::Eq, text, literal))
712 });
713
714 let mut ctx = MockCtx {
715 tokens: vec![(7, Some("of"))],
716 ..MockCtx::default()
717 };
718 assert!(eval_pred(&ir, root, &mut ctx));
719
720 ctx.tokens = vec![(7, Some("in"))];
721 assert!(!eval_pred(&ir, root, &mut ctx));
722
723 ctx.tokens = Vec::new();
725 assert!(!eval_pred(&ir, root, &mut ctx));
726 }
727
728 #[test]
729 fn ctx_rule_text_not_equals_passes_when_child_absent() {
730 let (ir, root) = build(|ir| {
731 let child = ir.expr(PExpr::CtxRuleText(4));
732 let literal = ir.intern("static");
733 let literal = ir.expr(PExpr::Str(literal));
734 ir.expr(PExpr::Cmp(CmpOp::Ne, child, literal))
735 });
736
737 assert!(eval_pred(&ir, root, &mut MockCtx::default()));
739
740 let mut ctx = MockCtx {
741 ctx_rule_texts: std::iter::once((4, "static".to_owned())).collect(),
742 ..MockCtx::default()
743 };
744 assert!(!eval_pred(&ir, root, &mut ctx));
745
746 ctx.ctx_rule_texts = std::iter::once((4, "dynamic".to_owned())).collect();
747 assert!(eval_pred(&ir, root, &mut ctx));
748 }
749
750 #[test]
751 fn absent_local_arg_composes_non_restrictive_guard() {
752 let (ir, root) = build(|ir| {
755 let arg = ir.expr(PExpr::LocalArg);
756 let absent = ir.expr(PExpr::IsNull(arg));
757 let value = ir.expr(PExpr::Int(2));
758 let equals = ir.expr(PExpr::Cmp(CmpOp::Eq, arg, value));
759 ir.expr(PExpr::Or([absent, equals].into()))
760 });
761
762 assert!(eval_pred(&ir, root, &mut MockCtx::default()));
763 let mut ctx = MockCtx {
764 local_arg: Some(2),
765 ..MockCtx::default()
766 };
767 assert!(eval_pred(&ir, root, &mut ctx));
768 ctx.local_arg = Some(3);
769 assert!(!eval_pred(&ir, root, &mut ctx));
770 }
771
772 #[test]
773 fn member_modulo_comparison() {
774 let (ir, root) = build(|ir| {
775 let member = ir.expr(PExpr::Member(0));
776 let modulus = ir.expr(PExpr::Int(2));
777 let remainder = ir.expr(PExpr::Arith(ArithOp::Mod, member, modulus));
778 let expected = ir.expr(PExpr::Int(0));
779 ir.expr(PExpr::Cmp(CmpOp::Eq, remainder, expected))
780 });
781
782 let mut ctx = MockCtx {
783 members: std::iter::once((0, 4)).collect(),
784 ..MockCtx::default()
785 };
786 assert!(eval_pred(&ir, root, &mut ctx));
787 ctx.members.insert(0, 5);
788 assert!(!eval_pred(&ir, root, &mut ctx));
789 ctx.members.clear();
791 assert!(!eval_pred(&ir, root, &mut ctx));
792 }
793
794 #[test]
795 fn arithmetic_null_propagation_and_division_by_zero() {
796 let (ir, root) = build(|ir| {
797 let member = ir.expr(PExpr::Member(9));
798 let zero = ir.expr(PExpr::Int(0));
799 let modulo = ir.expr(PExpr::Arith(ArithOp::Mod, member, zero));
800 ir.expr(PExpr::IsNull(modulo))
801 });
802 let mut ctx = MockCtx {
804 members: std::iter::once((9, 3)).collect(),
805 ..MockCtx::default()
806 };
807 assert!(eval_pred(&ir, root, &mut ctx));
808 }
809
810 #[test]
811 fn and_or_short_circuit_left_to_right() {
812 let (ir, root) = build(|ir| {
813 let gate = ir.expr(PExpr::Bool(false));
814 let la = ir.expr(PExpr::La(1));
815 let one = ir.expr(PExpr::Int(1));
816 let la_check = ir.expr(PExpr::Cmp(CmpOp::Eq, la, one));
817 ir.expr(PExpr::And([gate, la_check].into()))
818 });
819 let mut ctx = MockCtx::default();
820 assert!(!eval_pred(&ir, root, &mut ctx));
821 assert_eq!(ctx.la_calls, 0, "false gate must short-circuit la()");
822
823 let (ir, root) = build(|ir| {
824 let gate = ir.expr(PExpr::Bool(true));
825 let la = ir.expr(PExpr::La(1));
826 let one = ir.expr(PExpr::Int(1));
827 let la_check = ir.expr(PExpr::Cmp(CmpOp::Eq, la, one));
828 ir.expr(PExpr::Or([gate, la_check].into()))
829 });
830 let mut ctx = MockCtx::default();
831 assert!(eval_pred(&ir, root, &mut ctx));
832 assert_eq!(ctx.la_calls, 0, "true gate must short-circuit la()");
833 }
834
835 #[test]
836 fn token_index_adjacency_and_lookahead_type() {
837 let (ir, root) = build(|ir| ir.expr(PExpr::TokenIndexAdjacent));
838 let mut ctx = MockCtx {
839 adjacent: true,
840 ..MockCtx::default()
841 };
842 assert!(eval_pred(&ir, root, &mut ctx));
843 ctx.adjacent = false;
844 assert!(!eval_pred(&ir, root, &mut ctx));
845
846 let (ir, root) = build(|ir| {
847 let la = ir.expr(PExpr::La(-1));
848 let expected = ir.expr(PExpr::Int(12));
849 ir.expr(PExpr::Cmp(CmpOp::Ne, la, expected))
850 });
851 let mut ctx = MockCtx {
852 tokens: vec![(12, None)],
853 ..MockCtx::default()
854 };
855 assert!(!eval_pred(&ir, root, &mut ctx));
856 ctx.tokens = vec![(13, None)];
857 assert!(eval_pred(&ir, root, &mut ctx));
858 }
859
860 #[test]
861 fn lexer_column_predicates() {
862 let (ir, root) = build(|ir| {
863 let column = ir.expr(PExpr::Column);
864 let limit = ir.expr(PExpr::Int(4));
865 ir.expr(PExpr::Cmp(CmpOp::Ge, column, limit))
866 });
867 let mut ctx = MockCtx {
868 column: Some(5),
869 ..MockCtx::default()
870 };
871 assert!(eval_pred(&ir, root, &mut ctx));
872 ctx.column = Some(3);
873 assert!(!eval_pred(&ir, root, &mut ctx));
874 ctx.column = None;
876 assert!(!eval_pred(&ir, root, &mut ctx));
877
878 let (ir, root) = build(|ir| {
879 let start = ir.expr(PExpr::TokenStartColumn);
880 let zero = ir.expr(PExpr::Int(0));
881 ir.expr(PExpr::Cmp(CmpOp::Eq, start, zero))
882 });
883 let mut ctx = MockCtx {
884 token_start_column: Some(0),
885 ..MockCtx::default()
886 };
887 assert!(eval_pred(&ir, root, &mut ctx));
888 }
889
890 #[test]
891 fn lexer_text_so_far_comparison() {
892 let (ir, root) = build(|ir| {
893 let text = ir.expr(PExpr::TokenTextSoFar);
894 let literal = ir.intern("aa");
895 let literal = ir.expr(PExpr::Str(literal));
896 ir.expr(PExpr::Cmp(CmpOp::Eq, text, literal))
897 });
898 let mut ctx = MockCtx {
899 text_so_far: Some("aa".to_owned()),
900 ..MockCtx::default()
901 };
902 assert!(eval_pred(&ir, root, &mut ctx));
903 ctx.text_so_far = Some("ab".to_owned());
904 assert!(!eval_pred(&ir, root, &mut ctx));
905 }
906
907 #[test]
908 fn hooks_defer_to_context() {
909 let (ir, root) = build(|ir| ir.expr(PExpr::Hook(HookId(0))));
910 let mut ctx = MockCtx {
911 hook_results: vec![true],
912 ..MockCtx::default()
913 };
914 assert!(eval_pred(&ir, root, &mut ctx));
915 assert_eq!(ctx.hook_calls, vec![HookId(0)]);
916 }
917
918 #[test]
919 fn statements_mutate_members_and_returns() {
920 let mut ir = SemIr::new();
921 let five = ir.expr(PExpr::Int(5));
922 let set = ir.stmt(AStmt::SetMember(1, five));
923 let two = ir.expr(PExpr::Int(2));
924 let add = ir.stmt(AStmt::AddMember(1, two));
925 let member = ir.expr(PExpr::Member(1));
926 let name = ir.intern("y");
927 let ret = ir.stmt(AStmt::SetReturn(name, member));
928 let seq = ir.stmt(AStmt::Seq([set, add, ret].into()));
929
930 let mut ctx = MockCtx::default();
931 exec_stmt(&ir, seq, &mut ctx);
932
933 assert_eq!(ctx.members.get(&1), Some(&7));
934 assert_eq!(ctx.returns.get("y"), Some(&7));
935 }
936
937 #[test]
938 fn string_interning_deduplicates() {
939 let mut ir = SemIr::new();
940 let first = ir.intern("of");
941 let second = ir.intern("of");
942 let third = ir.intern("in");
943 assert_eq!(first, second);
944 assert_ne!(first, third);
945 assert_eq!(ir.text(third), "in");
946 }
947}