Skip to content

Add param/ret/sig refinement-type annotations via thrust_macros#98

Draft
coord-e wants to merge 8 commits into
mainfrom
claude/eager-lovelace-8wLLI
Draft

Add param/ret/sig refinement-type annotations via thrust_macros#98
coord-e wants to merge 8 commits into
mainfrom
claude/eager-lovelace-8wLLI

Conversation

@coord-e
Copy link
Copy Markdown
Owner

@coord-e coord-e commented May 26, 2026

Summary

Adds thrust_macros::param, thrust_macros::ret, and thrust_macros::sig attribute macros that express refinement types (e.g. List<{ v: i32 | v > 0 }>) and lower them to the same rustc-typechecked #[thrust::formula_fn] machinery already used by requires/ensures. This gives refinement-type formulas full name resolution, model-type checking, user-defined predicates, etc. — instead of the limited hand-written DSL in src/annot.rs.

This is a step toward #70 (dropping the old annotation parser): it implements thrust::param / thrust::ret (plus sig) on top of the typechecked formula mechanism. See "Follow-up: removing the old parser" below.

Design

A refinement is placed via a type position — a path of indices that addresses into the function type uniformly:

  • first index selects a parameter (by index) or the return slot (== parameter count);
  • subsequent indices descend into generic arguments (enum type-args, Box pointee).

Each macro emits, per refinement, a sibling #[thrust::formula_fn] and injects a #[thrust::refine(i, j, …)] path statement into the body. The plugin collects these, translates each formula to a Refinement, and installs it into the parameter/return RefinedType template via a new RefinedType::install_refinement_at navigator.

  • param(name: ty) — root [param_index(name)]
  • ret(ty) — root [param_count]
  • sig(fn(n0: t0, …) -> ret) — one job per arg + the return; the direct counterpart of the legacy thrust::sig

All three share one token scanner and lower to the same #[thrust::refine(..)] attribute + navigator (no param/ret special-casing in the plugin).

Changes

  • thrust-macros/src/lib.rs: param/ret/sig proc macros + shallow type-expression scanner.
  • src/analyze/annot.rs: refine_path() + integer-position parser.
  • src/analyze.rs: collect #[thrust::refine(..)] statements and resolve them to positioned refinements (reusing FormulaFn::to_ensure_annot).
  • src/rty.rs: RefinedType::install_refinement_at navigator (Function params/ret, enum args, Box pointee).
  • src/refine/template.rs: FunctionTemplateTypeBuilder::refine entry.
  • src/analyze/local_def.rs: wiring.
  • Migrated the existing thrust::sig tests (annot_box_term) to thrust_macros::sig, plus new refine_param_simple / refine_sig pass+fail tests.

The legacy parse_rty path is left intact and untouched.

Verification

  • Full suite green: 192 UI tests (188 baseline + 4 new) + doc tests, against Z3 4.13.0 + the COAR pcsat image (matching CI).
  • cargo fmt --all -- --check and cargo clippy -- -D warnings clean.

Known limitation (pre-existing, not introduced here)

While building the type at any depth is correct, thrust's verifier reliably enforces only top-level (depth-0) parameter/return refinements today. Enum/datatype nested args are enforced soundly, but Box (own-pointer) pointee refinements in return position are not — filed as #97. The committed tests therefore stick to depth-0 cases plus the whole-box sig migration.

Follow-up: removing the old parser (#70)

After this change, nothing in the repo (src, tests, examples, injected std.rs prelude) uses the legacy thrust::param/thrust::ret/thrust::sig or raw thrust::requires(..)/thrust::ensures(..) forms — all use thrust_macros::*. The AnnotParser (parse_rty/parse_formula) is reachable only from its own code paths and can be removed in a follow-up (retaining AnnotFormula, which the formula_fn path still uses). Left out of this PR to keep it focused.

Test plan

  • cargo test (full UI + doc tests) passes with Z3 + COAR image
  • cargo fmt --all -- --check
  • cargo clippy -- -D warnings
  • param/ret/sig pass+fail tests behave correctly (refinement enforced)
  • annot_box_term migration: pass stays pass, fail stays Unsat

Refs #70.

https://claude.ai/code/session_01Km9xwaVaGQjnAy1iHsPewa


Generated by Claude Code

claude added 8 commits May 26, 2026 05:07
Introduce `thrust_macros::param`, `thrust_macros::ret`, and
`thrust_macros::sig` attribute macros that lower refinement types (e.g.
`{ v: i32 | v > 0 }`) into `#[thrust::formula_fn]`s, giving refinement
formulas the same rustc-typechecked treatment as requires/ensures. Each
refinement is placed via a new "type position" — a path addressing into
the function type (parameter index or return slot, then generic-argument
indices for enum args and Box pointees) — emitted as a
`#[thrust::refine(..)]` path statement and installed into the parameter
or return RefinedType template.

Migrate the existing `thrust::sig` tests to `thrust_macros::sig`.
Replace the bare `Vec<usize>` type position with `rty::TypePosition` (a
`TypePositionRoot` of `Param(idx)` / `Return`, plus a projection of nested
type-argument indices). The `#[thrust::refine(..)]` attribute now uses the
`result` keyword to select the return instead of the parameter count, which
was unintuitive. Adds `Display` (`$1`, `result.0`).
Replace the previous TypePositionRoot + projection: Vec<usize> split
with a flat Vec<TypePositionStep>, where each step is one of:

- Param(FunctionParamIdx): navigate into a function type's parameter
- Return: navigate into a function type's return slot
- TypeArg(usize): navigate into a generic type's type argument

This makes the path representation uniform across all type levels,
enabling future support for refinements on positions inside
higher-order function types (e.g. [$0, result] for the return type
of a function-typed first parameter).

The attribute encoding changes accordingly:
- result (ident) => Return
- integer i => Param(i)
- bracket group [i] => TypeArg(i)

The macro-side RefineRoot+projection split is similarly replaced with
a flat Vec<RefineStep> in the Refinement struct, and scan_type now
threads the full steps Vec through recursive calls.

https://claude.ai/code/session_01Km9xwaVaGQjnAy1iHsPewa
Align the macro-emitted attribute with the existing requires_path /
ensures_path convention: the _path suffix conveys that the attribute's
target is a path to a formula_fn. The symbol-path helper follows suit
(refine_path -> refinement_path_path).

https://claude.ai/code/session_01Km9xwaVaGQjnAy1iHsPewa
Move the param/ret/sig expansion logic and its token-scanning helpers
out of the crate root into a dedicated refine module, leaving only the
thin proc-macro entry points in lib.rs. Shrinks lib.rs from ~1232 to
~813 lines.

https://claude.ai/code/session_01Km9xwaVaGQjnAy1iHsPewa
Encode function parameters as $i (matching FunctionParamIdx's Display)
and type arguments as bare integers, instead of bare integers for
params and bracketed [i] for type args. Reads more naturally and keeps
the attribute syntax consistent with how parameters are displayed
elsewhere.

https://claude.ai/code/session_01Km9xwaVaGQjnAy1iHsPewa
- Rename FunctionTemplateTypeBuilder::refine to install_refinement_at,
  matching the RefinedType method it delegates to.
- Rename extract_refine_paths / extract_refine_annots to
  extract_refinement_paths / extract_refinement_annots, and clarify the
  related panic messages, to reduce the overloaded use of "refine".
- Make TypePosition's Display match the refinement_path(..) surface
  syntax (comma-separated steps) instead of dot-separated.
- Replace the macro's build_refine_jobs (and its complex tuple return
  type) with annotated_type_exprs returning PositionedTypeExpr, and
  introduce NamedType / SigAnnotation structs so parse_sig_attr no
  longer needs an allow(clippy::type_complexity). Rename RefineStep to
  PositionStep, RefineKind to AnnotationKind, and expand_refine to
  expand for clarity.

https://claude.ai/code/session_01Km9xwaVaGQjnAy1iHsPewa
- Replace the undefined phrase "refine position" in parser diagnostics
  with "type position".
- Drop the non-empty invariant from TypePosition: an empty path is a
  valid notion (it addresses the type itself); a path is only non-empty
  when applied to a function type. TypePosition::new now takes the full
  step vector, and that non-emptiness is checked where it matters (the
  function-type builder).
- Rename the macro module file thrust-macros/src/refine.rs to rty.rs and
  use TypePositionStep there, mirroring the plugin's rty module instead
  of naming the same concept differently.

https://claude.ai/code/session_01Km9xwaVaGQjnAy1iHsPewa
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.

2 participants