use std::borrow::Cow;
use std::fmt::Debug;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ExprId(u32);
impl ExprId {
#[must_use]
pub const fn new(index: u32) -> Self {
Self(index)
}
#[must_use]
pub const fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct StmtId(u32);
impl StmtId {
#[must_use]
pub const fn new(index: u32) -> Self {
Self(index)
}
#[must_use]
pub const fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct StrId(u32);
impl StrId {
#[must_use]
pub const fn new(index: u32) -> Self {
Self(index)
}
#[must_use]
pub const fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct HookId(u32);
impl HookId {
#[must_use]
pub const fn new(index: u32) -> Self {
Self(index)
}
#[must_use]
pub const fn index(self) -> usize {
self.0 as usize
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CmpOp {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ArithOp {
Add,
Sub,
Mul,
Div,
Mod,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PExpr {
Bool(bool),
Int(i64),
Str(StrId),
La(isize),
TokenText(isize),
TokenIndexAdjacent,
CtxRuleText(usize),
Member(usize),
LocalArg,
Column,
TokenStartColumn,
TokenTextSoFar,
IsNull(ExprId),
Not(ExprId),
And(Box<[ExprId]>),
Or(Box<[ExprId]>),
Cmp(CmpOp, ExprId, ExprId),
Arith(ArithOp, ExprId, ExprId),
Hook(HookId),
EvalTrace(bool),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AStmt {
SetMember(usize, ExprId),
AddMember(usize, ExprId),
SetReturn(StrId, ExprId),
Seq(Box<[StmtId]>),
Hook(HookId),
}
#[allow(variant_size_differences)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Value {
Null,
Bool(bool),
Int(i64),
}
impl Value {
#[must_use]
pub const fn truthy(self) -> bool {
match self {
Self::Null => false,
Self::Bool(value) => value,
Self::Int(value) => value != 0,
}
}
}
pub trait PredContext {
fn la(&mut self, offset: isize) -> i64;
fn token_text(&mut self, offset: isize) -> Option<&str>;
fn token_index_adjacent(&mut self) -> bool;
fn ctx_rule_text(&self, rule_index: usize) -> Option<String>;
fn member(&self, member: usize) -> Option<i64>;
fn local_arg(&self) -> Option<i64>;
fn column(&self) -> Option<i64>;
fn token_start_column(&self) -> Option<i64>;
fn token_text_so_far(&self) -> Option<String>;
fn hook(&mut self, hook: HookId) -> bool;
fn trace_bool(&mut self, value: bool) -> bool {
value
}
}
pub trait ActContext: PredContext {
fn set_member(&mut self, member: usize, value: i64);
fn set_return(&mut self, name: &str, value: i64);
fn action_hook(&mut self, hook: HookId);
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct SemIr {
exprs: Vec<PExpr>,
stmts: Vec<AStmt>,
strings: Vec<Box<str>>,
}
impl SemIr {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn expr(&mut self, node: PExpr) -> ExprId {
let id = ExprId(u32::try_from(self.exprs.len()).expect("expression arena fits in u32"));
self.exprs.push(node);
id
}
pub fn stmt(&mut self, node: AStmt) -> StmtId {
let id = StmtId(u32::try_from(self.stmts.len()).expect("statement arena fits in u32"));
self.stmts.push(node);
id
}
pub fn intern(&mut self, value: &str) -> StrId {
if let Some(position) = self.strings.iter().position(|entry| &**entry == value) {
return StrId(u32::try_from(position).expect("string pool fits in u32"));
}
let id = StrId(u32::try_from(self.strings.len()).expect("string pool fits in u32"));
self.strings.push(value.into());
id
}
#[must_use]
pub fn text(&self, id: StrId) -> &str {
&self.strings[id.0 as usize]
}
fn node(&self, id: ExprId) -> &PExpr {
&self.exprs[id.0 as usize]
}
fn stmt_node(&self, id: StmtId) -> &AStmt {
&self.stmts[id.0 as usize]
}
}
pub fn eval_pred<C: PredContext>(ir: &SemIr, expr: ExprId, ctx: &mut C) -> bool {
eval_value(ir, expr, ctx).truthy()
}
pub fn exec_stmt<C: ActContext>(ir: &SemIr, stmt: StmtId, ctx: &mut C) {
match ir.stmt_node(stmt) {
AStmt::SetMember(member, value) => {
let value = int_or_zero(eval_value(ir, *value, ctx));
ctx.set_member(*member, value);
}
AStmt::AddMember(member, delta) => {
let delta = int_or_zero(eval_value(ir, *delta, ctx));
let current = ctx.member(*member).unwrap_or_default();
ctx.set_member(*member, current + delta);
}
AStmt::SetReturn(name, value) => {
let value = int_or_zero(eval_value(ir, *value, ctx));
let name = ir.text(*name).to_owned();
ctx.set_return(&name, value);
}
AStmt::Seq(stmts) => {
for stmt in stmts {
exec_stmt(ir, *stmt, ctx);
}
}
AStmt::Hook(hook) => ctx.action_hook(*hook),
}
}
const fn int_or_zero(value: Value) -> i64 {
match value {
Value::Int(value) => value,
Value::Null | Value::Bool(_) => 0,
}
}
fn eval_value<C: PredContext>(ir: &SemIr, expr: ExprId, ctx: &mut C) -> Value {
match ir.node(expr) {
PExpr::Str(_) | PExpr::TokenText(_) | PExpr::CtxRuleText(_) | PExpr::TokenTextSoFar => {
debug_assert!(false, "text-valued node evaluated outside a comparison");
Value::Null
}
PExpr::Bool(value) => Value::Bool(*value),
PExpr::Int(value) => Value::Int(*value),
PExpr::La(offset) => Value::Int(ctx.la(*offset)),
PExpr::TokenIndexAdjacent => Value::Bool(ctx.token_index_adjacent()),
PExpr::Member(member) => ctx.member(*member).map_or(Value::Null, Value::Int),
PExpr::LocalArg => ctx.local_arg().map_or(Value::Null, Value::Int),
PExpr::Column => ctx.column().map_or(Value::Null, Value::Int),
PExpr::TokenStartColumn => ctx.token_start_column().map_or(Value::Null, Value::Int),
PExpr::IsNull(inner) => Value::Bool(eval_is_null(ir, *inner, ctx)),
PExpr::Not(inner) => Value::Bool(!eval_value(ir, *inner, ctx).truthy()),
PExpr::And(children) => Value::Bool(
children
.iter()
.all(|child| eval_value(ir, *child, ctx).truthy()),
),
PExpr::Or(children) => Value::Bool(
children
.iter()
.any(|child| eval_value(ir, *child, ctx).truthy()),
),
PExpr::Cmp(op, lhs, rhs) => eval_cmp(ir, *op, *lhs, *rhs, ctx),
PExpr::Arith(op, lhs, rhs) => eval_arith(ir, *op, *lhs, *rhs, ctx),
PExpr::Hook(hook) => Value::Bool(ctx.hook(*hook)),
PExpr::EvalTrace(value) => Value::Bool(ctx.trace_bool(*value)),
}
}
fn eval_is_null<C: PredContext>(ir: &SemIr, inner: ExprId, ctx: &mut C) -> bool {
if let Some(source) = text_source(ir, inner) {
return resolve_owned_text(ir, source, ctx).is_none();
}
eval_value(ir, inner, ctx) == Value::Null
}
fn eval_cmp<C: PredContext>(ir: &SemIr, op: CmpOp, lhs: ExprId, rhs: ExprId, ctx: &mut C) -> Value {
let left_source = text_source(ir, lhs);
let right_source = text_source(ir, rhs);
if left_source.is_some() || right_source.is_some() {
return eval_text_cmp(ir, op, (lhs, left_source), (rhs, right_source), ctx);
}
let left = eval_value(ir, lhs, ctx);
let right = eval_value(ir, rhs, ctx);
Value::Bool(match (left, right) {
(Value::Null, Value::Null) => cmp_on_equality(op, true),
(Value::Null, _) | (_, Value::Null) => cmp_on_equality(op, false),
(Value::Bool(left), Value::Bool(right)) => cmp_on_equality(op, left == right),
(Value::Int(left), Value::Int(right)) => cmp_ints(op, left, right),
(Value::Bool(_), Value::Int(_)) | (Value::Int(_), Value::Bool(_)) => {
cmp_on_equality(op, false)
}
})
}
const fn cmp_on_equality(op: CmpOp, equal: bool) -> bool {
match op {
CmpOp::Eq => equal,
CmpOp::Ne => !equal,
CmpOp::Lt | CmpOp::Le | CmpOp::Gt | CmpOp::Ge => false,
}
}
const fn cmp_ints(op: CmpOp, left: i64, right: i64) -> bool {
match op {
CmpOp::Eq => left == right,
CmpOp::Ne => left != right,
CmpOp::Lt => left < right,
CmpOp::Le => left <= right,
CmpOp::Gt => left > right,
CmpOp::Ge => left >= right,
}
}
#[derive(Clone, Copy, Debug)]
enum TextSource {
Literal(StrId),
Lookahead(isize),
CtxRule(usize),
SoFar,
}
fn text_source(ir: &SemIr, expr: ExprId) -> Option<TextSource> {
match ir.node(expr) {
PExpr::Str(id) => Some(TextSource::Literal(*id)),
PExpr::TokenText(offset) => Some(TextSource::Lookahead(*offset)),
PExpr::CtxRuleText(rule_index) => Some(TextSource::CtxRule(*rule_index)),
PExpr::TokenTextSoFar => Some(TextSource::SoFar),
_ => None,
}
}
fn resolve_static_text<'ir, C: PredContext>(
ir: &'ir SemIr,
source: TextSource,
ctx: &C,
) -> Option<Cow<'ir, str>> {
match source {
TextSource::Literal(id) => Some(Cow::Borrowed(ir.text(id))),
TextSource::Lookahead(_) => unreachable!("lookahead operands are resolved last"),
TextSource::CtxRule(rule_index) => ctx.ctx_rule_text(rule_index).map(Cow::Owned),
TextSource::SoFar => ctx.token_text_so_far().map(Cow::Owned),
}
}
fn resolve_owned_text<C: PredContext>(
ir: &SemIr,
source: TextSource,
ctx: &mut C,
) -> Option<String> {
match source {
TextSource::Lookahead(offset) => ctx.token_text(offset).map(str::to_owned),
other => resolve_static_text(ir, other, ctx).map(Cow::into_owned),
}
}
fn eval_text_cmp<C: PredContext>(
ir: &SemIr,
op: CmpOp,
(lhs, left_source): (ExprId, Option<TextSource>),
(rhs, right_source): (ExprId, Option<TextSource>),
ctx: &mut C,
) -> Value {
let (Some(left_source), Some(right_source)) = (left_source, right_source) else {
debug_assert!(false, "text operand compared with non-text operand");
let _ = (lhs, rhs);
return Value::Bool(cmp_on_equality(op, false));
};
Value::Bool(match (left_source, right_source) {
(TextSource::Lookahead(left), TextSource::Lookahead(right)) => {
let left = ctx.token_text(left).map(str::to_owned);
let right = ctx.token_text(right);
cmp_texts(op, left.as_deref(), right)
}
(TextSource::Lookahead(offset), other) => {
let right = resolve_static_text(ir, other, ctx);
let left = ctx.token_text(offset);
cmp_texts(op, left, right.as_deref())
}
(other, TextSource::Lookahead(offset)) => {
let left = resolve_static_text(ir, other, ctx);
let right = ctx.token_text(offset);
cmp_texts(op, left.as_deref(), right)
}
(left, right) => {
let left = resolve_static_text(ir, left, ctx);
let right = resolve_static_text(ir, right, ctx);
cmp_texts(op, left.as_deref(), right.as_deref())
}
})
}
fn cmp_texts(op: CmpOp, left: Option<&str>, right: Option<&str>) -> bool {
match (left, right) {
(None, None) => cmp_on_equality(op, true),
(None, Some(_)) | (Some(_), None) => cmp_on_equality(op, false),
(Some(left), Some(right)) => match op {
CmpOp::Eq => left == right,
CmpOp::Ne => left != right,
CmpOp::Lt => left < right,
CmpOp::Le => left <= right,
CmpOp::Gt => left > right,
CmpOp::Ge => left >= right,
},
}
}
fn eval_arith<C: PredContext>(
ir: &SemIr,
op: ArithOp,
lhs: ExprId,
rhs: ExprId,
ctx: &mut C,
) -> Value {
let (Value::Int(left), Value::Int(right)) =
(eval_value(ir, lhs, ctx), eval_value(ir, rhs, ctx))
else {
return Value::Null;
};
let result = match op {
ArithOp::Add => left.checked_add(right),
ArithOp::Sub => left.checked_sub(right),
ArithOp::Mul => left.checked_mul(right),
ArithOp::Div => left.checked_div(right),
ArithOp::Mod => left.checked_rem(right),
};
result.map_or(Value::Null, Value::Int)
}
#[cfg(test)]
mod tests {
use super::{
AStmt, ActContext, ArithOp, CmpOp, ExprId, HookId, PExpr, PredContext, SemIr, eval_pred,
exec_stmt,
};
use std::collections::BTreeMap;
#[derive(Debug, Default)]
struct MockCtx {
tokens: Vec<(i64, Option<&'static str>)>,
adjacent: bool,
ctx_rule_texts: BTreeMap<usize, String>,
members: BTreeMap<usize, i64>,
local_arg: Option<i64>,
column: Option<i64>,
token_start_column: Option<i64>,
text_so_far: Option<String>,
hook_results: Vec<bool>,
hook_calls: Vec<HookId>,
la_calls: usize,
returns: BTreeMap<String, i64>,
}
impl PredContext for MockCtx {
fn la(&mut self, offset: isize) -> i64 {
self.la_calls += 1;
self.lookup(offset).map_or(-1, |(token_type, _)| token_type)
}
fn token_text(&mut self, offset: isize) -> Option<&str> {
self.lookup(offset).and_then(|(_, text)| text)
}
fn token_index_adjacent(&mut self) -> bool {
self.adjacent
}
fn ctx_rule_text(&self, rule_index: usize) -> Option<String> {
self.ctx_rule_texts.get(&rule_index).cloned()
}
fn member(&self, member: usize) -> Option<i64> {
self.members.get(&member).copied()
}
fn local_arg(&self) -> Option<i64> {
self.local_arg
}
fn column(&self) -> Option<i64> {
self.column
}
fn token_start_column(&self) -> Option<i64> {
self.token_start_column
}
fn token_text_so_far(&self) -> Option<String> {
self.text_so_far.clone()
}
fn hook(&mut self, hook: HookId) -> bool {
self.hook_calls.push(hook);
self.hook_results[hook.index()]
}
}
impl ActContext for MockCtx {
fn set_member(&mut self, member: usize, value: i64) {
self.members.insert(member, value);
}
fn set_return(&mut self, name: &str, value: i64) {
self.returns.insert(name.to_owned(), value);
}
fn action_hook(&mut self, hook: HookId) {
self.hook_calls.push(hook);
}
}
impl MockCtx {
fn lookup(&self, offset: isize) -> Option<(i64, Option<&'static str>)> {
let index = if offset > 0 {
usize::try_from(offset - 1).ok()?
} else {
self.tokens.len().checked_sub(offset.unsigned_abs())?
};
self.tokens.get(index).copied()
}
}
fn build(build: impl FnOnce(&mut SemIr) -> ExprId) -> (SemIr, ExprId) {
let mut ir = SemIr::new();
let root = build(&mut ir);
(ir, root)
}
#[test]
fn literals_and_truthiness() {
for (value, expected) in [(true, true), (false, false)] {
let (ir, root) = build(|ir| ir.expr(PExpr::Bool(value)));
assert_eq!(eval_pred(&ir, root, &mut MockCtx::default()), expected);
}
let (ir, root) = build(|ir| ir.expr(PExpr::Int(2)));
assert!(eval_pred(&ir, root, &mut MockCtx::default()));
let (ir, root) = build(|ir| ir.expr(PExpr::Int(0)));
assert!(!eval_pred(&ir, root, &mut MockCtx::default()));
}
#[test]
fn lookahead_text_equals_literal_and_absent_token_fails() {
let (ir, root) = build(|ir| {
let text = ir.expr(PExpr::TokenText(1));
let literal = ir.intern("of");
let literal = ir.expr(PExpr::Str(literal));
ir.expr(PExpr::Cmp(CmpOp::Eq, text, literal))
});
let mut ctx = MockCtx {
tokens: vec![(7, Some("of"))],
..MockCtx::default()
};
assert!(eval_pred(&ir, root, &mut ctx));
ctx.tokens = vec![(7, Some("in"))];
assert!(!eval_pred(&ir, root, &mut ctx));
ctx.tokens = Vec::new();
assert!(!eval_pred(&ir, root, &mut ctx));
}
#[test]
fn ctx_rule_text_not_equals_passes_when_child_absent() {
let (ir, root) = build(|ir| {
let child = ir.expr(PExpr::CtxRuleText(4));
let literal = ir.intern("static");
let literal = ir.expr(PExpr::Str(literal));
ir.expr(PExpr::Cmp(CmpOp::Ne, child, literal))
});
assert!(eval_pred(&ir, root, &mut MockCtx::default()));
let mut ctx = MockCtx {
ctx_rule_texts: std::iter::once((4, "static".to_owned())).collect(),
..MockCtx::default()
};
assert!(!eval_pred(&ir, root, &mut ctx));
ctx.ctx_rule_texts = std::iter::once((4, "dynamic".to_owned())).collect();
assert!(eval_pred(&ir, root, &mut ctx));
}
#[test]
fn absent_local_arg_composes_non_restrictive_guard() {
let (ir, root) = build(|ir| {
let arg = ir.expr(PExpr::LocalArg);
let absent = ir.expr(PExpr::IsNull(arg));
let value = ir.expr(PExpr::Int(2));
let equals = ir.expr(PExpr::Cmp(CmpOp::Eq, arg, value));
ir.expr(PExpr::Or([absent, equals].into()))
});
assert!(eval_pred(&ir, root, &mut MockCtx::default()));
let mut ctx = MockCtx {
local_arg: Some(2),
..MockCtx::default()
};
assert!(eval_pred(&ir, root, &mut ctx));
ctx.local_arg = Some(3);
assert!(!eval_pred(&ir, root, &mut ctx));
}
#[test]
fn member_modulo_comparison() {
let (ir, root) = build(|ir| {
let member = ir.expr(PExpr::Member(0));
let modulus = ir.expr(PExpr::Int(2));
let remainder = ir.expr(PExpr::Arith(ArithOp::Mod, member, modulus));
let expected = ir.expr(PExpr::Int(0));
ir.expr(PExpr::Cmp(CmpOp::Eq, remainder, expected))
});
let mut ctx = MockCtx {
members: std::iter::once((0, 4)).collect(),
..MockCtx::default()
};
assert!(eval_pred(&ir, root, &mut ctx));
ctx.members.insert(0, 5);
assert!(!eval_pred(&ir, root, &mut ctx));
ctx.members.clear();
assert!(!eval_pred(&ir, root, &mut ctx));
}
#[test]
fn arithmetic_null_propagation_and_division_by_zero() {
let (ir, root) = build(|ir| {
let member = ir.expr(PExpr::Member(9));
let zero = ir.expr(PExpr::Int(0));
let modulo = ir.expr(PExpr::Arith(ArithOp::Mod, member, zero));
ir.expr(PExpr::IsNull(modulo))
});
let mut ctx = MockCtx {
members: std::iter::once((9, 3)).collect(),
..MockCtx::default()
};
assert!(eval_pred(&ir, root, &mut ctx));
}
#[test]
fn and_or_short_circuit_left_to_right() {
let (ir, root) = build(|ir| {
let gate = ir.expr(PExpr::Bool(false));
let la = ir.expr(PExpr::La(1));
let one = ir.expr(PExpr::Int(1));
let la_check = ir.expr(PExpr::Cmp(CmpOp::Eq, la, one));
ir.expr(PExpr::And([gate, la_check].into()))
});
let mut ctx = MockCtx::default();
assert!(!eval_pred(&ir, root, &mut ctx));
assert_eq!(ctx.la_calls, 0, "false gate must short-circuit la()");
let (ir, root) = build(|ir| {
let gate = ir.expr(PExpr::Bool(true));
let la = ir.expr(PExpr::La(1));
let one = ir.expr(PExpr::Int(1));
let la_check = ir.expr(PExpr::Cmp(CmpOp::Eq, la, one));
ir.expr(PExpr::Or([gate, la_check].into()))
});
let mut ctx = MockCtx::default();
assert!(eval_pred(&ir, root, &mut ctx));
assert_eq!(ctx.la_calls, 0, "true gate must short-circuit la()");
}
#[test]
fn token_index_adjacency_and_lookahead_type() {
let (ir, root) = build(|ir| ir.expr(PExpr::TokenIndexAdjacent));
let mut ctx = MockCtx {
adjacent: true,
..MockCtx::default()
};
assert!(eval_pred(&ir, root, &mut ctx));
ctx.adjacent = false;
assert!(!eval_pred(&ir, root, &mut ctx));
let (ir, root) = build(|ir| {
let la = ir.expr(PExpr::La(-1));
let expected = ir.expr(PExpr::Int(12));
ir.expr(PExpr::Cmp(CmpOp::Ne, la, expected))
});
let mut ctx = MockCtx {
tokens: vec![(12, None)],
..MockCtx::default()
};
assert!(!eval_pred(&ir, root, &mut ctx));
ctx.tokens = vec![(13, None)];
assert!(eval_pred(&ir, root, &mut ctx));
}
#[test]
fn lexer_column_predicates() {
let (ir, root) = build(|ir| {
let column = ir.expr(PExpr::Column);
let limit = ir.expr(PExpr::Int(4));
ir.expr(PExpr::Cmp(CmpOp::Ge, column, limit))
});
let mut ctx = MockCtx {
column: Some(5),
..MockCtx::default()
};
assert!(eval_pred(&ir, root, &mut ctx));
ctx.column = Some(3);
assert!(!eval_pred(&ir, root, &mut ctx));
ctx.column = None;
assert!(!eval_pred(&ir, root, &mut ctx));
let (ir, root) = build(|ir| {
let start = ir.expr(PExpr::TokenStartColumn);
let zero = ir.expr(PExpr::Int(0));
ir.expr(PExpr::Cmp(CmpOp::Eq, start, zero))
});
let mut ctx = MockCtx {
token_start_column: Some(0),
..MockCtx::default()
};
assert!(eval_pred(&ir, root, &mut ctx));
}
#[test]
fn lexer_text_so_far_comparison() {
let (ir, root) = build(|ir| {
let text = ir.expr(PExpr::TokenTextSoFar);
let literal = ir.intern("aa");
let literal = ir.expr(PExpr::Str(literal));
ir.expr(PExpr::Cmp(CmpOp::Eq, text, literal))
});
let mut ctx = MockCtx {
text_so_far: Some("aa".to_owned()),
..MockCtx::default()
};
assert!(eval_pred(&ir, root, &mut ctx));
ctx.text_so_far = Some("ab".to_owned());
assert!(!eval_pred(&ir, root, &mut ctx));
}
#[test]
fn hooks_defer_to_context() {
let (ir, root) = build(|ir| ir.expr(PExpr::Hook(HookId(0))));
let mut ctx = MockCtx {
hook_results: vec![true],
..MockCtx::default()
};
assert!(eval_pred(&ir, root, &mut ctx));
assert_eq!(ctx.hook_calls, vec![HookId(0)]);
}
#[test]
fn statements_mutate_members_and_returns() {
let mut ir = SemIr::new();
let five = ir.expr(PExpr::Int(5));
let set = ir.stmt(AStmt::SetMember(1, five));
let two = ir.expr(PExpr::Int(2));
let add = ir.stmt(AStmt::AddMember(1, two));
let member = ir.expr(PExpr::Member(1));
let name = ir.intern("y");
let ret = ir.stmt(AStmt::SetReturn(name, member));
let seq = ir.stmt(AStmt::Seq([set, add, ret].into()));
let mut ctx = MockCtx::default();
exec_stmt(&ir, seq, &mut ctx);
assert_eq!(ctx.members.get(&1), Some(&7));
assert_eq!(ctx.returns.get("y"), Some(&7));
}
#[test]
fn string_interning_deduplicates() {
let mut ir = SemIr::new();
let first = ir.intern("of");
let second = ir.intern("of");
let third = ir.intern("in");
assert_eq!(first, second);
assert_ne!(first, third);
assert_eq!(ir.text(third), "in");
}
}