feat(macros): match_arg / choices / several_ok on impl-block methods (#153)#211
Open
feat(macros): match_arg / choices / several_ok on impl-block methods (#153)#211
Conversation
…153) Closes #153. Extends the `match.arg` validation surface from standalone `#[miniextendr]` functions to methods inside `#[miniextendr(r6|env|s3|s4|s7|vctrs)]` impl blocks. The R wrapper now emits `base::match.arg(param, CHOICES[, several.ok = TRUE])` before `.Call()` for annotated parameters, and the C wrapper routes `match_arg + several_ok` parameters through `match_arg_vec_from_sexp` so each STRSXP element is validated against the enum's `MatchArg::CHOICES`. ## Why method-level (not parameter-level) annotation Standalone functions can write `#[miniextendr(match_arg)] param: Enum` inside their parameter list because the outer `#[miniextendr]` attribute on the fn claims the whole item — Rust defers inner-attribute resolution to the macro. Inside an impl block the outer attribute is on the impl itself, so Rust parses each method's signature strictly and rejects `#[miniextendr(...)]` on parameters with "expected non-macro attribute, found attribute macro". The surface for impl methods is therefore method-level, mirroring the existing `defaults(param = "value")`: ```rust #[miniextendr(r6)] impl Counter { #[miniextendr(match_arg(mode))] pub fn new(mode: Mode) -> Self { ... } #[miniextendr(match_arg_several_ok(modes))] pub fn reset(&mut self, modes: Vec<Mode>) -> i32 { ... } #[miniextendr(choices(level = "low, medium, high"))] pub fn describe(level: String) -> String { ... } #[miniextendr(choices_several_ok(colors = "red, green, blue"))] pub fn paint(colors: Vec<String>) -> String { ... } } ``` ## What's wired - `MethodAttrs` gains `per_param_match_arg`, `per_param_several_ok`, `per_param_choices`, `match_arg_span`. `parse_method_attrs` parses the four new `#[miniextendr(...)]` variants; `from_impl_item` validates that every named parameter exists on the signature (catches typos). - `MethodContext::match_arg_prelude()` emits the per-param `match.arg` lines (factor→character normalization, C-helper lookup for match_arg, inline `match.arg(x)` for explicit `choices`). `precondition_checks()` now skips match_arg params (they're already validated). - `effective_r_defaults()` layers user defaults, then `c("a","b")` defaults from `choices(...)`, then a `.__MX_MATCH_ARG_CHOICES_*__` placeholder for `match_arg` that the cdylib's write-time pass substitutes with the enum's rendered `MatchArg::CHOICES`. - Every class-system generator (`r6`, `env`, `s3`, `s4`, `s7`, `vctrs`) injects `ctx.match_arg_prelude()` after precondition checks in its method bodies (and inside the `is.null(.ptr)` guard of R6/S7 constructors that use the factory-pattern `.ptr` shortcut). - `generate_method_c_wrapper` forwards `match_arg_several_ok` parameter names to `CWrapperContext`, which threads them into `RustConversionBuilder::with_match_arg_several_ok` — the conversion path that uses `match_arg_vec_from_sexp::<Inner>` instead of the generic `TryFromSexp`. - `generate_method_match_arg_helpers` emits the per-param `C_<type>__<method>__match_arg_choices__<param>` extern fn plus two `linkme::distributed_slice` registrations: `MX_CALL_DEFS` so R can `.Call()` the helper, and `MX_MATCH_ARG_CHOICES` so the placeholder substitution pass resolves to the enum's `CHOICES` at cdylib write time. ## Follow-ups (filed as issues) - #208 — vctrs fixture (vctrs ctors need a vector-shaped `.data`, so a struct-returning ctor can't be exercised with the same pattern used for the other class systems). - #209 — S4 fixture + validation (codegen path patched, but not yet exercised end-to-end in rpkg tests). - #210 — auto-inject `@param` docs from enum `CHOICES` (standalone fns emit `MX_MATCH_ARG_PARAM_DOCS`; impl methods don't, yet — they fall back to `MethodDocBuilder`'s generic "(no documentation available)"). Also adds two unrelated workflow tweaks per this session's feedback: - `CLAUDE.md` rule requiring that scope cuts and deferred items become GitHub issues referenced from the PR body. - `CLAUDE.md` note + `just dev-tools-install` recipe for `cargo-limit`, which provides `cargo lcheck`/`lclippy`/`ltest`/`lbuild` — limits output during iterative development so errors surface without scrolling through thousands of lines of noise. ## Test plan - [x] `cargo lcheck -p miniextendr-macros`, `just clippy`, `just fmt` - [x] `cargo clippy --workspace --all-targets --locked -- -D warnings` (CI `clippy_default`) - [x] `cargo clippy --workspace --all-targets --locked --features rayon,rand,...` (CI `clippy_all`) - [x] `just lint` clean - [x] `just devtools-test` → 4536 PASS / 0 FAIL / 15 SKIP (+1 vs pre-patch baseline from new fixture) - [x] `just vendor` (regenerates `rpkg/inst/vendor.tar.xz`) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #153.
Extends the
match.argvalidation surface from standalone#[miniextendr]functions to methods inside#[miniextendr(r6|env|s3|s4|s7|vctrs)]impl blocks.Surface
Unlike standalone fns, impl methods can't carry per-parameter
#[miniextendr(...)]attributes — Rust's parser rejects attribute macros on fn parameters inside impl items. The annotation is instead method-level, mirroring the existingdefaults(param = "value"):```rust
#[miniextendr(r6)]
impl Counter {
#[miniextendr(match_arg(mode))]
pub fn new(mode: Mode) -> Self { ... }
}
```
Codegen path
MethodAttrsgainsper_param_match_arg,per_param_several_ok,per_param_choices,match_arg_span.parse_method_attrsparses the four new variants.from_impl_itemvalidates that every named parameter exists (catches typos).MethodContext::match_arg_prelude()emits the per-parammatch.arglines.precondition_checks()skips match_arg params (already validated).effective_r_defaults()layers user defaults,c(\"a\",\"b\")for choices, and a.__MX_MATCH_ARG_CHOICES_*__placeholder for match_arg that the cdylib's write-time pass substitutes from the enum'sMatchArg::CHOICES.r6,env,s3,s4,s7,vctrs) injectsctx.match_arg_prelude()after precondition checks.generate_method_c_wrapperforwardsmatch_arg_several_okparam names toCWrapperContext/RustConversionBuilder→match_arg_vec_from_sexp::<Inner>for Vec decoding.generate_method_match_arg_helpersemits the per-param C helper extern fn plusMX_CALL_DEFS+MX_MATCH_ARG_CHOICESlinkme registrations.Follow-ups (filed as issues per CLAUDE.md rule)
.data)@paramdocs from enumCHOICES(standalone fns emitMX_MATCH_ARG_PARAM_DOCS; impl methods don't yet)Also in this PR (not issue-153)
CLAUDE.mdrule requiring concessions / deferred items become GitHub issues referenced from the PR body.CLAUDE.mdnote +just dev-tools-installrecipe forcargo-limit(providescargo lcheck/lclippy/ltest/lbuildfor iterative dev).Test plan
cargo lcheck -p miniextendr-macros/just clippy/just fmtcargo clippy --workspace --all-targets --locked -- -D warnings(CIclippy_default)cargo clippy --workspace --all-targets --locked --features rayon,...(CIclippy_all)just lintcleanjust devtools-test→ 4536 PASS / 0 FAIL / 15 SKIP (new fixture at rpkg/src/rust/match_arg_impl_tests.rs + rpkg/tests/testthat/test-match-arg-impl.R)just vendor(regeneratesrpkg/inst/vendor.tar.xz)Generated with Claude Code