Minimal trampoline explorations — what archmage's macro stack earns
- Dominant language
- Rust
- Stars
- 12
- Forks
- 2
- Avg merge
- 13h 55m
- Merged PRs (30d)
- 28
Description
A design-exploration doc produced while analyzing archmage's macro stack
(~6,200 LoC) against third-party alternatives (multiversion 0.8, pulp 0.22,
fearless_simd 0.4, target-feature-dispatch 3.1, safe_arch 1.0).
Not a proposal to rewrite archmage — a sketch of what a ~520-LoC
forbid(unsafe)-compatible dispatch macro crate would look like, and a ranking
of archmage's features by how much you'd lose by removing each one.
## Actionable proposal
- **Suffix inference on `#[rite]`** — already tracked in #26. The exploration's
`#[cpu_tier]` primitive (iterations 8–9) is the same idea.
## Feature-removal pain ranking
Framing: if you were trying to strip archmage down toward the ~520-LoC
minimum, which features would you lose last? Higher in the list = more
load-bearing.
### Most painful to remove
1. **Tokens-as-proof type system.** The entire safety model is built on
zero-sized proof types. Every downstream crate's API takes tokens.
Removing tokens breaks: compile-time feature tracking across call graphs,
trait-bounded SIMD helpers (`fn f(t: T, ...)`), safe
wrapper-free API boundaries. The minimal version's constant-CPUID
assumption replaces type-level tracking with dispatch-site proof — which
works, but you lose the ability to pass "I have AVX-512" through multiple
function boundaries without re-checking.
2. **`#[magetypes]` text substitution.** Write one `Token`/`f32xN`/`LANES`
body, get per-tier implementations. Without it, every hot SIMD kernel
triples or quadruples in size (one hand-written version per tier), or
you reinvent the substitution via `macro_rules!` — which works for
simple cases but not for the token-aware parts.
3. **xtask codegen.** Generates tokens, macro registry, SIMD types,
cross-arch parity tests, intrinsics reference docs, and the browser JSON
from `token-registry.toml` (~1000 lines of TOML → ~5000+ lines of Rust).
Replaceable with const arrays for the feature lists, but you lose
auto-generated parity tests (which have caught real codegen divergence)
and the intrinsics browser.
### Moderately painful to remove
4. **`incant!` body rewriting.** Turns `incant!(helper(x))` inside a tier
function into a direct call to the matching-suffix variant. Replaceable
with manual `helper_v3(x)` calls from `process_v3` — ergonomic regression,
not a capability loss. Matters most in code where a single hot function
calls many tier-dispatched helpers.
5. **`#[autoversion]` scalar auto-vectorization.** Write a scalar body, get
all tiers compiled with matching `#[target_feature]`. Partially
replaceable with `#[magetypes]` on a scalar body, or manual copies.
Most of the win is when LLVM's auto-vectorizer actually does the work —
a minority of kernels.
### Least painful to remove
6. **`import_intrinsics` auto-import.** Brings `_mm256_loadu_ps` etc. into
scope inside `#[arcane]`/`#[rite]`. Replaceable by manual
`use archmage::intrinsics::x86_64::*;` at the top of each function.
DX regression, zero capability loss.
7. **`#[rite]` as a separate macro from `#[arcane]`.** Both apply
`#[target_feature]`; `#[arcane]` adds a safe wrapper, `#[rite]` doesn't.
Could collapse into one macro with a mode argument. Collapsing would
churn every call site but not lose capability.
## Comparison table
| | Minimal (iter 9) | archmage |
|---|---|---|
| Macro LoC | ~520 | ~6,200 |
| Macros | 2 | 5+ |
| Registry | Const array (~80 lines) | TOML + xtask (~3,500 lines) |
| Token-as-proof type system | No | Yes |
| Body-level `incant!` rewriting | No | Yes |
| `#[magetypes]` text substitution | No | Yes |
| `#[autoversion]` auto-vectorization | No | Yes |
| Cross-tier dispatch inside a tier | User-managed | Macro-rewritten |
| Auto-generated parity tests | No | Yes |
| Intrinsics browser / reference docs | No | Yes |
## Footnote: invariants archmage already satisfies
The exploration surfaced three invariants that archmage already gets right.
Listed for documentation, not as work items:
- **`features == detect`.** The feature list passed to `#[target_feature(enable)]`
must exactly equal the list checked by `is_*_feature_detected!()`. Otherwise
a VM that masks CPUID can expose AVX2 while hiding LZCNT, and an
`lzcnt`-emitting inner traps. `token-registry.toml` has a single `features`
array per token — no drift possible.
- **Constant-CPUID axiom.** On tier-1 Rust targets, CPU features don't change
during process lifetime. Collapses all closure-escape/async-future/
fn-pointer soundness concerns to zero.
- **`unsafe fn` vs `unsafe { }` for `forbid(unsafe_code)`.** The sibling
`#[target_feature]` inner generated by `#[arcane]` must be a safe `fn`
(Rust 2024), never `unsafe fn` — the latter breaks `#![forbid(unsafe_code)]`
even when emitted by a proc-macro. multiversion 0.8 gets this wrong;
archmage gets it right.
## Full exploration (613 lines)
Full iteration-by-iteration document
# Minimal Trampoline Explorations
Design iterations exploring how small a `#[target_feature]` dispatch macro can be
while remaining sound and `#![forbid(unsafe_code)]`-compatible. Each iteration
builds on lessons from the previous one.
Produced during a design analysis session comparing archmage's macro stack (~6,200
lines) against the theoretical minimum. Third-party crates surveyed: multiversion
0.8, pulp 0.22, fearless_simd 0.4, target-feature-dispatch 3.1, safe_arch 1.0.
## Table of Contents
1. [Iteration 1: Bare wrapper (not a trampoline)](#iteration-1-bare-wrapper)
2. [Iteration 2: Single-tier trampoline with CPUID](#iteration-2-single-tier-trampoline)
3. [Iteration 3: Attribute partitioning and rejection](#iteration-3-attribute-partitioning)
4. [Iteration 4: Constant-CPUID assumption (reject list drops to zero)](#iteration-4-constant-cpuid)
5. [Iteration 5: Rich signatures](#iteration-5-rich-signatures)
6. [Iteration 6: Multi-tier multi-arch dispatch](#iteration-6-multi-tier)
7. [Iteration 7: Compile-time elision](#iteration-7-compile-time-elision)
8. [Iteration 8: Suffix-mangle convention](#iteration-8-suffix-mangle)
9. [Iteration 9: Unified two-macro design](#iteration-9-unified-design)
10. [Key findings](#key-findings)
---
## Iteration 1: Bare wrapper
**Problem:** Not a real trampoline. Just wraps an `unsafe { inner() }` call.
No CPUID check — relies on the caller to have already proved features exist
(e.g., via a token parameter).
**~30 lines.** Does NOT perform any CPU detection.
```rust
#[proc_macro_attribute]
pub fn trampoline(attr: TokenStream, item: TokenStream) -> TokenStream {
let features = parse_macro_input!(attr as LitStr); // "avx2,fma"
let f = parse_macro_input!(item as ItemFn);
let (vis, sig, body) = (&f.vis, &f.sig, &f.block);
let inner = format_ident!("__trampoline_{}", sig.ident);
let args = sig.inputs.iter().map(|a| match a {
FnArg::Typed(PatType { pat: box Pat::Ident(p), .. }) => Ok(&p.ident),
_ => Err(syn::Error::new_spanned(a, "trampoline: only `name: T` params")),
}).collect::, _>>();
let args = match args { Ok(v) => v, Err(e) => return e.to_compile_error().into() };
let mut inner_sig = sig.clone();
inner_sig.ident = inner.clone();
quote!(
#[target_feature(enable = #features)]
#[inline]
fn #inner_sig #body
#[inline(always)]
#vis #sig {
// SAFETY: caller asserts CPU supports `#features`.
unsafe { #inner(#(#args),*) }
}
).into()
}
```
**Verdict:** Not useful alone. The caller must write `unsafe { }` to call the
wrapper (since the wrapper has no `#[target_feature]` but calls one that does),
which defeats `#![forbid(unsafe_code)]`. Shown only as a baseline.
---
## Iteration 2: Single-tier trampoline
**Problem:** Add the actual CPUID check — cached in an `AtomicU8`, with a
scalar fallback. This is the first "real" trampoline.
**~60 lines.**
```rust
#[proc_macro_attribute]
pub fn trampoline(attr: TokenStream, item: TokenStream) -> TokenStream {
// attr: features = "avx2,fma", fallback = path::to::scalar_impl
let TrampolineArgs { features, fallback } = parse_macro_input!(attr);
let f = parse_macro_input!(item as ItemFn);
let (vis, sig, body) = (&f.vis, &f.sig, &f.block);
let inner = format_ident!("__tramp_{}", sig.ident);
let args = collect_ident_params(sig)?;
// One is_x86_feature_detected! per feature, ANDed
let checks = features.split(',').map(|f| {
let f = f.trim();
quote!(std::is_x86_feature_detected!(#f))
});
let mut inner_sig = sig.clone();
inner_sig.ident = inner.clone();
quote!(
#[target_feature(enable = #features)]
#[inline]
fn #inner_sig #body
#vis #sig {
use core::sync::atomic::{AtomicU8, Ordering};
static CACHE: AtomicU8 = AtomicU8::new(0); // 0=unknown 1=no 2=yes
let supported = match CACHE.load(Ordering::Relaxed) {
2 => true,
1 => false,
_ => {
let v = #(#checks)&&*;
CACHE.store(if v { 2 } else { 1 }, Ordering::Relaxed);
v
}
};
if supported {
// SAFETY: CPUID for these features just succeeded above.
unsafe { #inner(#(#args),*) }
} else {
#fallback(#(#args),*)
}
}
).into()
}
```
**Audit surface:** One `unsafe { }` block with the CPUID check 5 lines above it.
**forbid(unsafe_code):** Compatible — the inner is a safe `fn` (Rust 2024
edition), the `unsafe { }` block is proc-macro-generated.
---
## Iteration 3: Attribute partitioning
**Problem:** What attributes on the user's function should go where? What
function signature forms should be rejected?
### Attribute routing
| Attribute | Inner | Wrapper | Reason |
|---|---|---|---|
| `#[target_feature]` (user's) | Reject | Reject | Macro owns this |
| `#[inline]` / `#[inline(always)]` | Strip (macro sets its own) | Pass through | User's hint applies to the public symbol |
| `#[no_mangle]`, `#[export_name]` | Strip | Keep | Symbol-table attrs — link error if both have them |
| `#[allow/warn/deny/forbid/expect]` | Keep | Keep | Lint scopes match the body |
| `#[track_caller]` | Keep | Keep | Panic locations |
| `#[cold]` | Strip | Keep | Trampoline branch is cold; inner is hot |
| `#[must_use]`, `#[deprecated]`, `#[doc]` | Strip | Keep | Public API metadata |
| `extern "ABI"` | Keep | Keep | ABI is part of the call type |
| Visibility | Private | User's vis | Inner is hidden |
### Rejection list (later revised)
| Modifier | Action | Why |
|---|---|---|
| `async fn` | Reject | Future's `poll` can run in non-feature context |
| `const fn` | Accept | CTFE doesn't run on the host CPU |
| `#[naked]` | Defer to rustc | E0658 already |
| Returning `impl Fn*` / `Box` / `fn(...)` | Reject | Closure-escape launders feature requirement |
| User `#[target_feature]` | Reject | Macro manages this |
**~90 lines** total (30 base + 30 partitioning + 30 rejection).
---
## Iteration 4: Constant-CPUID assumption
**Key insight:** CPU features do not change during process lifetime on tier-1
Rust targets. Once CPUID returns "avx2 available," it's available for the
entire process — including closures, futures, fn pointers, struct fields,
thread-spawned work, and anything else constructed inside a trampolined call.
**Consequence:** The entire rejection list from iteration 3 drops to zero.
- `async fn`: Sound — future constructed inside trampolined call, polled on same CPU.
- `impl Fn` / `Box` return: Sound — closure inherits features, CPU still has them.
- `fn` pointer coercion: Sound — pointer callable from anywhere in same process.
- Thread spawn: Sound — same CPU, same features.
All soundness concerns were about feature availability changing between
construction and use. Under constant-CPUID, it doesn't.
The reject list becomes:
- `#[naked]` — rustc rejects already (E0658)
- User `#[target_feature]` — reject for clarity, not soundness
**Enforcement for forbid(unsafe) users:** Under `#![forbid(unsafe_code)]`,
the user physically cannot bypass the trampoline — calling the inner directly
requires an `unsafe { }` block the lint forbids. This mechanically enforces
"all SIMD entry goes through a trampoline" without relying on user discipline.
**~30 lines** (same as iteration 2 — the reject logic is removed, not added).
---
## Iteration 5: Rich signatures
**Problem:** Support generics, self receivers, pattern parameters.
### Generic type/const forwarding (+20 lines)
Turbofish forwarding so `__inner::(args)` resolves:
```rust
fn build_turbofish(generics: &syn::Generics) -> TokenStream {
let params: Vec<_> = generics.params.iter().filter_map(|p| match p {
GenericParam::Type(tp) => Some(&tp.ident),
GenericParam::Const(cp) => Some(&cp.ident),
GenericParam::Lifetime(_) => None,
}).map(|i| quote!(#i)).collect();
if params.is_empty() { quote!() } else { quote!(::<#(#params),*>) }
}
```
### Self receivers in inherent impls (+25 lines)
Detect `FnArg::Receiver`, emit method-style call `self.__inner(args)`.
Both the wrapper and inner live in the same impl scope, so `Self` resolves
naturally. Trait method impls are rejected (sibling expansion can't add
non-trait methods).
### Pattern parameter rebinding (+15 lines)
Wildcards (`_: T`) and destructured patterns (`(a, b): (T, U)`) are renamed
to `__tramp_arg_N` in both sigs, with `let original_pattern: T = __tramp_arg_N;`
prepended to the inner body.
### `unsafe fn` propagation (+5 lines)
```rust
let was_unsafe = sig.unsafety.is_some();
let fallback_call = if was_unsafe {
quote!(#[allow(unused_unsafe)] unsafe { #fallback(#(#args),*) })
} else {
quote!(#fallback(#(#args),*))
};
```
**Running total: ~125 lines.**
---
## Iteration 6: Multi-tier multi-arch dispatch
**Problem:** Generate dispatchers that try multiple tiers in priority order.
### Const registry (~80 lines)
One field per tier. The features string is the single source of truth for
BOTH `#[target_feature(enable = ...)]` AND runtime `is_*_feature_detected!()`.
No separate "detect" list — that's a soundness bug (see below).
```rust
pub struct Tier {
pub suffix: &'static str, // "v3", "v4", "neon"
pub features: &'static str, // comma-separated, used for BOTH target_feature AND detection
pub arch: &'static str, // "x86_64", "aarch64", "wasm32"
pub priority: u8, // higher = tried first within same arch
}
pub const TIERS: &[Tier] = &[
Tier { suffix: "v4x", arch: "x86_64", priority: 90,
features: "avx512f,avx512bw,avx512cd,avx512dq,avx512vl,avx512vbmi,avx512vbmi2,avx512vnni,vpclmulqdq,vaes,gfni,bmi1,bmi2" },
Tier { suffix: "v4", arch: "x86_64", priority: 80,
features: "avx512f,avx512bw,avx512cd,avx512dq,avx512vl" },
Tier { suffix: "v3", arch: "x86_64", priority: 50,
features: "avx2,fma,bmi1,bmi2,f16c,lzcnt,popcnt,movbe" },
Tier { suffix: "v2", arch: "x86_64", priority: 30,
features: "sse3,ssse3,sse4.1,sse4.2,popcnt,cmpxchg16b" },
Tier { suffix: "v1", arch: "x86_64", priority: 10,
features: "sse,sse2" },
Tier { suffix: "arm_v3", arch: "aarch64", priority: 80,
features: "neon,crc,rdm,dotprod,fp16,aes,sha2,sha3,fhm,fcma,i8mm,bf16" },
Tier { suffix: "arm_v2", arch: "aarch64", priority: 50,
features: "neon,crc,rdm,dotprod,fp16,aes,sha2" },
Tier { suffix: "neon", arch: "aarch64", priority: 30,
features: "neon" },
Tier { suffix: "wasm", arch: "wasm32", priority: 50,
features: "simd128" },
];
```
### Soundness rule: features == detect
**features and detection MUST be the same list.** Every feature in
`#[target_feature(enable = ...)]` must be checked by
`is_*_feature_detected!()`. If you enable 8 features but check 3, the proof
obligation doesn't cover the other 5.
Concrete failure mode: a VM masks CPUID to expose AVX2 but hide LZCNT.
`is_x86_feature_detected!("avx2")` returns true, the trampoline calls the
inner, LLVM emits `lzcnt` (because `#[target_feature]` enabled it), and the
program traps.
archmage's `token-registry.toml` gets this right. The comment at the top:
> *LLVM deduplicates redundant features in `#[target_feature]` — listing
> the full set is harmless and eliminates the class of bugs where
> "minimal" lists diverge from "cumulative" lists.*
One field. Derive both uses from it. No drift.
### Dispatch expansion
Multi-tier generates one cache + check per tier, grouped by arch:
```rust
pub fn process(data: &[f32]) -> f32 {
#[cfg(target_arch = "x86_64")] {
static C_V4: AtomicU8 = AtomicU8::new(0);
if check(&C_V4, is_x86_feature_detected!("avx512f")
&& is_x86_feature_detected!("avx512bw") && /* ... */) {
return unsafe { process_v4(data) };
}
static C_V3: AtomicU8 = AtomicU8::new(0);
if check(&C_V3, is_x86_feature_detected!("avx2")
&& is_x86_feature_detected!("fma") && /* ... */) {
return unsafe { process_v3(data) };
}
}
#[cfg(target_arch = "aarch64")] {
static C_NEON: AtomicU8 = AtomicU8::new(0);
if check(&C_NEON, is_aarch64_feature_detected!("neon")) {
return unsafe { process_neon(data) };
}
}
process_scalar(data) // unconditional, every arch, always the last line
}
```
The scalar fallback is always the last line — no cfg, no check, no unsafe.
On uncovered arches (riscv, etc.), no cfg block matches, every SIMD check
is absent, and execution falls straight through to scalar.
**Running total: ~285 lines** (125 rich-sig + 80 registry + 65 multi-tier + 15 multi-arch).
---
## Iteration 7: Compile-time elision
**Problem:** When the binary is compiled with `-Ctarget-cpu=native` or
`-Ctarget-feature=+avx2,+fma,...`, the runtime dispatch should disappear
entirely — no atomic load, no branch, direct call to the best tier.
### Mechanism
Use `#[cfg(target_feature = "...")]` to detect ambient features at compile
time. Emit one elision block per tier before the runtime dispatch:
```rust
pub fn process(data: &[f32]) -> f32 {
// v4 features in ambient target → direct call, no runtime check.
#[cfg(all(
target_feature = "avx512f", target_feature = "avx512bw",
target_feature = "avx512cd", target_feature = "avx512dq",
target_feature = "avx512vl",
))]
{ return unsafe { process_v4(data) }; }
// v3 features in ambient, but NOT v4 (mutually exclusive with above).
#[cfg(all(
all(target_feature = "avx2", target_feature = "fma", /* ... */),
not(all(target_feature = "avx512f", /* ... */)),
))]
{ return unsafe { process_v3(data) }; }
// Runtime path — only compiled when no tier is fully ambient.
#[cfg(not(any(
all(target_feature = "avx512f", /* v4 */),
all(target_feature = "avx2", /* v3 */),
)))]
{
// cached CPUID dispatch (same as iteration 6)
...
process_scalar(data)
}
}
```
Higher tiers are excluded from lower tiers' cfgs via `not(all(...))` to
ensure mutual exclusivity — exactly one block is compiled for any given build.
The runtime path gets a `#[cfg(not(any(...)))]` wrapping everything.
When compiled with `-Ctarget-cpu=haswell`, the v3 elision block fires, the
function becomes `unsafe { process_v3(data) }`, and since `process_v3`'s
features are a subset of ambient, rustc can inline the body directly. The
`AtomicU8`, `is_x86_feature_detected!` calls, and scalar fallback all disappear.
archmage uses the same pattern in its generated `summon()`:
```rust
#[cfg(all(target_feature = "avx2", target_feature = "fma", /* ... */))]
{ Some(unsafe { Self::forge_token_dangerously() }) }
#[cfg(not(all(/* ... */)))]
{ match CACHE.load(Ordering::Relaxed) { /* runtime path */ } }
```
**Added lines: ~30.** Running total: **~315 lines.**
---
## Iteration 8: Suffix-mangle convention
**Key insight:** The function name suffix (`_v3`, `_v4`, `_neon`) is the
natural lookup key for the tier registry. The macro can infer the
`#[target_feature]` set from the suffix instead of requiring it as a macro
argument.
### `#[cpu_tier]` — the primitive annotation
```rust
#[cpu_tier]
fn dot_v3(a: &[f32; 8], b: &[f32; 8]) -> f32 { /* avx2 body */ }
```
Macro splits the name at the last `_`, looks up `v3` in `TIERS`, applies
`#[target_feature(enable = "avx2,fma,...")]` + `#[inline]` + `#[cfg(target_arch = "x86_64")]`.
Implementation (~30 lines):
```rust
#[proc_macro_attribute]
pub fn cpu_tier(_attr: TokenStream, item: TokenStream) -> TokenStream {
let f = parse_macro_input!(item as ItemFn);
let name = f.sig.ident.to_string();
let (_base, suffix) = match name.rsplit_once('_') {
Some(p) => p,
None => return err(f.sig.ident.span(),
"#[cpu_tier] requires a suffixed name like `foo_v3`"),
};
let tier = match find_tier(suffix) {
Some(t) => t,
None => return err(f.sig.ident.span(),
&format!("unknown tier suffix `_{suffix}`")),
};
let features = tier.features;
let arch = tier.arch;
let attrs = &f.attrs;
let vis = &f.vis;
let sig = &f.sig;
let body = &f.block;
quote!(
#[cfg(target_arch = #arch)]
#(#attrs)*
#[target_feature(enable = #features)]
#[inline]
#vis #sig #body
).into()
}
```
### Dispatch with suffix list
```rust
#[dispatch(self, _v3, _neon)]
fn compute_v4(data: &[f32]) -> f32 { /* avx-512 body */ }
#[cpu_tier]
fn compute_v3(data: &[f32]) -> f32 { /* avx2 body */ }
#[cpu_tier]
fn compute_neon(data: &[f32]) -> f32 { /* neon body */ }
fn compute_scalar(data: &[f32]) -> f32 { data.iter().sum() }
// User calls: compute(&data)
```
The `#[dispatch]` attribute on `compute_v4`:
1. Promotes `compute_v4` itself to `#[cpu_tier]` (applies target_feature from suffix).
2. Generates a dispatcher `compute` (base name, suffix stripped).
3. The dispatch list `(self, _v3, _neon)` specifies priority order.
4. The macro reads each suffix's arch from TIERS, groups by arch, emits cfg blocks.
5. `compute_scalar` is the unconditional fallback at the bottom.
---
## Iteration 9: Unified two-macro design
**Key insight:** The presence or absence of a token parameter determines the
dispatch mode. Two macros cover all use cases:
### `#[cpu_tier]` — raw annotation
Apply `#[target_feature]` from suffix. No wrapper, no dispatch. The caller
must be in a matching-feature context.
```rust
#[cpu_tier]
fn helper_v3(chunk: &mut [f32; 8]) { /* inner helper */ }
```
### `#[dispatch]` — two modes from one macro
**Mode A: No token parameter → CPUID dispatcher with scalar fallback.**
```rust
#[dispatch(self, _v3, _v2)]
fn compute_v4(data: &[f32]) -> f32 { /* avx-512 body */ }
```
The function IS the top-tier implementation. The macro generates a dispatcher
`compute()` that does cached CPUID, tries tiers in order, falls through to
`compute_scalar()`. The scalar fallback is **always required** in this mode —
without a token, the caller hasn't proved anything, so the function must
handle "CPU supports nothing."
**Mode B: Token parameter → safe wrapper (no CPUID, no fallback).**
```rust
#[dispatch]
fn compute_v4(token: X64V4Token, data: &[f32]) -> f32 { /* avx-512 body */ }
```
The token proves features exist. No CPUID check. No scalar fallback. The macro
generates a safe wrapper that calls the `#[target_feature]` inner via proc-macro
`unsafe { }`. This is archmage's `#[arcane]` under the unified name.
### Mode switch rule
| | No token parameter | Has token parameter |
|---|---|---|
| What it generates | CPUID dispatcher + `_scalar` fallback | Safe wrapper (token = proof) |
| Scalar fallback | **Required** | Not needed |
| CPUID check | Yes (cached) | No |
| Equivalent archmage macro | `incant!` + dispatch | `#[arcane]` |
### Summary
| Macro | Lines | Role |
|---|---|---|
| `#[cpu_tier]` | ~30 | Apply `#[target_feature]` from suffix |
| `#[dispatch]` (tokenless, with tier list) | ~120 | Runtime CPUID dispatcher |
| `#[dispatch]` (with token) | ~50 | Safe wrapper (arcane-equivalent) |
| `registry.rs` (const TIERS) | ~80 | Feature/arch/suffix lookup |
| Parse helpers | ~40 | Shared syn utilities |
| Safe intrinsics re-exports | ~200 | Memory-op wrappers for forbid(unsafe) |
| **Total** | **~520** | |
---
## Key findings
### Soundness
- **Constant-CPUID assumption** (tier-1 Rust targets): CPU features don't
change during process lifetime. This collapses all closure-escape,
async-future, fn-pointer-laundering concerns. The proof at the dispatch
site covers every later use of any value constructed inside the dispatched
call.
- **features == detect**: The feature list used for `#[target_feature(enable)]`
and the feature list checked by `is_*_feature_detected!()` must be identical.
Checking only "headline" features (e.g., `avx2` for the v3 bundle) while
enabling more (e.g., `lzcnt`, `bmi2`) is a soundness bug. archmage's
`token-registry.toml` gets this right with a single `features` array per token.
- **`unsafe fn` vs `unsafe { }`**: `#![forbid(unsafe_code)]` rejects `unsafe fn`
declarations from proc macros but allows `unsafe { }` blocks from proc macros.
The trampoline inner must be a safe `fn` with `#[target_feature]` (Rust 2024
edition), never `unsafe fn`. multiversion 0.8 generates `unsafe fn` inners
and therefore breaks forbid(unsafe).
### Architecture
- **forbid(unsafe_code) is the spec, not a feature.** The library must enable
forbid for users who want it. It must not require it. Macros emit
forbid-compatible code; users who don't set forbid can write their own
`unsafe { }` blocks to bypass the macros when useful.
- **Token parameter presence/absence** cleanly switches between "I am the
runtime dispatch entry (CPUID + scalar fallback required)" and "the caller
already proved features (no CPUID, no fallback)."
- **The scalar fallback** is always the unconditional last line of the
dispatcher. No cfg. No check. No unsafe. It's just `compute_scalar(data)`.
On uncovered arches, no SIMD cfg block matches and execution falls through.
### Comparison with archmage
| | Minimal (iteration 9) | archmage |
|---|---|---|
| Macro LoC | ~520 | ~6,200 |
| Macros | 2 (`#[cpu_tier]`, `#[dispatch]`) | 5+ (`#[arcane]`, `#[rite]`, `incant!`, `#[magetypes]`, `#[autoversion]`) |
| Registry | Const array (~80 lines) | TOML + xtask codegen (~3,500 lines) |
| Body inspection | None | `rewrite.rs` walks token trees for `incant!` rewriting |
| Token hoisting | Via `#[dispatch]` with token param | Via `Token::summon()` + pass-through |
| forbid(unsafe) | Yes | Yes |
| Compile-time elision | Yes | Yes (via `compiled_with()`) |
### What the minimal version does not cover
- **Token-as-proof type system.** No zero-sized proof types, no trait
hierarchy, no downcast methods. The constant-CPUID assumption replaces
type-level feature tracking.
- **Body-level `incant!` rewriting.** No body walking. Inner calls are
explicit: `helper_v3(args)` from `process_v3`, safe under Rust 1.86+
matching-feature rules.
- **`#[magetypes]` text substitution.** No `Token`/`f32xN`/`LANES` placeholders.
Write per-tier implementations manually, or use a `macro_rules!` to clone
one body across tiers.
- **`#[autoversion]` auto-vectorization.** No auto-clone of scalar code across
tiers. Add as a ~50-line extension if needed.
- **Cross-tier dispatch from inside a tier.** Archmage's `incant!` rewriter
turns `incant!(helper(x))` inside `process_v3` into a direct call to
`helper_v3(x)`. The minimal version makes this the user's responsibility.
### Upstream issue
[imazen/archmage#26](https://github.com/imazen/archmage/issues/26) — proposed
adding suffix inference to `#[rite]` as an additive, non-breaking change.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the actionable proposal and existing issue #26, then read the full exploration and compare its feature ranking with archmage's current macro stack. The issue does not define an implementation target or completion condition; any coding work would need to be separated into a concrete issue.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- compilers, developer-experience
- Issue type
- Documentation
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100