Skip to content

feat(macros): match_arg / choices / several_ok on impl-block methods (#153)#211

Open
CGMossa wants to merge 1 commit intomainfrom
fix/issue-153-match-arg-impl-methods
Open

feat(macros): match_arg / choices / several_ok on impl-block methods (#153)#211
CGMossa wants to merge 1 commit intomainfrom
fix/issue-153-match-arg-impl-methods

Conversation

@CGMossa
Copy link
Copy Markdown
Collaborator

@CGMossa CGMossa commented Apr 17, 2026

Closes #153.

Extends the match.arg validation 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 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 { ... }

}
```

Codegen path

  • MethodAttrs gains per_param_match_arg, per_param_several_ok, per_param_choices, match_arg_span. parse_method_attrs parses the four new variants. from_impl_item validates that every named parameter exists (catches typos).
  • MethodContext::match_arg_prelude() emits the per-param match.arg lines. 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's MatchArg::CHOICES.
  • Every class-system generator (r6, env, s3, s4, s7, vctrs) injects ctx.match_arg_prelude() after precondition checks.
  • generate_method_c_wrapper forwards match_arg_several_ok param names to CWrapperContext/RustConversionBuildermatch_arg_vec_from_sexp::<Inner> for Vec decoding.
  • generate_method_match_arg_helpers emits the per-param C helper extern fn plus MX_CALL_DEFS + MX_MATCH_ARG_CHOICES linkme registrations.

Follow-ups (filed as issues per CLAUDE.md rule)

Also in this PR (not issue-153)

  • CLAUDE.md rule requiring concessions / deferred items become GitHub issues referenced from the PR body.
  • CLAUDE.md note + just dev-tools-install recipe for cargo-limit (provides cargo lcheck/lclippy/ltest/lbuild for iterative dev).

Test plan

  • cargo lcheck -p miniextendr-macros / just clippy / just fmt
  • cargo clippy --workspace --all-targets --locked -- -D warnings (CI clippy_default)
  • cargo clippy --workspace --all-targets --locked --features rayon,... (CI clippy_all)
  • just lint clean
  • just 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 (regenerates rpkg/inst/vendor.tar.xz)

Generated with Claude Code

…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support match_arg, choices, and several_ok on impl block methods

1 participant