use super::types::{
Block, Choice, ChoiceSet, CondKind, Conditional, Content, ContentPart, DivertTarget, Expr,
HirFile, Knot, Sequence, Stitch, Stmt, StringPart,
};
#[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_expr(&mut self, _expr: &Expr) {}
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 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) => walk_expr(e, v),
Stmt::EndOfLine => {}
}
v.exit_stmt(stmt);
}
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 {
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::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) {
for branch in &seq.branches {
walk_block_ctx(branch, 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(lhs, _, rhs) => {
walk_expr(lhs, v);
walk_expr(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(_) => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FileId;
use brink_syntax::parse;
#[derive(Default)]
struct Counts {
knots: usize,
stitches: usize,
enter_block: usize,
exit_block: usize,
stmts: usize,
content: usize,
exprs: 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_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 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");
}
}