refactor: one shared AST walker for the hand-rolled visitors - #161
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Team Run ID: 📒 Files selected for processing (11)
📝 WalkthroughWalkthroughChangesThe PR adds a configurable AST walker to Shared AST walker
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant AST
participant walk_stmts
participant collect_dynamic_bindings_stmts
participant BindingSets
AST->>walk_stmts: provide statements and subexpressions
walk_stmts->>collect_dynamic_bindings_stmts: visit nodes with function depth
collect_dynamic_bindings_stmts->>BindingSets: record native symbols and dynamic bindings
walk_stmts-->>collect_dynamic_bindings_stmts: continue through configured children
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Important
The conversion itself is equivalence-clean — I audited all eleven families against their removed hand-rolled recursion and found no traversal deviations — but two of the converted ry-workspace walkers ship without the pinning test #152's acceptance criteria require. Details below.
Reviewed changes
Full diff (11 files, 2 commits) with a family-by-family equivalence audit of every conversion against the removed recursion, plus local gates: cargo test --workspace (43 suites, 0 failures), clippy --workspace --all-targets -- -D warnings, and cargo fmt --check — all green.
- New
ry_core::walkmodule: pre-orderwalk_stmts/walk_stmt/walk_exprskeleton withControlFlow<B>early termination,Descend::Skipsubtree pruning,fn_depthcounting, and a five-knobWalkpolicy constructed via..Walk::ALL. - Eleven hand-rolled visitors converted to thin policy + callback: six
infer/families from #152's list, the #148-erastmt_any/expr_any(now delegating to the same core), the twodump.rspairs (merged), and the two ry-workspace walkers. - RY098 force family left hand-rolled with an in-place rationale (per-child laziness / evaluation-order rules), exactly as #152 sanctioned.
- 13 new tests: exact-set regression pins for the two families that regressed during development (
collect_condition_assignment_names,assigned_names_in_body), plus walker-level knob,fn_depth, andBreak/Skiptests.
Equivalence notes from the audit, for the record: the knob axes check out at every seam — assign_operands gating only <-/<<- (never %<>%, matching every base walker), stmt_any's every-node pred being Call-only (verified against all five leaf predicates), index_scope_bodies's index-then-Skip matching index_function_body's own recursion, and vector_intent_parameters's FunctionDef-skip vs function-literal-walk split.
⚠️ Two converted walkers ship without the pinning test #152 requires
#152's acceptance criteria say "each converted walker keeps a test that pins its difference." The two ry-workspace conversions have none. attached_packages is only exercised by top-level library(...) calls in CLI e2e fixtures, so the "counts wherever the call appears" semantic this PR's new doc sentence claims is unpinned. And nothing in the worktree tests collect_dynamic_bindings_stmts at all — its most delicate semantic, the fn_depth == 0 gate that keeps a bare two-argument assign("x", v) namespace-scoped only at file top level, now rides on the new walker with zero coverage. This PR's own pre-push history (two traversal regressions, both in families that lacked pins at the time) is the argument for why these two need pins before merge.
Technical details
# Pin the ry-workspace walker conversions
## Affected sites
- crates/ry-workspace/src/packages.rs:130 — `attached_packages`: walks `Walk::ALL`
including function bodies; only coverage is top-level `library()` in
`crates/ry-cli/tests/config_e2e.rs` fixtures.
- crates/ry-workspace/src/lib.rs:397 — `collect_dynamic_bindings_stmts`: the
`name == "assign" && fn_depth == 0` arm, the positional/named `envir` arms,
and the `.Call(ffi_enquo)` native-symbol witness have no test references
anywhere (`grep -rn "delayedAssign\|makeActiveBinding" crates` hits only the
implementation and an unrelated NSE list).
## Required outcome
- A test pinning `library()`/`require()` inside a nested function body still
attaching the package.
- A test pinning bare two-argument `assign("x", v)` recording at `fn_depth == 0`
but NOT inside a function body, while named/positional `envir` variants still
record at depth >= 1.
## Suggested approach
Parse small sources with `RParser` and call the two functions directly,
mirroring the `assert_exact` HashSet style of the new test module in
`crates/ry-checker/src/infer/binop.rs`.ℹ️ Nitpicks
- The PR body's "+10 new tests" undercounts: the diff adds 13
#[test]functions (5 inbinop.rs, 4 inindex.rs, 4 inry-core/src/walk.rs). - "exactly one walker exists in the codebase" shouldn't be read literally:
collect_parameter_uses_in_stmt/_in_exprandfirst_parameter_use_in_stmt/_in_expr(crates/ry-checker/src/collect.rs:759,:885) also hand-roll Stmt/Expr recursion. They're outside #152's listed scope and fit the same per-child-rules exclusion documented for the RY098 family, but a follow-up issue would keep them from being lost.
openai-compatible/glm-5.3 | 𝕏
|
Addressed the pinning-test finding in e938dbb: 9 direct tests on the two ry-workspace walkers — nested-body |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Re-reviewed the delta since the prior review (b825060): a single commit, e938dbb, adding exactly the two ry-workspace pinning test modules the prior review asked for. Full-diff coverage re-confirmed across all 11 files; cargo test -p ry-workspace (19/19), clippy --all-targets -- -D warnings, and cargo fmt --check all green locally.
- Pinned
collect_dynamic_bindings_stmtswith 7 exact-set tests indynamic_binding_tests: thefn_depth == 0bare-assigngate (braced blocks don't count toward depth, function bodies do), namedenvir/env/assign.envrecording at any depth, positional environment slots (third argument ofassign/makeActiveBinding, fourth ofdelayedAssign), a named non-environment argument (inherits =) staying depth-gated, non-literal target names recording nothing, and the.Call(ffi_sym, ...)native-symbol witness with its string-entry-point and named-argument negative controls. - Pinned
attached_packageswith 2 tests:library()/require()attaching from inside function bodies and a nestedif, plus negative controls (requireNamespace(),loadNamespace(), a runtime-computed package name, andmy_library). - Verified both pins actually bite via mutation: deleting the
fn_depth == 0gate makesbare_assign_records_only_at_top_levelfail ({"top", "nested"}), and walking withfn_bodies: falsemakes the attachment test fail ({"dplyr"}only). Every expectation was also cross-checked line-by-line against both the converted walker and the removed hand-rolled recursion. - Corrected the PR body test count: "+22 new tests" now matches the actual 22
#[test]additions (13 from the earlier commits + 9 here).
openai-compatible/glm-5.3 | 𝕏
Adds ry_core::walk: a single pre-order Stmt/Expr walker with (a) a Walk policy struct naming the five subtree edges the hand-rolled visitors disagreed on (assignment targets, `<-` operands, `$` subscript idents, function bodies, control tests), (b) a per-node Descend::Skip answer for one-off prunes, (c) ControlFlow early termination with payload, and (d) a fn_depth count for callbacks that need function-nesting depth. The walkers differed on purpose; the knobs expose the differences rather than erasing them. Converted onto it (each site states its skip policy in one line): - checker: collect_condition_assignment_names, assigned_names_in_body, collect_executed_identifiers (merged with collect_identifiers_in_stmt), collect_forwarded_calls_in_stmts (merged with _in_expr), vector_intent_parameters' visit pair, r6_rebound_members' visit pair, and PR #148's stmt_any/expr_any in collect.rs, which now delegate to the shared walker so exactly one walker exists. - cli dump.rs: collect_local_bindings (+ _in_expr) and index_scope_bodies (+ _in_expr). - workspace: the attachment walkers in packages.rs and collect_dynamic_bindings_stmt/_expr in lib.rs (depth tracking moves into the walker's fn_depth). Left hand-rolled with a comment saying why: the RY098 force/identifier family in infer/misc.rs. Its rules select individual children of a node (call args skipped unless the callee is a strict builtin, literal-cond `if` takes one branch, `$` idents skipped while the base is kept) and stop at the first identifier forced in evaluation order -- an evaluation-order analysis, not a subtree-skip policy. Net -248 lines (949 deleted, 344 inserted, 357-line walker added). All corpus/oracle/vendor snapshots byte-identical.
e938dbb to
c29c0ed
Compare

Cleanup sprint, PR 6 of 7 (stacks on #160). Closes #152. Net −18 lines overall, but that's a new 357-line reusable walker module (+tests) against a −605 line reduction across the eleven files that had hand-rolled walkers. Every consumer is now a thin policy + callback.
The walker (
ry_core::walk)walk_stmts/walk_stmt/walk_expr: pre-order,ControlFlow<B>early-termination with payload,fn_depthcounting.Walkpolicy struct, five boolean knobs:assign_targets,assign_operands,dollar_args($-synthesized idents),fn_bodies,control_tests— constructed via..Walk::ALL(noDefault, so partial-literal misuse is impossible).stmt_any/expr_anyin collect.rs now delegate to it: exactly one walker exists in the codebase, where before this PR there were two designs.Conversion table
collect_condition_assignment_names(binop.rs)assigned_names_in_body(index.rs)collect_executed_identifierspair (misc.rs)collect_forwarded_callspair (misc.rs)vector_intent_parameters(mod.rs)r6_rebound_members(call.rs)stmt_any/expr_any(collect.rs)collect_local_bindingspair (dump.rs)index_scope_bodiespair (dump.rs)Nice confirmation of the knob axes: dump.rs's two walkers hold opposite fn-body policies (skip vs. walk); one
fn_bodiesflag expresses both.Caught in review, fixed before push (b825060)
The family-by-family equivalence audit found two real traversal regressions where a wildcard
Descend::Skiparm was one variant too broad:assigned_names_in_bodystopped walkingExpr::Block/Expr::Ifin expression position, andcollect_condition_assignment_namesprunedStmt::Exprand non-Ident-target assigns inside condition blocks. Under-collection shapes that byte-identical snapshots cannot catch — exactly why the audit exists. Both restored to base semantics, with 9 new exact-set regression tests (4 proven to fail against the broken version). A third flagged case turned out to be a false positive (assign_operandswas alreadytrueviaWalk::ALL) — now explicit and pinned.Gates
cargo test --workspace(43 suites, +22 new tests), clippy-D warnings, fmt, perf suite (8/8--release -- --ignored), oracle suite with R; snapshots byte-identical throughout, including after the fixes.Summary by CodeRabbit
library()andrequire(), including nested calls.