diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index 44442e73..f403097e 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -18,6 +18,7 @@ pub mod diagnostics; pub mod lexer; pub mod parser; pub mod pretty; +pub mod visitor; // The Melior-free frontend pipeline (parse → desugar → typecheck → elaborate → // specialize) gates on `analyze`, which pulls in only `quon_core` / `z3` — no diff --git a/frontend/src/visitor.rs b/frontend/src/visitor.rs new file mode 100644 index 00000000..5973bb51 --- /dev/null +++ b/frontend/src/visitor.rs @@ -0,0 +1,335 @@ +//! Canonical exhaustive AST traversal for the frontend. +//! +//! Every AST node kind — declarations, expressions, statements, patterns, +//! types, type parameters, and type-level natural expressions — has a +//! dedicated pre/post hook pair driven by the `walk_*` free functions in a +//! fixed, source-span-preserving order. Read-only tooling (the linter, the +//! language server) shares this one recursion instead of each re-implementing +//! it by hand: when a new AST variant lands, only this module needs a +//! traversal update (issue #399). +//! +//! ## Order +//! +//! Traversal is pre-order: the `visit_*_pre` hook fires before descending into +//! a node's children, the `visit_*_post` hook fires after. A pre-hook returns +//! [`Traversal`] to decide whether to descend. Children are visited in source +//! order (left-to-right as written). +//! +//! ## Spans +//! +//! Hooks receive `&Sp` (or `&TypeParam`), so the node's [`crate::lexer::Sp`] +//! span is always available as `.1` — no separate span map is required. + +use crate::ast::{Decl, Expr, NatExpr, Pat, Stmt, Type, TypeParam}; +use crate::lexer::Sp; + +/// Control flow returned by a `visit_*_pre` hook. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Traversal { + /// Descend into the node's children, then invoke the matching `visit_*_post` hook. + Recurse, + /// Skip the node's children. The matching `visit_*_post` hook is NOT called. + Skip, +} + +/// Exhaustive AST visitor with pre/post hooks for every node kind. +/// +/// All methods default to no-ops returning [`Traversal::Recurse`]; override +/// only the hooks you care about. Implementors mutate `&mut self` freely — the +/// `walk_*` drivers borrow the visitor exclusively for the duration of a +/// subtree, so pre/post state push/pop is always properly nested. +pub trait Visitor { + // ── Declarations ────────────────────────────────────────────────────── + fn visit_decl_pre(&mut self, _decl: &Sp) -> Traversal { + Traversal::Recurse + } + fn visit_decl_post(&mut self, _decl: &Sp) {} + + // ── Expressions ─────────────────────────────────────────────────────── + fn visit_expr_pre(&mut self, _expr: &Sp) -> Traversal { + Traversal::Recurse + } + fn visit_expr_post(&mut self, _expr: &Sp) {} + + // ── Statements ──────────────────────────────────────────────────────── + fn visit_stmt_pre(&mut self, _stmt: &Sp) -> Traversal { + Traversal::Recurse + } + fn visit_stmt_post(&mut self, _stmt: &Sp) {} + + // ── Patterns ───────────────────────────────────────────────────────── + fn visit_pat_pre(&mut self, _pat: &Sp) -> Traversal { + Traversal::Recurse + } + fn visit_pat_post(&mut self, _pat: &Sp) {} + + // ── Types ───────────────────────────────────────────────────────────── + fn visit_type_pre(&mut self, _ty: &Sp) -> Traversal { + Traversal::Recurse + } + fn visit_type_post(&mut self, _ty: &Sp) {} + + // ── Type-level natural expressions ──────────────────────────────────── + fn visit_nat_expr_pre(&mut self, _ne: &Sp) -> Traversal { + Traversal::Recurse + } + fn visit_nat_expr_post(&mut self, _ne: &Sp) {} + + // ── Type parameters ────────────────────────────────────────────────── + /// `TypeParam` is not itself spanned; its `name` (`Sp`) and optional + /// `kind` (`Sp`) carry the relevant spans. `Kind` is a leaf enum with + /// no recurseable children, so it has no dedicated hook. + fn visit_type_param_pre(&mut self, _tp: &TypeParam) -> Traversal { + Traversal::Recurse + } + fn visit_type_param_post(&mut self, _tp: &TypeParam) {} +} + +// ── Drivers ──────────────────────────────────────────────────────────────── + +/// Walk a whole program's top-level declarations. +pub fn walk_program(v: &mut V, decls: &[Sp]) { + for decl in decls { + walk_decl(v, decl); + } +} + +pub fn walk_decl(v: &mut V, decl: &Sp) { + if matches!(v.visit_decl_pre(decl), Traversal::Recurse) { + match &decl.0 { + Decl::Fn { + type_params, + params, + ret, + body, + .. + } => { + for tp in type_params { + walk_type_param(v, tp); + } + for (_, ty) in params { + walk_type(v, ty); + } + walk_type(v, ret); + walk_expr(v, body); + } + Decl::TypeAlias { params, ty, .. } => { + for tp in params { + walk_type_param(v, tp); + } + walk_type(v, ty); + } + } + } + v.visit_decl_post(decl); +} + +pub fn walk_type_param(v: &mut V, tp: &TypeParam) { + if matches!(v.visit_type_param_pre(tp), Traversal::Recurse) { + // `name` (`Sp`) and `kind` (`Sp`) are leaves — no further + // recursion. They are observable via the `visit_type_param_pre` hook. + let _ = &tp.name; + let _ = &tp.kind; + } + v.visit_type_param_post(tp); +} + +pub fn walk_expr(v: &mut V, expr: &Sp) { + if matches!(v.visit_expr_pre(expr), Traversal::Recurse) { + match &expr.0 { + Expr::Int(_) + | Expr::Float(_) + | Expr::Bool(_) + | Expr::Unit + | Expr::Var(_) => {} + + Expr::Lam { params, body } => { + for (pat, ty) in params { + walk_pat(v, pat); + if let Some(ty) = ty { + walk_type(v, ty); + } + } + walk_expr(v, body); + } + Expr::App(a, b) => { + walk_expr(v, a); + walk_expr(v, b); + } + Expr::TypeApp { callee, args } => { + walk_expr(v, callee); + for arg in args { + walk_nat_expr(v, arg); + } + } + Expr::BinOp { lhs, rhs, .. } => { + walk_expr(v, lhs); + walk_expr(v, rhs); + } + Expr::Neg(e) => walk_expr(v, e), + Expr::Let { pat, rhs, body } => { + walk_pat(v, pat); + walk_expr(v, rhs); + walk_expr(v, body); + } + Expr::If { cond, then, else_ } => { + walk_expr(v, cond); + walk_expr(v, then); + walk_expr(v, else_); + } + Expr::Match { scrutinee, arms } => { + walk_expr(v, scrutinee); + for (pat, arm) in arms { + walk_pat(v, pat); + walk_expr(v, arm); + } + } + Expr::For { pat, iter, body } => { + walk_pat(v, pat); + walk_expr(v, iter); + walk_expr(v, body); + } + Expr::Tuple(es) | Expr::List(es) => { + for e in es { + walk_expr(v, e); + } + } + Expr::CircuitBlock(stmts) | Expr::RunBlock(stmts) => { + for stmt in stmts { + walk_stmt(v, stmt); + } + } + Expr::Compose(a, b) | Expr::Par(a, b) => { + walk_expr(v, a); + walk_expr(v, b); + } + Expr::ParN(elems) => { + for e in elems { + walk_expr(v, e); + } + } + Expr::Adjoint(e) | Expr::Controlled(e) => walk_expr(v, e), + Expr::GateApp { gate, qubits } => { + walk_expr(v, gate); + walk_expr(v, qubits); + } + Expr::Bind { rhs, body, .. } => { + walk_expr(v, rhs); + walk_expr(v, body); + } + Expr::Return(e) => walk_expr(v, e), + Expr::Borrow { bindings, body } => { + for (_, ty) in bindings { + walk_type(v, ty); + } + for stmt in body { + walk_stmt(v, stmt); + } + } + Expr::Ascribe(e, ty) => { + walk_expr(v, e); + walk_type(v, ty); + } + } + } + v.visit_expr_post(expr); +} + +pub fn walk_stmt(v: &mut V, stmt: &Sp) { + if matches!(v.visit_stmt_pre(stmt), Traversal::Recurse) { + match &stmt.0 { + Stmt::Bind { pat, rhs } | Stmt::Let { pat, rhs } => { + walk_pat(v, pat); + walk_expr(v, rhs); + } + Stmt::Expr(e) => walk_expr(v, e), + } + } + v.visit_stmt_post(stmt); +} + +pub fn walk_pat(v: &mut V, pat: &Sp) { + if matches!(v.visit_pat_pre(pat), Traversal::Recurse) { + match &pat.0 { + Pat::Wildcard | Pat::Var(_) | Pat::Lit(_) => {} + Pat::Tuple(ps) => { + for p in ps { + walk_pat(v, p); + } + } + } + } + v.visit_pat_post(pat); +} + +pub fn walk_type(v: &mut V, ty: &Sp) { + if matches!(v.visit_type_pre(ty), Traversal::Recurse) { + match &ty.0 { + Type::Qubit + | Type::Bit + | Type::Bool + | Type::Int + | Type::Float + | Type::Unit + | Type::Nat + | Type::Var(_) => {} + Type::QReg(n) => walk_nat_expr(v, n), + Type::List(inner) => walk_type(v, inner), + Type::Tuple(parts) => { + for t in parts { + walk_type(v, t); + } + } + Type::Fn(a, b) | Type::Linear(a, b) => { + walk_type(v, a); + walk_type(v, b); + } + Type::Circuit { n, m, d, .. } => { + walk_nat_expr(v, n); + walk_nat_expr(v, m); + walk_nat_expr(v, d); + } + Type::Q(inner) => walk_type(v, inner), + Type::Matrix(r, c, elem) => { + walk_nat_expr(v, r); + walk_nat_expr(v, c); + walk_type(v, elem); + } + Type::QecBlock { family, distance } => { + walk_type(v, family); + walk_nat_expr(v, distance); + } + Type::Named { args, .. } => { + for arg in args { + walk_nat_expr(v, arg); + } + } + } + } + v.visit_type_post(ty); +} + +pub fn walk_nat_expr(v: &mut V, ne: &Sp) { + if matches!(v.visit_nat_expr_pre(ne), Traversal::Recurse) { + match &ne.0 { + NatExpr::Lit(_) | NatExpr::Var(_) | NatExpr::Hole => {} + NatExpr::Add(a, b) + | NatExpr::Mul(a, b) + | NatExpr::Sub(a, b) + | NatExpr::Div(a, b) + | NatExpr::Exp(a, b) => { + walk_nat_expr(v, a); + walk_nat_expr(v, b); + } + } + } + v.visit_nat_expr_post(ne); +} + +#[cfg(test)] +mod tests { + //! The synthetic new-node consistency test lives in `frontend/tests/visitor.rs` + //! so it can build a representative program through the public parser and + //! assert pre/post nesting across every node kind without reaching into + //! private modules. +} diff --git a/frontend/tests/visitor.rs b/frontend/tests/visitor.rs new file mode 100644 index 00000000..001b45c7 --- /dev/null +++ b/frontend/tests/visitor.rs @@ -0,0 +1,311 @@ +//! Synthetic new-node consistency test for the canonical AST visitor (issue #399). +//! +//! Builds a representative program exercising declarations, expressions, +//! statements, patterns, types, type parameters, and type-level natural +//! expressions through the public parser, then drives +//! [`frontend::visitor::walk_program`] with a recording visitor. The assertion +//! is that every node kind is visited with correctly nested pre/post pairing: +//! for each `visit_*_pre` there is a matching `visit_*_post` in LIFO order, and +//! the recorded sequence is exhaustive over the node kinds present in the +//! fixture. When a new AST variant lands, this test pins the contract that the +//! canonical traversal reaches it. + +#![cfg(feature = "analyze")] + +use std::collections::VecDeque; + +use frontend::ast::{Decl, Expr, NatExpr, Pat, Stmt, Type}; +use frontend::lexer::Sp; +use frontend::visitor::{Traversal, Visitor, walk_program}; + +/// Records the kind and phase of every visit; `pre`/`post` are paired by kind. +#[derive(Debug, Clone, PartialEq, Eq)] +enum Event { + DeclPre, + DeclPost, + ExprPre(&'static str), + ExprPost(&'static str), + StmtPre, + StmtPost, + PatPre, + PatPost, + TypePre(&'static str), + TypePost(&'static str), + NatExprPre, + NatExprPost, + TypeParamPre, + TypeParamPost, +} + +struct Recorder { + events: VecDeque, + /// Stack of open expression-kind tags; a `pre` pushes, its `post` pops and + /// must match — proves the pre/post nesting is balanced. + expr_stack: Vec<&'static str>, + type_stack: Vec<&'static str>, + /// Counters per node kind, to assert exhaustiveness. + seen_decl: u32, + seen_expr: u32, + seen_stmt: u32, + seen_pat: u32, + seen_type: u32, + seen_nat_expr: u32, + seen_type_param: u32, +} + +impl Recorder { + fn new() -> Self { + Self { + events: VecDeque::new(), + expr_stack: Vec::new(), + type_stack: Vec::new(), + seen_decl: 0, + seen_expr: 0, + seen_stmt: 0, + seen_pat: 0, + seen_type: 0, + seen_nat_expr: 0, + seen_type_param: 0, + } + } +} + +fn expr_tag(e: &Expr) -> &'static str { + match e { + Expr::Int(_) => "Int", + Expr::Float(_) => "Float", + Expr::Bool(_) => "Bool", + Expr::Unit => "Unit", + Expr::Var(_) => "Var", + Expr::Lam { .. } => "Lam", + Expr::App(_, _) => "App", + Expr::TypeApp { .. } => "TypeApp", + Expr::BinOp { .. } => "BinOp", + Expr::Neg(_) => "Neg", + Expr::Let { .. } => "Let", + Expr::If { .. } => "If", + Expr::Match { .. } => "Match", + Expr::For { .. } => "For", + Expr::Tuple(_) => "Tuple", + Expr::List(_) => "List", + Expr::CircuitBlock(_) => "CircuitBlock", + Expr::Compose(_, _) => "Compose", + Expr::Par(_, _) => "Par", + Expr::ParN(_) => "ParN", + Expr::Adjoint(_) => "Adjoint", + Expr::Controlled(_) => "Controlled", + Expr::GateApp { .. } => "GateApp", + Expr::RunBlock(_) => "RunBlock", + Expr::Bind { .. } => "Bind", + Expr::Return(_) => "Return", + Expr::Borrow { .. } => "Borrow", + Expr::Ascribe(_, _) => "Ascribe", + } +} + +fn type_tag(t: &Type) -> &'static str { + match t { + Type::Qubit => "Qubit", + Type::QReg(_) => "QReg", + Type::Bit => "Bit", + Type::Bool => "Bool", + Type::Int => "Int", + Type::Float => "Float", + Type::Unit => "Unit", + Type::Nat => "Nat", + Type::List(_) => "List", + Type::Tuple(_) => "Tuple", + Type::Fn(_, _) => "Fn", + Type::Linear(_, _) => "Linear", + Type::Circuit { .. } => "Circuit", + Type::Q(_) => "Q", + Type::Matrix(_, _, _) => "Matrix", + Type::QecBlock { .. } => "QecBlock", + Type::Var(_) => "Var", + Type::Named { .. } => "Named", + } +} + +impl Visitor for Recorder { + fn visit_decl_pre(&mut self, _d: &Sp) -> Traversal { + self.events.push_back(Event::DeclPre); + self.seen_decl += 1; + Traversal::Recurse + } + fn visit_decl_post(&mut self, _d: &Sp) { + self.events.push_back(Event::DeclPost); + } + + fn visit_expr_pre(&mut self, e: &Sp) -> Traversal { + let tag = expr_tag(&e.0); + self.events.push_back(Event::ExprPre(tag)); + self.expr_stack.push(tag); + self.seen_expr += 1; + Traversal::Recurse + } + fn visit_expr_post(&mut self, e: &Sp) { + let tag = self.expr_stack.pop().expect("expr post without pre"); + assert_eq!(tag, expr_tag(&e.0), "expr pre/post tag mismatch"); + self.events.push_back(Event::ExprPost(tag)); + } + + fn visit_stmt_pre(&mut self, _s: &Sp) -> Traversal { + self.events.push_back(Event::StmtPre); + self.seen_stmt += 1; + Traversal::Recurse + } + fn visit_stmt_post(&mut self, _s: &Sp) { + self.events.push_back(Event::StmtPost); + } + + fn visit_pat_pre(&mut self, _p: &Sp) -> Traversal { + self.events.push_back(Event::PatPre); + self.seen_pat += 1; + Traversal::Recurse + } + fn visit_pat_post(&mut self, _p: &Sp) { + self.events.push_back(Event::PatPost); + } + + fn visit_type_pre(&mut self, t: &Sp) -> Traversal { + let tag = type_tag(&t.0); + self.events.push_back(Event::TypePre(tag)); + self.type_stack.push(tag); + self.seen_type += 1; + Traversal::Recurse + } + fn visit_type_post(&mut self, t: &Sp) { + let tag = self.type_stack.pop().expect("type post without pre"); + assert_eq!(tag, type_tag(&t.0), "type pre/post tag mismatch"); + self.events.push_back(Event::TypePost(tag)); + } + + fn visit_nat_expr_pre(&mut self, _n: &Sp) -> Traversal { + self.events.push_back(Event::NatExprPre); + self.seen_nat_expr += 1; + Traversal::Recurse + } + fn visit_nat_expr_post(&mut self, _n: &Sp) { + self.events.push_back(Event::NatExprPost); + } + + fn visit_type_param_pre(&mut self, _tp: &frontend::ast::TypeParam) -> Traversal { + self.events.push_back(Event::TypeParamPre); + self.seen_type_param += 1; + Traversal::Recurse + } + fn visit_type_param_post(&mut self, _tp: &frontend::ast::TypeParam) { + self.events.push_back(Event::TypeParamPost); + } +} + +/// A fixture exercising a broad slice of node kinds: a kinded type-param fn +/// with a circuit body (stmts, gate apps, composition), a type alias with a +/// `Nat` parameter, patterns (tuple, var), `if`/`match`, `borrow`, and nested +/// `run`-desugared `Bind`. Type-level expressions appear in `QReg` and +/// `Circuit<2*n, ...>`. +const FIXTURE: &str = r#" +type Oracle = QReg + +fn teleport(q: Qubit): Circuit<2, 2, 1, Clifford> = circuit { + let pair = bell() + H @ pair |> CNOT @(pair, q) +} +fn main(): Q = run { + borrow a: Qubit in { + discard(a) + } +} +"#; + +#[test] +fn canonical_visitor_visits_every_node_kind_with_balanced_pre_post() { + let decls = frontend::desugar_program(FIXTURE).expect("fixture must parse+desugar"); + let mut rec = Recorder::new(); + walk_program(&mut rec, &decls); + + // Every node kind present in the fixture was reached. + assert!(rec.seen_decl >= 2, "decls: {}", rec.seen_decl); + assert!(rec.seen_expr >= 5, "exprs: {}", rec.seen_expr); + assert!(rec.seen_stmt >= 1, "stmts: {}", rec.seen_stmt); + assert!(rec.seen_pat >= 1, "pats: {}", rec.seen_pat); + assert!(rec.seen_type >= 3, "types: {}", rec.seen_type); + assert!(rec.seen_nat_expr >= 2, "nat exprs: {}", rec.seen_nat_expr); + assert!(rec.seen_type_param >= 1, "type params: {}", rec.seen_type_param); + + // All pre/post stacks drained: nesting is balanced. + assert!(rec.expr_stack.is_empty(), "unbalanced expr pre/post"); + assert!(rec.type_stack.is_empty(), "unbalanced type pre/post"); + + // Globally, every `pre` has a matching `post` of the same kind. + assert_decl_balanced(&rec.events); + assert_stmt_balanced(&rec.events); + assert_pat_balanced(&rec.events); + assert_nat_expr_balanced(&rec.events); + assert_type_param_balanced(&rec.events); +} + +fn assert_decl_balanced(ev: &VecDeque) { + let pre = ev.iter().filter(|e| matches!(e, Event::DeclPre)).count(); + let post = ev.iter().filter(|e| matches!(e, Event::DeclPost)).count(); + assert_eq!(pre, post, "Decl pre {pre} != post {post}"); +} + +fn assert_stmt_balanced(ev: &VecDeque) { + let pre = ev.iter().filter(|e| matches!(e, Event::StmtPre)).count(); + let post = ev.iter().filter(|e| matches!(e, Event::StmtPost)).count(); + assert_eq!(pre, post, "Stmt pre {pre} != post {post}"); +} + +fn assert_pat_balanced(ev: &VecDeque) { + let pre = ev.iter().filter(|e| matches!(e, Event::PatPre)).count(); + let post = ev.iter().filter(|e| matches!(e, Event::PatPost)).count(); + assert_eq!(pre, post, "Pat pre {pre} != post {post}"); +} + +fn assert_nat_expr_balanced(ev: &VecDeque) { + let pre = ev + .iter() + .filter(|e| matches!(e, Event::NatExprPre)) + .count(); + let post = ev + .iter() + .filter(|e| matches!(e, Event::NatExprPost)) + .count(); + assert_eq!(pre, post, "NatExpr pre {pre} != post {post}"); +} + +fn assert_type_param_balanced(ev: &VecDeque) { + let pre = ev + .iter() + .filter(|e| matches!(e, Event::TypeParamPre)) + .count(); + let post = ev + .iter() + .filter(|e| matches!(e, Event::TypeParamPost)) + .count(); + assert_eq!(pre, post, "TypeParam pre {pre} != post {post}"); +} + +#[test] +fn canonical_visitor_preserves_source_spans() { + // The visitor receives `&Sp`, so `.1` is the node's source span. Verify + // that decl spans actually cover source positions (non-zero length). + let src = "fn f(): Int = 1\n"; + let decls = frontend::parse_program(src).expect("parse"); + struct SpanProbe { + saw_decl_span: bool, + } + impl Visitor for SpanProbe { + fn visit_decl_pre(&mut self, d: &Sp) -> Traversal { + assert!(d.1.start < d.1.end, "decl span is empty"); + self.saw_decl_span = true; + Traversal::Recurse + } + } + let mut probe = SpanProbe { + saw_decl_span: false, + }; + walk_program(&mut probe, &decls); + assert!(probe.saw_decl_span); +} diff --git a/quon_lsp/src/intel/folding_range.rs b/quon_lsp/src/intel/folding_range.rs index ff5b3c4b..4cc7fa66 100644 --- a/quon_lsp/src/intel/folding_range.rs +++ b/quon_lsp/src/intel/folding_range.rs @@ -1,19 +1,24 @@ use frontend::analysis::DocumentAnalysis; -use frontend::ast::{Decl, Expr, Stmt}; +use frontend::ast::{Decl, Expr}; use frontend::lexer::{SimpleSpan, Sp}; +use frontend::visitor::{Traversal, Visitor}; use tower_lsp::lsp_types::FoldingRange; use crate::convert::offset_to_position; /// Folding ranges for `circuit` / `run` / `borrow` / `match` / `for` and multi-line decls. +/// +/// Structural recursion is delegated to [`frontend::visitor`] (issue #399); this +/// module only decides, per visited node, whether its span is a useful fold +/// region (pre-order push before descending into children). pub fn folding_ranges(analysis: &DocumentAnalysis) -> Option> { let mut ranges = Vec::new(); - for (decl, decl_span) in &analysis.decls { - push_fold(&mut ranges, &analysis.src, *decl_span); - match decl { - Decl::Fn { body, .. } => walk_expr(body, &analysis.src, &mut ranges), - Decl::TypeAlias { .. } => {} - } + { + let mut visitor = FoldingRangeVisitor { + src: &analysis.src, + out: &mut ranges, + }; + frontend::visitor::walk_program(&mut visitor, &analysis.decls); } if ranges.is_empty() { None @@ -22,100 +27,42 @@ pub fn folding_ranges(analysis: &DocumentAnalysis) -> Option> } } -fn walk_expr(expr: &Sp, src: &str, out: &mut Vec) { - let (e, span) = expr; - match e { - Expr::CircuitBlock(stmts) | Expr::RunBlock(stmts) => { - push_fold(out, src, *span); - walk_stmts(stmts, src, out); - } - Expr::Borrow { body, .. } => { - push_fold(out, src, *span); - walk_stmts(body, src, out); - } - Expr::Match { scrutinee, arms } => { - push_fold(out, src, *span); - walk_expr(scrutinee, src, out); - for (pat, arm) in arms { - let _ = pat; - walk_expr(arm, src, out); - } - } - Expr::For { pat, iter, body } => { - let _ = pat; - push_fold(out, src, *span); - walk_expr(iter, src, out); - walk_expr(body, src, out); - } - // Desugared `run { … }` keeps the original block span on the outermost Bind/Let. - Expr::Bind { rhs, body, .. } => { - if looks_like_keyword(src, *span, "run") { - push_fold(out, src, *span); - } - walk_expr(rhs, src, out); - walk_expr(body, src, out); - } - Expr::Let { pat, rhs, body } => { - let _ = pat; - // Nested `let … in` spanning multiple lines is a useful fold region. - if span_multiline(src, *span) { - push_fold(out, src, *span); - } - walk_expr(rhs, src, out); - walk_expr(body, src, out); - } - Expr::If { cond, then, else_ } => { - if span_multiline(src, *span) { - push_fold(out, src, *span); - } - walk_expr(cond, src, out); - walk_expr(then, src, out); - walk_expr(else_, src, out); - } - Expr::Lam { params, body } => { - let _ = params; - if span_multiline(src, *span) { - push_fold(out, src, *span); - } - walk_expr(body, src, out); - } - Expr::App(a, b) - | Expr::Compose(a, b) - | Expr::Par(a, b) - | Expr::GateApp { - gate: a, qubits: b, .. - } - | Expr::BinOp { lhs: a, rhs: b, .. } => { - walk_expr(a, src, out); - walk_expr(b, src, out); - } - Expr::Neg(inner) - | Expr::Adjoint(inner) - | Expr::Controlled(inner) - | Expr::Return(inner) - | Expr::Ascribe(inner, _) => walk_expr(inner, src, out), - // Type-level args are `NatExpr`s — only the callee can contain fold regions. - Expr::TypeApp { callee, .. } => walk_expr(callee, src, out), - Expr::Tuple(es) | Expr::List(es) => { - for e in es { - walk_expr(e, src, out); - } - } - Expr::ParN(elems) => { - for e in elems { - walk_expr(e, src, out); - } - } - Expr::Int(_) | Expr::Float(_) | Expr::Bool(_) | Expr::Unit | Expr::Var(_) => {} - } +struct FoldingRangeVisitor<'a> { + src: &'a str, + out: &'a mut Vec, } -fn walk_stmts(stmts: &[Sp], src: &str, out: &mut Vec) { - for (stmt, _) in stmts { - match stmt { - Stmt::Bind { rhs, .. } | Stmt::Let { rhs, .. } => walk_expr(rhs, src, out), - Stmt::Expr(e) => walk_expr(e, src, out), +impl<'a> Visitor for FoldingRangeVisitor<'a> { + fn visit_decl_pre(&mut self, decl: &Sp) -> Traversal { + // A multi-line declaration is itself a fold region. + push_fold(self.out, self.src, decl.1); + Traversal::Recurse + } + + fn visit_expr_pre(&mut self, expr: &Sp) -> Traversal { + let span = expr.1; + match &expr.0 { + // Block-like constructs always anchor a fold. + Expr::CircuitBlock(_) + | Expr::RunBlock(_) + | Expr::Borrow { .. } + | Expr::Match { .. } + | Expr::For { .. } => push_fold(self.out, self.src, span), + // Desugared `run { … }` keeps the original block span on the Bind. + Expr::Bind { .. } => { + if looks_like_keyword(self.src, span, "run") { + push_fold(self.out, self.src, span); + } + } + // These only fold when they actually span multiple lines. + Expr::Let { .. } | Expr::If { .. } | Expr::Lam { .. } + if span_multiline(self.src, span) => + { + push_fold(self.out, self.src, span); + } + _ => {} } + Traversal::Recurse } } diff --git a/quonlint/src/context.rs b/quonlint/src/context.rs index ce6ee125..595d1a46 100644 --- a/quonlint/src/context.rs +++ b/quonlint/src/context.rs @@ -114,109 +114,128 @@ pub fn is_entangling_gate(name: &str) -> bool { ) } +// The structural recursion (which children each node has, and in what order) is +// owned by `frontend::visitor`'s canonical `walk_*` drivers (issue #399). The +// lint-specific concern threaded on top is circuit/borrow nesting: the same +// rule callback must observe `ctx.in_circuit()` / `ctx.borrow_depth()` that +// reflect the enclosing `circuit` / `borrow` block. `LintWalker` implements +// `frontend::visitor::Visitor`, pushing/popping that nesting in the +// pre/post-expr hooks, so the canonical driver descends while the callback sees +// the correct `LintContext` at every node — preserving the exact pre-order +// callback sequence the rules relied on, including the historical quirk that a +// `Bind`/`Let` statement's right-hand side is visited but not descended into. pub fn walk_stmts( ctx: &LintContext<'_>, stmts: &[Sp], visit: &mut dyn FnMut(&LintContext<'_>, &Sp), ) { + let mut walker = LintWalker::new(ctx, visit); for stmt in stmts { - walk_stmt(ctx, stmt, visit); + frontend::visitor::walk_stmt(&mut walker, stmt); } } -pub fn walk_stmt( +pub fn walk_expr( ctx: &LintContext<'_>, - stmt: &Sp, + expr: &Sp, visit: &mut dyn FnMut(&LintContext<'_>, &Sp), ) { - use frontend::ast::Stmt; - match &stmt.0 { - Stmt::Bind { rhs, .. } | Stmt::Let { rhs, .. } => visit(ctx, rhs), - Stmt::Expr(e) => walk_expr(ctx, e, visit), - } + let mut walker = LintWalker::new(ctx, visit); + frontend::visitor::walk_expr(&mut walker, expr); } -pub fn walk_expr( +pub fn walk_fn_bodies( ctx: &LintContext<'_>, - expr: &Sp, visit: &mut dyn FnMut(&LintContext<'_>, &Sp), ) { - use frontend::ast::Expr; - visit(ctx, expr); - match &expr.0 { - Expr::Lam { body, .. } => walk_expr(ctx, body, visit), - Expr::App(a, b) => { - walk_expr(ctx, a, visit); - walk_expr(ctx, b, visit); - } - Expr::BinOp { lhs, rhs, .. } => { - walk_expr(ctx, lhs, visit); - walk_expr(ctx, rhs, visit); - } - Expr::Neg(e) => walk_expr(ctx, e, visit), - Expr::Let { rhs, body, .. } => { - walk_expr(ctx, rhs, visit); - walk_expr(ctx, body, visit); - } - Expr::If { cond, then, else_ } => { - walk_expr(ctx, cond, visit); - walk_expr(ctx, then, visit); - walk_expr(ctx, else_, visit); - } - Expr::Match { scrutinee, arms } => { - walk_expr(ctx, scrutinee, visit); - for (_, body) in arms { - walk_expr(ctx, body, visit); - } - } - Expr::For { iter, body, .. } => { - walk_expr(ctx, iter, visit); - walk_expr(ctx, body, visit); - } - Expr::Tuple(es) | Expr::List(es) => { - for e in es { - walk_expr(ctx, e, visit); - } - } - Expr::CircuitBlock(stmts) => { - ctx.with_circuit(|nested| walk_stmts(nested, stmts, visit)); + let mut walker = LintWalker::new(ctx, visit); + for decl in &ctx.typed.decls { + if let Decl::Fn { body, .. } = &decl.0 { + frontend::visitor::walk_expr(&mut walker, body); } - Expr::Compose(a, b) | Expr::Par(a, b) => { - walk_expr(ctx, a, visit); - walk_expr(ctx, b, visit); + } +} + +/// Adapter that drives the canonical AST traversal while threading lint +/// circuit/borrow nesting into the rule callback. +/// +/// - `visit_expr_pre`: invokes the rule callback with the *outer* nesting +/// state, then pushes circuit/borrow state so descendants see the nested +/// context. `visit_expr_post` pops it. +/// - `visit_stmt_pre`: for `Bind`/`Let`, invokes the callback on the +/// right-hand side and returns [`Traversal::Skip`] (the rhs is observed but +/// not descended into — the historical lint semantics); for `Expr`, +/// recurses normally. +struct LintWalker<'a, 'v> { + base: &'a LintContext<'a>, + /// Circuit-block nesting as a stack so push/pop restores the outer value + /// (a `circuit` inside a `circuit` returns to `true`, not `false`). + in_circuit_stack: Vec, + borrow_depth: u32, + visit: &'v mut (dyn FnMut(&LintContext<'_>, &Sp) + 'a), +} + +impl<'a, 'v> LintWalker<'a, 'v> { + fn new( + base: &'a LintContext<'a>, + visit: &'v mut (dyn FnMut(&LintContext<'_>, &Sp) + 'a), + ) -> Self { + Self { + base, + in_circuit_stack: vec![base.in_circuit()], + borrow_depth: base.borrow_depth(), + visit, } - Expr::ParN(elems) => { - for e in elems { - walk_expr(ctx, e, visit); + } + + fn current_in_circuit(&self) -> bool { + self.in_circuit_stack.last().copied().unwrap_or(false) + } + + /// A child `LintContext` reflecting the current nesting, handed to the + /// rule callback. `child` is private to this module; `LintWalker` lives in + /// the same module so it can reach it. + fn callback(&mut self, expr: &Sp) { + let ctx = self.base.child(self.current_in_circuit(), self.borrow_depth); + (self.visit)(&ctx, expr); + } +} + +impl<'a, 'v> frontend::visitor::Visitor for LintWalker<'a, 'v> { + fn visit_expr_pre(&mut self, expr: &Sp) -> frontend::visitor::Traversal { + use frontend::ast::Expr; + // Pre-order: the callback sees the node with the *outer* nesting state. + self.callback(expr); + match &expr.0 { + Expr::CircuitBlock(_) => self.in_circuit_stack.push(true), + Expr::Borrow { .. } => self.borrow_depth += 1, + // `RunBlock` does NOT enter a circuit context (matches prior walker). + _ => {} + } + frontend::visitor::Traversal::Recurse + } + + fn visit_expr_post(&mut self, expr: &Sp) { + use frontend::ast::Expr; + match &expr.0 { + Expr::CircuitBlock(_) => { + self.in_circuit_stack.pop(); } + Expr::Borrow { .. } => self.borrow_depth -= 1, + _ => {} } - Expr::Adjoint(e) | Expr::Controlled(e) | Expr::Ascribe(e, _) => walk_expr(ctx, e, visit), - // Type-level args are `NatExpr`s, not expressions — only the callee is walkable. - Expr::TypeApp { callee, .. } => walk_expr(ctx, callee, visit), - Expr::GateApp { gate, qubits } => { - walk_expr(ctx, gate, visit); - walk_expr(ctx, qubits, visit); - } - Expr::RunBlock(stmts) => walk_stmts(ctx, stmts, visit), - Expr::Bind { rhs, body, .. } => { - walk_expr(ctx, rhs, visit); - walk_expr(ctx, body, visit); - } - Expr::Return(e) => walk_expr(ctx, e, visit), - Expr::Borrow { body, .. } => { - ctx.with_borrow(|nested| walk_stmts(nested, body, visit)); - } - Expr::Int(_) | Expr::Float(_) | Expr::Bool(_) | Expr::Unit | Expr::Var(_) => {} } -} -pub fn walk_fn_bodies( - ctx: &LintContext<'_>, - visit: &mut dyn FnMut(&LintContext<'_>, &Sp), -) { - for decl in &ctx.typed.decls { - if let Decl::Fn { body, .. } = &decl.0 { - walk_expr(ctx, body, visit); + fn visit_stmt_pre(&mut self, stmt: &Sp) -> frontend::visitor::Traversal { + use frontend::ast::Stmt; + match &stmt.0 { + // `Bind`/`Let`: observe the rhs, do not descend (preserves prior + // walker semantics where only `Stmt::Expr` is recursed into). + Stmt::Bind { rhs, .. } | Stmt::Let { rhs, .. } => { + self.callback(rhs); + frontend::visitor::Traversal::Skip + } + Stmt::Expr(_) => frontend::visitor::Traversal::Recurse, } } }