feat(ops): abstract op groups with TOML-driven concrete instances#116
feat(ops): abstract op groups with TOML-driven concrete instances#116jellllly420 wants to merge 32 commits into
Conversation
03b4f36 to
352e8a9
Compare
There was a problem hiding this comment.
Pull request overview
Adds “abstract ops” to the RPL toolchain: .rpl patterns can declare parameterized op groups in a new ops { ... } block and reference them via $group::$op, while concrete instances are supplied via [[ops.<group>]] in rpl.toml. The driver/matcher then iterates over the cartesian product of referenced op-group instances and resolves $group::$op to concrete Rust DefIds during MIR matching.
Changes:
- Extend the parser/grammar and pattern IR to support
ops { ... }blocks andOpRefoperands ($group::$op). - Load
[[ops.<group>]]instances fromrpl.toml, resolve/expand them, and thread op bindings through the matcher/driver with cartesian-product enumeration. - Add unit/integration tests plus end-to-end “features” fixtures (including a CVE POC) and a design spec document.
Reviewed changes
Copilot reviewed 49 out of 51 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/features/test.sh | Expands the manual features test script to run new ops fixtures; adjusts shell strictness. |
| tests/features/ops/two_groups.rs | New feature fixture exercising cartesian matching across two op groups. |
| tests/features/ops/two_groups.rpl | New pattern using two op refs (sync_2g and logger_2g). |
| tests/features/ops/set_op_with_ops.rs | New feature fixture covering set-op subtraction with ops-based util patterns. |
| tests/features/ops/set_op_with_ops.rpl | New pattern demonstrating p_lock - p_uncovered with op refs. |
| tests/features/ops/partial_bad.rs | New feature fixture for “skip malformed instance, keep others” behavior. |
| tests/features/ops/partial_bad.rpl | New pattern for partial-bad instance scenario. |
| tests/features/ops/lock_unlock.rs | New basic feature fixture for a single op group instance. |
| tests/features/ops/lock_unlock.rpl | New lock pattern referencing $sync::$lock. |
| tests/features/ops/folded.rs | New fixture verifying fold-to-zero when no instances exist. |
| tests/features/ops/folded.rpl | New “folded” pattern referencing an op group with no instances. |
| tests/features/ops_cve_2025_68260/pattern.rpl | New multi-group ops pattern modelling the CVE bug class. |
| tests/features/ops_cve_2025_68260/fixed.rs | New “fixed” variant fixture that should not match. |
| tests/features/ops_cve_2025_68260/buggy.rs | New “buggy” variant fixture that should match. |
| rust-toolchain | Adds rust-analyzer component to the pinned nightly toolchain config. |
| rpl.toml | Adds workspace ops instances ([[ops.*]]) used by feature fixtures. |
| docs/superpowers/specs/2026-04-28-abstract-ops-design.md | Design doc describing the abstract-ops feature and validation/matching model. |
| crates/rpl_resolve/tests/ops_resolve.rs | Adds resolver-side tests for ops WF / op-ref checks (delegating to rpl_context). |
| crates/rpl_resolve/Cargo.toml | Adds dev-dependencies needed by new tests. |
| crates/rpl_parser/tests/test.rs | Adds parser tests for opsBlock and OpRef parsing. |
| crates/rpl_parser/src/grammar/RPL.pest | Extends grammar with opsBlock, opsItem, OpFnDecl, and OpRef. |
| crates/rpl_meta/src/meta.rs | Updates block collection to include ops blocks. |
| crates/rpl_meta/src/check.rs | Updates MIR operand checking for the new OpRef grammar variant. |
| crates/rpl_match/src/statement.rs | Adds runtime OpRef matching that resolves to MIR DefId via expanded paths. |
| crates/rpl_match/src/mir.rs | Threads op bindings into MIR matching context. |
| crates/rpl_match/src/matches/color.rs | Threads op bindings into the “color” matcher context. |
| crates/rpl_driver/tests/ops_threading.rs | Smoke test ensuring ops config threading compiles and can be empty. |
| crates/rpl_driver/tests/cartesian.rs | Unit tests for the new cartesian-product helper. |
| crates/rpl_driver/src/lib.rs | Loads raw ops from rpl.toml, resolves them, iterates cartesian products, and threads bindings into matching. |
| crates/rpl_driver/Cargo.toml | Adds dependency on rpl_config for ops loading. |
| crates/rpl_context/tests/referenced_op_groups.rs | Tests referenced op-group collection in lowered patterns. |
| crates/rpl_context/tests/ops_wf.rs | Tests ops-block WF checks (R1–R3). |
| crates/rpl_context/tests/ops_resolution.rs | Tests ops instance substitution/validation (resolve_ops_config). |
| crates/rpl_context/tests/ops_lowering.rs | Tests lowering of ops blocks into Pattern.ops_block. |
| crates/rpl_context/src/pat/ops.rs | Introduces OpsBlock, OpGroup, and OpSignature IR structures. |
| crates/rpl_context/src/pat/ops_wf.rs | Implements WF checks (R1–R3) plus parse-tree R6 helper. |
| crates/rpl_context/src/pat/ops_uses.rs | Implements post-lowering op-ref use-site checks (R4–R5) and group collection. |
| crates/rpl_context/src/pat/ops_resolved.rs | Implements ops instance expansion/validation and per-attempt bindings structures. |
| crates/rpl_context/src/pat/mod.rs | Integrates ops blocks/op refs into the pattern IR and population pipeline. |
| crates/rpl_context/src/pat/mir/visitor.rs | Updates visitor to traverse the new Operand::OpRef variant. |
| crates/rpl_context/src/pat/mir/pretty.rs | Adds formatting/debug output for Operand::OpRef. |
| crates/rpl_context/src/pat/mir/mod.rs | Adds Operand::OpRef and parsing/lowering support for it. |
| crates/rpl_context/src/context.rs | Wires ops blocks into pattern loading and triggers op-ref validation/population. |
| crates/rpl_context/Cargo.toml | Adds rpl_config dependency for ops instance types in context/tests. |
| crates/rpl_config/tests/ops_loading.rs | Adds TOML deserialization tests for ops instances. |
| crates/rpl_config/src/patterns.rs | Fixes empty resolved pattern paths mapping to avoid RPL_PATS="" failures. |
| crates/rpl_config/src/ops.rs | Adds RawOpInstance TOML deserializer (type = [...] + string bindings). |
| crates/rpl_config/src/lib.rs | Exposes ops loading (load_raw_ops) and adds ops to config schema. |
| Cargo.lock | Updates lockfile for new/changed crate dependencies. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
352e8a9 to
8c041cd
Compare
When a `PatternOperation` (set-op composition like `p = p_a - p_b`)
references multiple op groups, the outer `{impl,fn}_matched_pat_op`
iterates the cartesian product over the union of all referenced
groups and threads one combo through to each sub-item.
`{impl,fn}_matched_pat_item` then matched on the sub-item. In the
`RustItems` arm it inspected the sub-item's own
`referenced_op_groups()` and started a *fresh* local cartesian
iteration — silently discarding the outer combo's choices for any
group this particular sub-item happened to reference. In a
`set_op_with_ops`-style composition where each util references a
different (or overlapping) subset of the union, this could pick
mismatched combos across positive/negative sub-items, breaking the
subtraction semantics.
Bug surfaced by Copilot's review of PR RPL-Toolchain#116 (review thread on lines
568 and 758).
The fix splits the two paths cleanly:
- bindings non-empty (came from an outer pat_op cartesian): descend
with bindings unchanged.
- bindings empty (top-level direct entry into a RustItems): iterate
the local cartesian over this RustItems' own referenced groups,
which is the original intended behaviour.
cargo fmt, cargo clippy --workspace --all-targets -- -D warnings, and
cargo test --workspace are all clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`check_r6_patt_vs_ops` was implemented in `pat::ops_wf` and
`pub use`-exported from `pat::mod` but never actually invoked by
`add_parsed_pattern`. As a result, R6 was silently unenforced: an
`.rpl` file declaring `sync[$T: type] = { ... }` and then
referencing `$T` inside a `patt {}` body (where it would be an
undeclared meta-var at the pattern level) would slip through
well-formedness checking and either produce surprising matches or
crash later during lowering.
Wired in alongside the existing R1–R3 check, after `add_ops_block`
runs for every ops block. R6 inspects the raw parse tree (it
walks `MirBody` for `TypeMetaVariable` references) so it must
run before `add_pattern_item` lowers the patt block.
Iteration variable in the surrounding ops loop is now borrowed
(`for ops_block in &ops`) so `ops` remains available for the R6
call below.
Bug surfaced by Copilot's review of PR RPL-Toolchain#116 (review thread on
`crates/rpl_context/src/context.rs` line 206).
cargo build --workspace, cargo fmt --check, cargo clippy
--workspace --all-targets -- -D warnings, cargo test --workspace
all clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@TheVeryDarkness ready for another look when you have time — both of your comments have been addressed:
The 6 Copilot threads on the previous round are also disposed of (3 fixed in CI green on the latest HEAD ( Since GitHub doesn't let fork contributors re-request reviewers, this comment is the closest I can do to a rebadge. Thanks for the careful review! — Generated by Claude Opus 4.7 (1M context) on behalf of @jellllly420 |
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… idiom) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 2 added OpRef as a sixth alternative to MirFnOperand, which shifted the auto-generated pest_typed Choice5 -> Choice6. Two hand-written match sites needed updating to compile. The new arm delegates to a Task 4 stub since OpRef IR lowering lands there. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the OpRef IR node so $group::$op call targets in patterns become
typed Operand::OpRef { group, op }. Replaces the Task 2 placeholder
todo! in from_fn_op with real lowering, and adds stub arms wherever
matches over Operand are now non-exhaustive (resolver and matcher
implementations land in later tasks).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous Operand::OpRef lowering preserved the $-prefix from the MetaVariable span, producing $-prefixed symbols. But OpGroup.name will be lowered from Identifier, which is bare. Task 7's resolver does a direct lookup against ops_block.groups, so the keys must match. Adds a parser-round-trip test to catch this kind of regression. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an ops_block field to Pattern<'pcx> and an add_ops_block lowering pass that walks pest's pairs::opsBlock and populates ops_block.groups with OpGroup entries. Hooks into the top-level Block dispatcher alongside pattBlock/utilBlock/diagBlock. Extends collect_blocks in rpl_meta to also return opsBlock slices so that add_parsed_pattern in context.rs can dispatch them. A local OpsMetaLookup struct implements GetType for resolving type meta- variables ($T, $U) when lowering op-fn parameter and return types; path types are not supported in ops items and will panic if attempted. Adds crates/rpl_context/tests/ops_lowering.rs with two integration tests that exercise the full parse→collect→lower pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses code review on Task 5: - Verify and document the index-counter alignment between OpsMetaLookup and NonLocalMetaVars::from_meta_decls (Path A: indices are correct as-is — both count only type-kind decls, starting independently at 0, matching NonLocalMetaVars' separate IndexVec per kind). - Replace panics in OpsMetaLookup with unreachable! and messages that explicitly reference resolver check R1 as the precondition. - Add a TODO marker on DUMMY_SP uses tying them to Task 6. - Add a test exercising multiple op groups within a single block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds three resolver checks that reject malformed ops blocks before lowering hits the unreachable! contracts: - R1: op-level meta-vars must be of kind 'type' (v1 restriction). - R2: op signatures may only reference meta-vars declared on the same op group. - R3: op signatures must be abstract — concrete Rust paths belong in rpl.toml. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds three more well-formedness checks plus per-pattern accounting: - R4: $group::$op references must resolve to a declared op group and op signature. Wrong group → "op group '<g>' is not declared"; wrong op within a known group → "op '<o>' is not declared in op group '<g>'". - R5: Op-call arity must match the op signature's arity at the call site. - R6: Pattern bodies cannot reference op-level meta-vars; those are scope-isolated to the ops block. Adds RustItems::referenced_op_groups() returning the set of op groups referenced by OpRefs anywhere in the pattern body. This feeds Task 11's cartesian-product iteration over instances. Also: replace the Task 4 todo!() stub in super_operand for Operand::OpRef with a proper no-op (OpRef carries no sub-operands to visit). Includes parser.rs changes from Tasks 1-4 that were uncommitted in the working tree. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds resolve_ops_config() that turns rpl_config::RawOpInstance into typed ResolvedOpInstance for the matcher. Implements C2-C7: - C2/C3: missing meta-var/op binding -> warning, skip instance. - C4: unknown extra key -> warning, skip. - C5: undeclared placeholder in template -> warning, skip. - C6: post-expansion parse failure -> warning, skip (deferred to match time). - C7: cyclic substitution -> warning, skip. Eager $T/$U expansion at load time via 16-iteration fixed-point loop; only $1/$2-style existentials remain when the matcher consumes the result. Deferred-parse path chosen for Step 5: binding values stored as post-expansion strings (BTreeMap<Symbol, String>) rather than typed Ty<pcx>/Path<pcx>. This keeps the implementation self-contained and avoids threading PatCtxt through resolution. C6 parse-clean check deferred to match time (Task 12). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Loads OpsConfig in check_crate from RplConfig.ops via resolve_ops_config() and threads it through impl_matched_pat_op so Task 11's cartesian-product iteration can consume the resolved instances. Surfaces ResolveDiagnostics as rustc warnings. Adds load_raw_ops() to rpl_config for the driver process to read rpl.toml without going through the cargo-wrapper path. Stores the raw ops vec on CheckFnCtxt and resolves per-pattern OpsConfig inside each for_each_rpl_pattern closure in check_fn/check_assoc_fn. The OpsConfig consumer code (cartesian iteration, OpRef resolution during matching) lands in Tasks 11 and 12. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps impl_matched_pat_op (and fn_matched_pat_op) with a cartesian- product loop over op-instance combinations. For each combination, builds a ResolvedOpBindings view and invokes the renamed impl_matched_pat_op_with_bindings (the original body, lifted out). The "fold on empty instances" property falls out of the cartesian helper: an empty factor folds the product to zero combinations, which contributes the empty match-set to set-op composition. Adds PatternOperation::referenced_op_groups() returning the union of op-group names referenced by any operand of this set-op (recursive through nested PatternOperations). Exports pub fn cartesian<T,I,II> (odometer-order iterator) and pub struct ResolvedOpBindings from rpl_driver; four unit tests cover the four corner-cases including fold-on-no-instances. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
At match time, when an Operand::OpRef { group, op } is encountered,
look up the active ResolvedOpInstance for the group, fetch the
expanded path string for the op, strip generic-arg brackets, split
on "::", intern to Symbols, build an ItemPath, and delegate to the
existing match_item_path_by_def_path matcher.
ResolvedOpBindings moved from rpl_driver (lifetime-parameterised) to
rpl_context::pat::ops_resolved as an owned struct (clones instances).
CheckMirCtxt gains an op_bindings field and a new_with_bindings
constructor. MatchStatement gains an op_bindings() method implemented
by both CheckMirCtxt and MatchCtxt.
Op-typed parameter handling: v1 punt — pattern-level type meta-vars
continue to match permissively; only call-target OpRef resolution is
wired end-to-end. The lock_unlock e2e tests (Task 13) will determine
if this is sufficient.
The bindings flow through impl_matched_pat_op_with_bindings and
fn_matched_pat_op_with_bindings (Task 11) into impl_matched /
fn_matched, which construct CheckMirCtxt::new_with_bindings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Exercises every layer of the abstract-ops feature: parser surface, ops block lowering, OpRef IR, OpsConfig substitution, cartesian iteration, and matcher OpRef resolution against std::sync::Mutex. A single rpl.toml [[ops.sync]] instance binds T -> Mutex<$1>, U -> MutexGuard<$1>; the pattern's $sync::$lock matches the m.lock() call site. Also fixes a gap found during testing: fn_matched_pat_item and impl_matched_pat_item for RustItems patterns were passing empty ResolvedOpBindings to CheckMirCtxt instead of iterating over the cartesian product of (group -> instance) assignments. The fix mirrors the existing cartesian-product logic in fn_matched_pat_op, using referenced_op_groups() to determine which groups to expand. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add four end-to-end UI tests for abstract-ops properties: - folded: ops group declared in .rpl with no rpl.toml instances → cartesian product of zero combos → zero matches → no diagnostics. - partial_bad: three [[ops.sync_pb]] entries, the third missing the 'unlock' binding (fails C3 validation and is silently skipped); the first valid instance (Mutex) still fires the lint. - set_op_with_ops: util patterns p_lock / p_uncovered both reference ops; patt = p_lock[] - p_uncovered[]. lock_only() fires, lock_and_mark() is subtracted. Demonstrates set-op composition with ops. - two_groups: two independent op groups (sync_2g × logger_2g); pattern uses both in one function body. Cartesian product (2×1=2 combos) fires only for the (Mutex, black_box) combination present in the test file. Bug fix (context): check_and_populate_op_refs previously only processed patt_block items. util_block items (arena-allocated, shared refs) were not populated with referenced_op_groups, causing the set-op and two-groups patterns to produce zero matches. Fixed by also iterating util_block items using an unsafe cast (sound: bump-arena items, single-threaded construction, field written only once before any reader observes it). test.sh: comment out the broken +rpl-dbg invocation; remove -e from set -euxo pipefail; add || true after invocations expected to exit 1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ttern Demonstrates the abstract-ops feature catching a real-world bug class. CVE-2025-68260 is the first Rust CVE in the Linux kernel: a race condition in Node::release where intrusive list elements get drained outside the lock that owned them. The pattern uses two op groups (sync_cve and intrusive_list_cve) to describe the bug structurally; rpl.toml binds them to std::sync::Mutex and stub free-function paths. buggy.rs mirrors the pre-fix call shape (lock -> transfer -> unlock -> drain) and matches; fixed.rs omits the explicit unlock sink (guard drops naturally) so the pattern's $unlock step finds no candidate and structurally fails to match. Key implementation notes: - #[inline(never)] on stub fns prevents MIR-inline from erasing the call sites that the pattern matches. - std::intrinsics::black_box (via core_intrinsics feature) is used as the opaque unlock sink; it is not inlined and resolves to a stable DefId matching the rpl.toml "unlock" binding. - Pattern arguments use wildcards (_) for the mutable-reference parameters since optimized MIR emits copy operands for &mut locals (which are not Copy, so move/copy cross-kind check would reject). This is the headline e2e validation: one pattern, swappable primitives, real bug class. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's cargo fmt --check step was failing on both ubuntu-latest and
windows-latest, blocking clippy and test runs. The diff is purely
mechanical:
- trailing commas on match-arm closing braces (rustfmt.toml has
match_block_trailing_comma = true)
- multi-line struct literals where the single-line form exceeded
max_width = 120
- merged use-statements per imports_granularity = "Module"
- comment wrapping per comment_width = 100, wrap_comments = true
No behavioural change. cargo build --workspace passes after the
formatting pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`Pattern::check_and_populate_op_refs` mutated a field on `util_block` items via a shared reference, casting `&PatternItem` through a raw pointer to obtain a `&mut PatternItem`. Although guarded by `#[allow(invalid_reference_casting)]` and a SAFETY comment, writing through a `&T` is undefined behaviour per the Rust reference regardless of aliasing — the compiler is free to assume `&T` is immutable for optimisation. The fix wraps `referenced_op_groups` in a `std::cell::OnceCell`, which exposes write-once interior mutability via a shared reference. Both the `patt_block` (owned) and `util_block` (arena-shared) iterations now use the same code path with no `unsafe` and no raw-pointer surgery. Reads before population return a durable empty set via `OnceCell::get_or_init`; a second `set` on an already-initialised cell is silently ignored, which preserves the existing invariant that this function runs once. All rpl_context, rpl_driver, rpl_match, rpl_resolve, and rpl_config unit/integration tests pass after the change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… empty test Two small hygiene fixes consolidated into one commit: 1. `OpsConfig::instances` and `ResolvedOpBindings::by_group` switch from `std::collections::HashMap<Symbol, _>` to `FxHashMap`. `HashMap` uses a randomly-seeded `RandomState` by default, which makes diagnostic ordering and cartesian-product iteration order non-deterministic across runs. rustc-internal data structures consistently use `FxHashMap` (deterministic seed, faster hash) for this exact reason. 2. Drop the `wf_no_errors_for_valid_ops_block` test in `ops_wf.rs` — it was `assert!(true, "placeholder")` with a comment admitting the real coverage lives in `tests/ops_lowering.rs` and `tests/ops_wf.rs`. Replaced with a one-line comment pointing at the integration tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tderr
Three coupled fixes to the end-to-end UI fixtures:
H5 (DefId mismatch). set_op_with_ops.rs and two_groups.rs called
`std::hint::black_box`, but rpl.toml binds the matching op to
`std::intrinsics::black_box`. The two paths resolve to different
DefIds at MIR level; tests previously passed only because the default
MIR inliner happened to devirtualise the hint::black_box re-export.
Switch both sources to call the intrinsic directly (with
`#![feature(core_intrinsics)]` + `#![allow(internal_features)]`),
matching the convention already used by the CVE POC.
H6 (inlining-policy independence). Only lock_unlock.rs set
`-Zinline-mir=false`; every other ops test relied on the default
inliner not devirtualising `Mutex::lock` / `RwLock::read`. Add
the flag uniformly so the tests are stable against toolchain
inlining changes.
H7 (signal-to-noise in CVE fixtures). buggy.rs and fixed.rs emitted
four unrelated warnings (`internal_features`, two `dead_code` on
struct fields, `unused_must_use` on a discarded
`black_box(g)`-of-Result). These drown the actual lint and rot
with each rustc upgrade. Allow the three categories at file scope
(narrow, targeted, easy to audit).
Bonus: rpl.toml's two_groups comment claimed two logger_2g instances
when only one is declared; corrected to match reality. test.sh's
matching comment updated likewise.
The .stderr fixtures under tests/features/ops/ and
tests/features/ops_cve_2025_68260/ are deleted: they contained raw
`cargo run` output ("Finished `dev` profile…", absolute target
paths) plus literal line numbers, and are not consumed by
`tests/compile-test.rs` (which only registers tests/ui). Wiring
those directories into compile-test.rs and re-blessing properly is
tracked as a follow-up (see PR body "Deferred work").
cargo build --workspace passes; rpl_context, rpl_driver, rpl_match,
rpl_resolve, rpl_config, rpl_parser unit/integration tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…seless_lifetime, useless_into_iter)
`cargo clippy --workspace --all-targets -- -D warnings` (the CI
invocation, which fmt was previously masking) surfaced 8 lints in
the new ops code:
- rpl_context/src/pat/ops_resolved.rs: two functions named a
`'pcx` lifetime parameter that only appeared in a single
parameter type; elided to `'_` (clippy::needless_lifetimes).
- rpl_context/src/pat/ops_uses.rs: triple-nested `if let ... {
if let ... { if let ... { } } }` for the
`Option<TerminatorKind::Call(Operand::OpRef { .. })>` walk;
flattened into a single pattern (clippy::collapsible_match).
- rpl_context/src/pat/ops_wf.rs: `check_op_sig(group_name, &sig,
...)` — `sig` is already `&pairs::OpFnSig`, the `&`
is redundant (clippy::needless_borrow).
- rpl_driver/tests/cartesian.rs (x3): `cartesian(iters.into_iter())`
where `cartesian` accepts `IntoIterator` directly; dropped the
redundant `.into_iter()` (clippy::useless_conversion).
`cargo clippy --workspace --all-targets -- -D warnings` is now
clean. `cargo test --workspace` reports 0 failures across all
suites (including the 207-test compile-test.rs UI run).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ush)
The collapsed `if let Some(TerminatorKind::Call { ... })` pattern in
the clippy-cleanup commit exceeded rustfmt's heuristic for
single-line struct destructuring, and the single-line
`errors.push(format!(...))` invocation underneath it crossed the
implicit wrap-at-comma threshold. Both are now multi-line per
rustfmt's preference; the CI's `cargo fmt --check` step was the
canary.
Pure formatting; no functional change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's 'Test installed rpl' step (cargo install --path . && cargo rpl
--workspace --all-targets --verbose) failed with:
WARN rpl_meta::cli Error[E100]: Cannot locate RPL pattern file `""`
error: An error was found with input RPL pattern(s)
Root cause: when an `rpl.toml` exists that has no `[patterns]`
table (e.g. one carrying only `[run]` or `[[ops.<group>]]` —
exactly the shape of the rpl.toml this PR introduces) AND
`--patterns` is not passed,
`patterns::resolve_patterns` falls through to the `Some(config)`
branch, finds `patterns = None`, and takes the
`selected_groups.iter().all(is_remote_spec)` branch. `all` is
vacuously `true` on an empty iterator (the identity element of
conjunction), so it calls `resolve_remote_selection(.., &[])`,
which loops over nothing and returns
`Ok(ResolvedPatterns { paths: vec![] })`.
`resolve_patterns_env` then joins zero paths into the empty
string and returns `Some(String::new())`. cargo-rpl sets
`RPL_PATS=""` in the child env; rpl-driver splits on `:`,
gets `vec![""]`, and rpl_meta fails to open the empty path.
This was masked on master because no rpl.toml existed, so the
earlier `config: None` branch (line 54-62) short-circuits to
`Ok(None)`. My PR adds the workspace rpl.toml (for ops
instances), which exposes the path.
The defensive fix is at the env-string construction boundary:
`Some("")` is never a valid `RPL_PATS` value — it would always
fail downstream — so map empty paths to `None` and let the
driver fall back to the built-in pattern set, the same behaviour
as when no rpl.toml exists.
Local: cargo fmt --check, cargo clippy --workspace --all-targets
-- -D warnings, cargo test --workspace all clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a `PatternOperation` (set-op composition like `p = p_a - p_b`)
references multiple op groups, the outer `{impl,fn}_matched_pat_op`
iterates the cartesian product over the union of all referenced
groups and threads one combo through to each sub-item.
`{impl,fn}_matched_pat_item` then matched on the sub-item. In the
`RustItems` arm it inspected the sub-item's own
`referenced_op_groups()` and started a *fresh* local cartesian
iteration — silently discarding the outer combo's choices for any
group this particular sub-item happened to reference. In a
`set_op_with_ops`-style composition where each util references a
different (or overlapping) subset of the union, this could pick
mismatched combos across positive/negative sub-items, breaking the
subtraction semantics.
Bug surfaced by Copilot's review of PR RPL-Toolchain#116 (review thread on lines
568 and 758).
The fix splits the two paths cleanly:
- bindings non-empty (came from an outer pat_op cartesian): descend
with bindings unchanged.
- bindings empty (top-level direct entry into a RustItems): iterate
the local cartesian over this RustItems' own referenced groups,
which is the original intended behaviour.
cargo fmt, cargo clippy --workspace --all-targets -- -D warnings, and
cargo test --workspace are all clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`check_r6_patt_vs_ops` was implemented in `pat::ops_wf` and
`pub use`-exported from `pat::mod` but never actually invoked by
`add_parsed_pattern`. As a result, R6 was silently unenforced: an
`.rpl` file declaring `sync[$T: type] = { ... }` and then
referencing `$T` inside a `patt {}` body (where it would be an
undeclared meta-var at the pattern level) would slip through
well-formedness checking and either produce surprising matches or
crash later during lowering.
Wired in alongside the existing R1–R3 check, after `add_ops_block`
runs for every ops block. R6 inspects the raw parse tree (it
walks `MirBody` for `TypeMetaVariable` references) so it must
run before `add_pattern_item` lowers the patt block.
Iteration variable in the surrounding ops loop is now borrowed
(`for ops_block in &ops`) so `ops` remains available for the R6
call below.
Bug surfaced by Copilot's review of PR RPL-Toolchain#116 (review thread on
`crates/rpl_context/src/context.rs` line 206).
cargo build --workspace, cargo fmt --check, cargo clippy
--workspace --all-targets -- -D warnings, cargo test --workspace
all clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The three-layer substitution model is:
1. `type = ["$1", "$2", ...]` declares EXISTENTIAL numbered
placeholders (wildcards at match time after `<...>` stripping).
2. Each TYPE meta-var binding (T, U, L, G, List, ...) is a string
that may reference `$1`, `$2`, ... — these define the
CONCRETE path each abstract type meta-var resolves to.
3. Each OP-NAME binding (lock, unlock, log, ...) is a string that
may reference any TYPE meta-var (`$T`, `$L`, ...) AND the
existential placeholders. The resolver runs a fixed-point
textual substitution in `resolve_one`: `$<type-meta-var>` is
expanded before the matcher sees the resolved string.
The implementation has supported this since the resolver was added
(see `happy_path_eager_expansion` test in
`crates/rpl_context/tests/ops_resolution.rs`), but `rpl.toml`
spelled out the full `std::sync::Mutex<$1>::lock` form for every
op binding instead of using the shorter `$T::lock` form. The
short form expands to byte-identical resolved paths and is the
syntax the design intended.
Updated entries:
- sync, sync_pb[0..2], sync_so, sync_2g[0..1]:
lock = "$T::lock" / "$T::read"
unlock = "$U::drop" (where applicable)
- sync_cve: lock = "$L::lock"
- sync_pb[2] (the intentionally-malformed instance for C3):
lock = "$T::read"
Op bindings to free functions (sync_so/sync_cve unlock = black_box;
logger_2g log = black_box; intrusive_list_cve transfer/drain =
crate::* free fns) remain literal — they're not methods on a type
meta-var so the `$T::method` form does not apply. The expanded
file header documents the three layers so future readers don't
have to reverse-engineer the model.
cargo build --workspace, cargo fmt --check, cargo clippy
--workspace --all-targets -- -D warnings, cargo test --workspace
all clean. The 207-test compile-test.rs UI suite passes without
change, confirming the resolved paths are byte-identical post-
substitution.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`rust-analyzer` was added to the toolchain components list but is IDE-only tooling — it isn't required by `cargo build`, `cargo test`, `cargo clippy`, or `cargo fmt` runs on CI, and users who want rust-analyzer can install it independently via rustup. Surfaced by review comment from @TheVeryDarkness on rust-toolchain:3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tion tests
Eight occurrences of identical 5-line `Box::leak` boilerplate were
sprinkled across the new ops integration tests:
crates/rpl_context/tests/ops_lowering.rs (3 sites)
crates/rpl_context/tests/ops_wf.rs (2 sites)
crates/rpl_context/tests/ops_resolution.rs (1 site)
crates/rpl_context/tests/referenced_op_groups.rs (1 site)
crates/rpl_resolve/tests/ops_resolve.rs (3 sites)
Each one leaked a fresh `Arena<'static>`, a fresh path/content
vector, and a fresh `MetaContext<'static>` — all so
`PatternCtxt::entered_no_tcx` could see them as `'static`. The
arena allocation was wasted: each test could just as well share one.
Consolidate the boilerplate into per-crate `tests/common/mod.rs`
modules exposing:
* `shared_arena()` — `LazyLock`-backed `&'static Arena<'static>`,
initialised once per integration-test binary and shared across
every `#[test]` in that binary.
* `make_static_mctx(filename, src)` — parse `src` and return a
`&'static MetaContext<'static>`, panicking on any
parse/collect error. Replaces 7 of the 8 call sites.
* `make_static_mctx_with(filename, src, handler)` — same with a
caller-supplied error handler. Used by the R6 fixture in
`ops_resolve.rs` that intentionally feeds undeclared
pattern-level meta-vars and needs to swallow the
`NonLocalMetaVariableNotDeclared` error from
`parse_and_collect` so the R6 check itself can run.
The two `tests/common/mod.rs` files are near-identical clones (the
two crates are independent integration-test binaries and Cargo
doesn't support sharing a test sub-module across crate boundaries
without a dedicated dev-dep crate, which would be heavier than the
~50 duplicated LOC).
All 34 ops integration tests still pass:
- referenced_op_groups: 3 ok
- ops_lowering: 3 ok
- ops_wf: 13 ok
- ops_resolution: 5 ok
- ops_resolve: 10 ok
cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings,
cargo test --workspace all clean.
Surfaced by review comment from @TheVeryDarkness on
crates/rpl_context/tests/ops_lowering.rs:47.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
c783ea7 to
2f10724
Compare
Summary
Adds a new
ops { ... }block to.rplpattern files for declaring abstract,parameterised op groups, plus
[[ops.<group>]]tables inrpl.tomlforsupplying concrete instances. At match time, the driver iterates the
cartesian product over each pattern's referenced groups, threading one
ResolvedOpBindingsassignment per combination through the matcher.This lets a single pattern model a primitive (e.g. lock/unlock, intrusive
list transfer) across many concrete implementations (
std::sync::Mutex,parking_lot::Mutex,RwLock, …) without duplicating the pattern body.Worked example
The driver iterates one combination per
[[ops.sync]]instance and triesto match the pattern body against the user crate's MIR with
$sync::$lockresolved to each binding in turn.
Design
rpl_parseropsBlock,opsItem,OpFnDecl,OpRefrpl_contextOpsBlock,OpGroup,OpSignature,Operand::OpRefrpl_contextrpl_context$varexpansionrpl_configRawOpInstancedeserialiser,RplConfig.opsrpl_match+rpl_driverreferenced_op_groups;match_op_refresolvesOperand::OpRef → DefIdvia the expanded path stringTests
Unit / integration (run by
cargo test --workspace):crates/rpl_config/tests/ops_loading.rs— TOML deserialisationcrates/rpl_context/tests/ops_lowering.rs— parse-tree → AST loweringcrates/rpl_context/tests/ops_wf.rs— R1–R6 well-formedness checkscrates/rpl_context/tests/ops_resolution.rs— C2/C4/C7 resolver pathscrates/rpl_context/tests/referenced_op_groups.rs— use-site OpRef collectioncrates/rpl_resolve/tests/ops_resolve.rs— end-to-end resolver flowcrates/rpl_driver/tests/cartesian.rs— cartesian-product helperEnd-to-end UI fixtures under
tests/features/ops/:lock_unlock— happy path (one group, one instance, one diagnostic)folded— zero instances → fold to zero combos → no diagnosticspartial_bad— one malformed instance is skipped; remaining instances fireset_op_with_ops— composition with set-op subtraction (p_lock - p_uncovered)two_groups— cartesian over two groups (Mutex/RwLock ×black_box)CVE-2025-68260 POC (
tests/features/ops_cve_2025_68260/) — realintrusive-list-under-lock pattern modelled after the Linux kernel Rust
Node::releaserace; includes abuggy.rsthat should fire and afixed.rsthat should not.What changed since CI last passed-or-failed
The previous CI run on this branch (25031080228)
failed in 30–44 s at
cargo fmt --check, blocking clippy and tests fromrunning. Subsequent fix-up commits address that, the substantive issues
surfaced by a careful review, the CI breakage exposed by the new
rpl.toml, and two correctness findings from Copilot's code review:style(ops): apply cargo fmt— trailing commas on match arms,multi-line struct literals, import merging per
rustfmt.toml.**
fix(context): replace UB \&PatternItem` -> `*mut` cast withOnceCell
** —Pattern::check_and_populate_op_refsmutated arena-shared items via#[allow(invalid_reference_casting)]and a raw-pointer cast. Writing through&Tis UB per the Rust reference regardless of aliasing. Replaced withOnceCell<FxHashSet>so the field admits write-once interior mutability via a shared reference; bothpatt_block(owned) andutil_block(shared) iterations now use the same code path with nounsafe`.refactor(context): use FxHashMap for deterministic ops ordering; drop empty test—OpsConfig::instancesandResolvedOpBindings::by_groupmoved fromstd::collections::HashMapto
FxHashMapso diagnostic ordering and cartesian iteration arestable across runs.
test(ops): align black_box paths, uniform -Zinline-mir, drop broken stderr—set_op_with_ops.rsandtwo_groups.rscalledstd::hint::black_boxbutrpl.tomlbound the matching op tostd::intrinsics::black_box; the two paths resolve to differentDefIds at MIR level. Switched to the intrinsic directly.//@compile-flags: -Zinline-mir=falseapplied uniformly to all opstests so they're stable against the toolchain's inlining-policy.
internal_features,dead_code,unused_must_usein theCVE fixtures so the diagnostic isn't drowned by churning rustc
warnings.
.stderrfixtures (rawcargo runoutput, literal linenumbers, not consumed by
compile-test.rs) are deleted.rpl.toml'slogger_2gcomment corrected to "1 instance".fix(ops): silence clippy lints— eight-D warningsviolations:collapsible_match×2,needless_borrow,useless_lifetime×2,useless_conversion×3.style(ops): re-fmt after if-let collapse— small format fixupthe previous push missed.
fix(config): treat empty resolved pattern paths as no-RPL_PATS—CI's
Test installed rplstep (cargo install --path . && cargo rpl --workspace --all-targets --verbose) was failing withrpl_meta::cli E100: Cannot locate RPL pattern file "". Root cause:resolve_patternshas a vacuous-true branch onselected_groups.iter().all(is_remote_spec)whenselected_groupsis empty (identity element of conjunction). With an
rpl.tomlthatcontains no
[patterns]table — exactly the shape this PR adds — thebranch is taken,
resolve_remote_selection(&[])returnsOk(ResolvedPatterns { paths: vec![] }), and the empty paths getjoined into
""and propagated asRPL_PATS=""to the rpl-driverchild process. Fixed defensively at the env-string boundary: map
empty paths to
Noneso the driver falls back to the built-inpattern set.
fix(driver): respect outer pat_op bindings in nested RustItems cartesian— Copilot review thread on lines 568/758. When aPatternOperation(set-op composition likep = p_a - p_b)pre-computed a combo over the union of referenced groups and
threaded it down to
impl_matched_pat_item/fn_matched_pat_item,the
RustItemsarm of those functions discarded the outer comboand started a fresh local cartesian over the sub-item's own
referenced_op_groups(). In a composition where the two armsreference overlapping or disjoint subsets of the union, that
silently picked mismatched combos across positive/negative arms.
The fix splits the two paths cleanly: descend with the outer
bindings when non-empty; iterate the local cartesian only when
bindingsis empty (top-level direct entry).fix(context): wire R6 (ops meta-vars must not leak into patt bodies)— Copilot review thread oncontext.rs:206. R6 wasimplemented as
check_r6_patt_vs_opsandpub use-exported, butadd_parsed_patternnever called it; the rule was silentlyunenforced. Wired alongside R1–R3 against the raw parse tree.
Local verification before pushing each commit:
cargo fmt --all -- --checkcleancargo clippy --workspace --all-targets -- -D warningscleancargo test --workspace— 0 failures across all suites (including the207-test
compile-test.rsUI run)CI: 25688906756
already passed both ubuntu and windows on the pre-Copilot HEAD; the
two new fix commits will trigger a fresh run.
Deferred work (follow-up issues to file post-merge)
These were surfaced during review (mine + Copilot's) but each deserves
its own focused PR.
ResolveDiagnostics as real rustc diagnostics. C1–C7errors from
resolve_ops_configare currently dropped in the driver(
let (ops, _ops_diags) = …). Users get "didn't fire" instead of"missing binding for
unlock".typeis the existential free-placeholder list, so an op namedtypesilently loses its binding. Either reject op nametypeinR2 or rename the TOML key.
0..16magic iteration cap inresolve_onewith topological-sort-first detection — current codeexponentially grows strings before declaring a cycle for inputs like
T = "Mutex<$U>",U = "Vec<$T>".strip_generics_and_splitinrpl_match/src/statement.rsdoesn't handle UFCS<T as Trait>::method. Document the limitation or parse the pathwith the real parser.
tests/features/ops/**andtests/features/ops_cve_2025_68260/**intests/compile-test.rsandbless
.stderrfiles with the project-standardLL |line-numberplaceholder (via
cargo uibless).// TODO(task-6): replace DUMMY_SPmarkers in
pat/mod.rsmean ops diagnostics point atDUMMY_SPrather than the user's
.rplsource line.resolve_ops_configis currently called once per RPL pattern per fnbody; hoist it out of the per-fn loop so the TOML is parsed and
resolved once per crate analysis run.
ResolvedOpBindings::from_comboshares instance ownership(raised by Copilot).
from_combocurrently clones fullResolvedOpInstancevalues (containingBTreeMap<Symbol, String>)into the per-attempt map. Switching to
Rc<ResolvedOpInstance>(orArcif matching ever becomes parallel) and usingFxHashMap<Symbol, Rc<ResolvedOpInstance>>would reduceper-iteration cost.
ResolveDiagnosticvariantsUnknownGroup(C1),MissingBindingon op-binding (C3 path),
UndeclaredPlaceholder(C5),ParseFailure(C6).patterns::resolve_patternsvacuous-allbranch. Commit 7fixes the symptom at the env-string boundary; the upstream branch in
resolve_patterns(vacuous-trueallon emptyselected_groups)remains and would benefit from a guard that requires non-empty
selected_groupsbefore taking the remote-selection path.Notes for reviewers
crates/rpl_parser/src/parser.rsis auto-generated fromRPL.pest(
build.rs); only the +37.pestlines and any hand-written sectionsare worth reviewing.
feat(layer):/fix(layer):/test(ops):/style(ops):messages aid bisecting..rplfiles — theops {}block isadditive.
opsdeliberately remains usable as a non-block identifier(e.g. in path segments like
core::ops::Range) — reserving itglobally would break the
cve_2020_35892_35893parser test and everypattern that references
core::ops::*.— Generated by Claude Opus 4.7 (1M context) on behalf of @jellllly420