use super::types::{
Block, BlockStmt, Choice, ChoiceSet, CondKind, Conditional, ConstDecl, Content, ContentPart,
DivertTarget, ElseBranch, Expr, ForStmt, HirFile, IfStmt, Knot, LambdaBody, LambdaExpr,
LogicBlock, Sequence, Stitch, Stmt, StringPart, VarDecl, WhileStmt,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContentContext {
Body,
ChoiceStart,
ChoiceBracket,
ChoiceInner,
}
pub trait HirVisitor {
fn enter_knot(&mut self, _knot: &Knot) {}
fn exit_knot(&mut self, _knot: &Knot) {}
fn enter_stitch(&mut self, _stitch: &Stitch) {}
fn exit_stitch(&mut self, _stitch: &Stitch) {}
fn enter_choice(&mut self, _choice: &Choice) {}
fn exit_choice(&mut self, _choice: &Choice) {}
fn enter_block(&mut self, _block: &Block) {}
fn exit_block(&mut self, _block: &Block) {}
fn enter_stmt(&mut self, _stmt: &Stmt) {}
fn exit_stmt(&mut self, _stmt: &Stmt) {}
fn enter_content(&mut self, _content: &Content, _ctx: ContentContext) {}
fn enter_sequence(&mut self, _seq: &Sequence) {}
fn enter_expr(&mut self, _expr: &Expr) {}
fn enter_lambda(&mut self, _lambda: &LambdaExpr) {}
fn exit_lambda(&mut self, _lambda: &LambdaExpr) {}
fn enter_var_decl(&mut self, _var: &VarDecl) {}
fn enter_const_decl(&mut self, _const: &ConstDecl) {}
fn visit_exprs(&self) -> bool {
false
}
}
pub fn visit(hir: &HirFile, v: &mut impl HirVisitor) {
walk_block(&hir.root_content, v);
for knot in &hir.knots {
v.enter_knot(knot);
walk_block(&knot.body, v);
for stitch in &knot.stitches {
v.enter_stitch(stitch);
walk_block(&stitch.body, v);
v.exit_stitch(stitch);
}
v.exit_knot(knot);
}
}
pub fn visit_with_decl_initializers(hir: &HirFile, v: &mut impl HirVisitor) {
visit(hir, v);
for var in &hir.variables {
v.enter_var_decl(var);
walk_expr(&var.value, v);
}
for konst in &hir.constants {
v.enter_const_decl(konst);
walk_expr(&konst.value, v);
}
}
pub fn walk_block(block: &Block, v: &mut impl HirVisitor) {
walk_block_ctx(block, ContentContext::Body, v);
}
fn walk_block_ctx(block: &Block, ctx: ContentContext, v: &mut impl HirVisitor) {
v.enter_block(block);
for stmt in &block.stmts {
walk_stmt(stmt, ctx, v);
}
v.exit_block(block);
}
fn walk_stmt(stmt: &Stmt, ctx: ContentContext, v: &mut impl HirVisitor) {
v.enter_stmt(stmt);
match stmt {
Stmt::Content(c) => walk_content(c, ctx, v),
Stmt::Divert(d) => walk_target(&d.target, v),
Stmt::TunnelCall(t) => {
for target in &t.targets {
walk_target(target, v);
}
}
Stmt::ThreadStart(t) => walk_target(&t.target, v),
Stmt::TempDecl(t) => {
if let Some(e) = &t.value {
walk_expr(e, v);
}
}
Stmt::Assignment(a) => {
walk_expr(&a.target, v);
walk_expr(&a.value, v);
}
Stmt::Return(r) => {
if let Some(e) = &r.value {
walk_expr(e, v);
}
for e in &r.onwards_args {
walk_expr(e, v);
}
}
Stmt::ChoiceSet(cs) => walk_choice_set(cs, v),
Stmt::LabeledBlock(b) => walk_block_ctx(b, ctx, v),
Stmt::Conditional(c) => walk_conditional(c, ctx, v),
Stmt::Sequence(s) => walk_sequence(s, ctx, v),
Stmt::ExprStmt(e) | Stmt::AttachElement(e) => walk_expr(e, v),
Stmt::EndOfLine | Stmt::EndElementRun => {}
Stmt::LogicBlock(lb) => walk_logic_block(lb, v),
Stmt::Await(a) => {
if let Some(e) = &a.condition {
walk_expr(e, v);
}
}
}
v.exit_stmt(stmt);
}
fn walk_logic_block(lb: &LogicBlock, v: &mut impl HirVisitor) {
for bs in &lb.stmts {
walk_block_stmt(bs, v);
}
}
fn walk_block_stmt(bs: &BlockStmt, v: &mut impl HirVisitor) {
match bs {
BlockStmt::TempDecl(t) => {
if let Some(e) = &t.value {
walk_expr(e, v);
}
}
BlockStmt::Assignment(a) => {
walk_expr(&a.target, v);
walk_expr(&a.value, v);
}
BlockStmt::Return(r) => {
if let Some(e) = &r.value {
walk_expr(e, v);
}
for e in &r.onwards_args {
walk_expr(e, v);
}
}
BlockStmt::If(i) => walk_if_stmt(i, v),
BlockStmt::While(w) => walk_while_stmt(w, v),
BlockStmt::For(f) => walk_for_stmt(f, v),
BlockStmt::Break(_) | BlockStmt::Continue(_) => {}
BlockStmt::ExprStmt(e) => walk_expr(e, v),
BlockStmt::Await(a) => {
if let Some(e) = &a.condition {
walk_expr(e, v);
}
}
}
}
fn walk_if_stmt(i: &IfStmt, v: &mut impl HirVisitor) {
walk_expr(&i.condition, v);
for s in &i.body {
walk_block_stmt(s, v);
}
match &i.else_branch {
Some(ElseBranch::ElseIf(inner)) => walk_if_stmt(inner, v),
Some(ElseBranch::Else(stmts)) => {
for s in stmts {
walk_block_stmt(s, v);
}
}
None => {}
}
}
fn walk_while_stmt(w: &WhileStmt, v: &mut impl HirVisitor) {
walk_expr(&w.condition, v);
for s in &w.body {
walk_block_stmt(s, v);
}
}
fn walk_for_stmt(f: &ForStmt, v: &mut impl HirVisitor) {
walk_expr(&f.iterable, v);
for s in &f.body {
walk_block_stmt(s, v);
}
}
fn walk_target(target: &DivertTarget, v: &mut impl HirVisitor) {
for e in &target.args {
walk_expr(e, v);
}
}
fn walk_content(content: &Content, ctx: ContentContext, v: &mut impl HirVisitor) {
v.enter_content(content, ctx);
for part in &content.parts {
walk_content_part(part, ctx, v);
}
}
fn walk_content_part(part: &ContentPart, ctx: ContentContext, v: &mut impl HirVisitor) {
match part {
ContentPart::Interpolation(e) => walk_expr(e, v),
ContentPart::InlineConditional(c) => walk_conditional(c, ctx, v),
ContentPart::InlineSequence(s) => walk_sequence(s, ctx, v),
ContentPart::Span(span) => {
for child in &span.children {
walk_content_part(child, ctx, v);
}
}
ContentPart::Text(_) | ContentPart::Glue | ContentPart::Spring => {}
}
}
fn walk_choice_set(cs: &ChoiceSet, v: &mut impl HirVisitor) {
for choice in &cs.choices {
v.enter_choice(choice);
if let Some(e) = &choice.condition {
walk_expr(e, v);
}
if let Some(c) = &choice.start_content {
walk_content(c, ContentContext::ChoiceStart, v);
}
if let Some(c) = &choice.bracket_content {
walk_content(c, ContentContext::ChoiceBracket, v);
}
if let Some(c) = &choice.inner_content {
walk_content(c, ContentContext::ChoiceInner, v);
}
walk_block_ctx(&choice.body, ContentContext::Body, v);
v.exit_choice(choice);
}
walk_block_ctx(&cs.continuation, ContentContext::Body, v);
}
fn walk_conditional(cond: &Conditional, ctx: ContentContext, v: &mut impl HirVisitor) {
if let CondKind::Switch(e) = &cond.kind {
walk_expr(e, v);
}
for branch in &cond.branches {
if let Some(e) = &branch.condition {
walk_expr(e, v);
}
walk_block_ctx(&branch.body, ctx, v);
}
}
fn walk_sequence(seq: &Sequence, ctx: ContentContext, v: &mut impl HirVisitor) {
v.enter_sequence(seq);
for branch in &seq.branches {
walk_block_ctx(&branch.body, ctx, v);
}
}
fn walk_expr(expr: &Expr, v: &mut impl HirVisitor) {
if !v.visit_exprs() {
return;
}
v.enter_expr(expr);
match expr {
Expr::Call(_path, args) => {
for arg in args {
walk_expr(arg, v);
}
}
Expr::Prefix(_, inner) | Expr::Postfix(inner, _) => walk_expr(inner, v),
Expr::Infix(ie) => {
walk_expr(&ie.lhs, v);
walk_expr(&ie.rhs, v);
}
Expr::String(s) => {
for part in &s.parts {
if let StringPart::Interpolation(e) = part {
walk_expr(e, v);
}
}
}
Expr::Int(_)
| Expr::Float(_)
| Expr::Bool(_)
| Expr::Null
| Expr::Path(_)
| Expr::DivertTarget(_)
| Expr::ListLiteral(_) => {}
Expr::ArrayLiteral(a) => {
for e in &a.elements {
walk_expr(e, v);
}
}
Expr::MapLiteral(m) => {
for (k, val) in &m.entries {
walk_expr(k, v);
walk_expr(val, v);
}
}
Expr::Index(idx) => {
walk_expr(&idx.base, v);
walk_expr(&idx.index, v);
}
Expr::StructLiteral(sl) => {
for (_name, val) in &sl.fields {
walk_expr(val, v);
}
}
Expr::FieldAccess(fa) => {
walk_expr(&fa.base, v);
}
Expr::FnLiteral(fl) => {
for arg in &fl.args {
walk_expr(arg, v);
}
}
Expr::RefArg(ra) => walk_expr(&ra.operand, v),
Expr::Lambda(l) => {
v.enter_lambda(l);
match &l.body {
LambdaBody::Expr(e) => walk_expr(e, v),
LambdaBody::Block { stmts, tail } => {
for bs in stmts {
walk_block_stmt(bs, v);
}
if let Some(t) = tail {
walk_expr(t, v);
}
}
}
v.exit_lambda(l);
}
Expr::Range(r) => {
walk_expr(&r.start, v);
walk_expr(&r.end, v);
}
Expr::Fragment(stmts) => {
for s in stmts {
walk_stmt(s, ContentContext::Body, v);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FileId;
use brink_syntax::parse;
use rowan::TextRange;
#[derive(Default)]
struct Counts {
knots: usize,
stitches: usize,
enter_block: usize,
exit_block: usize,
stmts: usize,
content: usize,
exprs: usize,
sequences: usize,
enter_lambda: usize,
exit_lambda: usize,
visit_exprs: bool,
}
impl HirVisitor for Counts {
fn enter_knot(&mut self, _: &Knot) {
self.knots += 1;
}
fn enter_stitch(&mut self, _: &Stitch) {
self.stitches += 1;
}
fn enter_lambda(&mut self, _: &LambdaExpr) {
self.enter_lambda += 1;
}
fn exit_lambda(&mut self, _: &LambdaExpr) {
self.exit_lambda += 1;
}
fn enter_block(&mut self, _: &Block) {
self.enter_block += 1;
}
fn exit_block(&mut self, _: &Block) {
self.exit_block += 1;
}
fn enter_stmt(&mut self, _: &Stmt) {
self.stmts += 1;
}
fn enter_content(&mut self, _: &Content, _: ContentContext) {
self.content += 1;
}
fn enter_expr(&mut self, _: &Expr) {
self.exprs += 1;
}
fn enter_sequence(&mut self, _: &Sequence) {
self.sequences += 1;
}
fn visit_exprs(&self) -> bool {
self.visit_exprs
}
}
fn lower_src(src: &str) -> HirFile {
let parsed = parse(src);
let tree = parsed.tree();
let (hir, _, _) = crate::hir::lower::lower(FileId(0), &tree);
hir
}
#[test]
fn visits_structure_and_balances_enter_exit() {
let hir = lower_src("Hello {name}\n=== greet ===\n= again\n+ [pick] -> greet\n");
let mut c = Counts {
visit_exprs: true,
..Default::default()
};
visit(&hir, &mut c);
assert_eq!(c.knots, 1, "one knot");
assert_eq!(c.stitches, 1, "one stitch");
assert_eq!(c.enter_block, c.exit_block, "enter/exit block balanced");
assert!(c.enter_block >= 3, "several blocks: {}", c.enter_block);
assert!(c.content >= 1, "at least the greeting content");
assert!(c.exprs >= 1, "the {{name}} interpolation is an expr");
}
#[test]
fn expr_descent_is_gated_off_by_default() {
let hir = lower_src("Hello {name}\n");
let mut c = Counts::default(); visit(&hir, &mut c);
assert_eq!(c.exprs, 0, "no expression hooks when visit_exprs is false");
assert!(c.content >= 1, "content is still visited");
}
#[test]
fn decl_initializers_are_reached_only_by_the_dedicated_entry_point() {
let hir = lower_src("VAR c = Colors.Red\nCONST k = Other.Thing\n");
let mut plain = Counts {
visit_exprs: true,
..Default::default()
};
visit(&hir, &mut plain);
assert_eq!(
plain.exprs, 0,
"`visit` walks the block tree only — no declaration initializers"
);
let mut with_decls = Counts {
visit_exprs: true,
..Default::default()
};
visit_with_decl_initializers(&hir, &mut with_decls);
assert_eq!(
with_decls.exprs, 2,
"both the VAR and the CONST initializer expressions are visited"
);
}
#[test]
fn decl_initializer_walk_still_covers_everything_visit_covers() {
let src = "Hello {name}\n=== greet ===\n= again\n+ [pick] -> greet\n";
let hir = lower_src(src);
let mut plain = Counts {
visit_exprs: true,
..Default::default()
};
visit(&hir, &mut plain);
let mut with_decls = Counts {
visit_exprs: true,
..Default::default()
};
visit_with_decl_initializers(&hir, &mut with_decls);
assert_eq!(plain.knots, with_decls.knots);
assert_eq!(plain.stitches, with_decls.stitches);
assert_eq!(plain.enter_block, with_decls.enter_block);
assert_eq!(plain.stmts, with_decls.stmts);
assert_eq!(plain.content, with_decls.content);
assert_eq!(plain.exprs, with_decls.exprs);
}
#[test]
fn enter_sequence_fires_for_inline_and_block_forms() {
let inline_hir = lower_src("{&a|b}\n");
let mut inline_counts = Counts::default();
visit(&inline_hir, &mut inline_counts);
assert_eq!(
inline_counts.sequences, 1,
"inline pipe-separated sequence: {inline_hir:?}"
);
let block_hir = lower_src("{&\n- a\n- b\n}\n");
let mut block_counts = Counts::default();
visit(&block_hir, &mut block_counts);
assert_eq!(
block_counts.sequences, 1,
"promoted multiline block sequence: {block_hir:?}"
);
}
struct Probe {
anchor: TextRange,
anchors_at_expr: Vec<TextRange>,
}
impl HirVisitor for Probe {
fn visit_exprs(&self) -> bool {
true
}
fn enter_knot(&mut self, knot: &Knot) {
self.anchor = knot.ptr.text_range();
}
fn enter_var_decl(&mut self, var: &VarDecl) {
self.anchor = var.ptr.text_range();
}
fn enter_const_decl(&mut self, konst: &ConstDecl) {
self.anchor = konst.ptr.text_range();
}
fn enter_expr(&mut self, _expr: &Expr) {
self.anchors_at_expr.push(self.anchor);
}
}
#[test]
fn decl_hooks_reset_a_stateful_visitors_anchor_before_each_initializer() {
let hir = lower_src("=== greet ===\nHello\n\nVAR c = 1\nCONST d = 2\n");
assert_eq!(hir.variables.len(), 1);
assert_eq!(hir.constants.len(), 1);
let mut p = Probe {
anchor: TextRange::new(0.into(), 0.into()),
anchors_at_expr: Vec::new(),
};
visit_with_decl_initializers(&hir, &mut p);
assert_eq!(
p.anchors_at_expr.len(),
2,
"the VAR value `1` and the CONST value `2`, no more: {:?}",
p.anchors_at_expr
);
let knot_range = hir.knots[0].ptr.text_range();
assert_ne!(
p.anchors_at_expr[0], knot_range,
"the VAR initializer must not see the knot's leftover anchor"
);
assert_eq!(p.anchors_at_expr[0], hir.variables[0].ptr.text_range());
assert_ne!(
p.anchors_at_expr[1], knot_range,
"the CONST initializer must not see the knot's leftover anchor"
);
assert_eq!(p.anchors_at_expr[1], hir.constants[0].ptr.text_range());
}
#[test]
fn enter_exit_lambda_fire_around_both_body_shapes_and_nest_correctly() {
let parsed = brink_syntax_native::parse(
"fn f() {\n let a = |x: int| x + 1;\n let b = |y: int| {\n let g = |z: int| z;\n g(y)\n };\n}\n",
);
assert!(parsed.errors().is_empty(), "{:?}", parsed.errors());
let (hir, _manifest, _diag) = crate::hir::lower_native::lower(FileId(0), &parsed.tree());
let mut c = Counts {
visit_exprs: true,
..Default::default()
};
visit(&hir, &mut c);
assert_eq!(c.enter_lambda, 3, "expr-body + block-body + nested");
assert_eq!(c.enter_lambda, c.exit_lambda, "enter/exit balanced");
}
}