magetypes: technique backlog — 256-bit boolean reductions, dynamic byte swizzle, 8-bit multiply, NEON bitmask, i686 tokens
- Dominant language
- Rust
- Stars
- 12
- Forks
- 2
- Avg merge
- 13h 55m
- Merged PRs (30d)
- 28
Description
Replaces the 2026-09-04 technique-survey issue (number 81), which mixed shipped work, open work, and prose into one unranked list. This is the same backlog, re-verified against `main` on 2026-09-08 and reduced to what is actually left. Self-contained: every claim below points at a file and line in this repo.
## Status at a glance
| Item | State | Where |
|---|---|---|
| `f32`→`i32` conversion contract | **Done** — `to_i32_saturating`, uniform contract | closed as #80 |
| Safe token materialization | **Done** — `Token::from_context()`, 0.9.29 | PR #87 |
| Cross-ISA divergence docs | **Done** | `docs/site/content/magetypes/isa-quirks.md` |
| Boolean-reduction reformulation | **Half done** — 512-bit only | §1 |
| Dynamic byte swizzle | **Open** — nothing exists | §2 |
| 8-bit multiply | **Open** — nothing exists | §3 |
| NEON bitmask lowering | **Blocked** — needs ARM silicon | §4 |
| `min_ieee` / `max_ieee` naming | **Open** — never adopted | §5 |
| 32-bit x86 tokens | **Undecided** | §6 |
| Generation-time shared emission | **Undecided** | §7 |
| Macro surface soundness audit | **Open** | §8 |
Two things that are neither open nor done are recorded at the bottom: one reversal and one parked branch.
---
## §1. The boolean-reduction reformulation stopped at 512-bit
`all_true([a, b])` as `reduce(and(a, b))` beats two reductions plus a `&&`. That landed and measured **+10.2%** on the x86 512-bit polyfill (4740 → 5280 ns, interleaved).
It was applied to the **512-bit** polyfills on every backend and never to the **256-bit** ones. Exact state:
| Backend | 512-bit polyfill | 256-bit polyfill |
|---|---|---|
| `x86_v3.rs` | 8 sites, reformulated | n/a — 256-bit is native `__m256i` |
| `arm_neon.rs` | 8 sites, reformulated (`[_; 4]`) | **12 sites, still two-reduction** |
| `wasm128.rs` | 8 sites, reformulated (`[_; 4]`) | **16 sites, still two-reduction** |
The unconverted sites, `all_true` and `any_true` on each:
- `magetypes/src/simd/impls/arm_neon.rs` — `I8x32` 2870/2875, `U8x32` 3342/3347, `I16x16` 3931/3936, `U16x16` 4402/4407, `I32x8` 1630/1635, `U32x8` 2026/2031
- `magetypes/src/simd/impls/wasm128.rs` — the same six, plus `I64x4` 1973/1978 and `U64x4` 4252/4257
Current shape (`arm_neon.rs:1630`):
```rust
fn all_true(self, a: [int32x4_t; 2]) -> bool {
vminvq_u32(vreinterpretq_u32_s32(a[0])) != 0 && vminvq_u32(vreinterpretq_u32_s32(a[1])) != 0
}
```
Wanted, matching what the 512-bit path already does: `bitand` the halves, then one reduction. `any_true` is the same with `bitor`.
- [ ] Reformulate the 12 NEON sites
- [ ] Reformulate the 16 WASM sites
- [ ] Fix the generator, not the output — these files are generated
- [ ] Re-measure; the x86 number does not transfer
## §2. There is no dynamic byte swizzle
magetypes exposes no `pshufb` / `tbl`-class operation at any width. Confirmed absent: no `swizzle`, `shuffle_dyn`, `_mm_shuffle_epi8` or `vqtbl` anywhere in `magetypes/src/simd/backends/` or the generic types. This is the workhorse for palette expansion, pixel-format shuffles and byte transforms, and its absence forces callers back to scalar or to hand-written intrinsics outside the type system.
The design problem is out-of-range control bytes, where the ISAs disagree: x86 `pshufb` zeroes a lane when the control byte's high bit is set and otherwise masks to the low 4 bits; NEON `tbl` zeroes out-of-range; WASM `i8x16.swizzle` zeroes at ≥16. A single portable contract has to pick one, and the cheap answer differs per backend.
The two-tier split that other work in this space converged on is worth copying: a **relaxed** variant whose out-of-range behavior is explicitly unspecified (so every backend uses its bare instruction), and a **zeroing** variant with a uniform contract (backends that need it pay for a mask). A third, block-scoped variant handles the 256/512-bit case where the natural instruction only shuffles within 128-bit lanes.
- [ ] Decide the tier names and contracts, and write them down before implementing
- [ ] `u8xN` backend methods via xtask, all three widths
- [ ] Lane-order-pinned tests — this is exactly the shape that AVX2's per-lane behavior breaks silently
- [ ] Asm probe per backend
## §3. There is no 8-bit multiply
Neither `I8x16Backend` nor `U8x16Backend` declares a `mul`. Widening to 16-bit and back is the obvious workaround, but on x86 `PMADDUBSW` does an unsigned×signed byte multiply with horizontal pairwise add in one instruction, which is the primitive most byte-domain kernels actually want. Related: `pairwise_widen_add` already exists on the unsigned byte and halfword types, so the pairwise shape is established here.
- [ ] Decide whether the exposed op is `mul` (lane-wise, needs the widen roundtrip on x86) or the pairwise multiply-add that maps to one instruction
- [ ] Implement on `i8`/`u8` at all three widths, native where possible
- [ ] Measure against the widen-multiply-narrow baseline before claiming a win
## §4. NEON bitmask packs lanes one at a time
`magetypes/src/simd/impls/arm_neon.rs:1398` (128-bit) and `:1639` (256-bit polyfill) extract each lane with `vgetq_lane_u32` and shift-or them together:
```rust
let shift = vshrq_n_u32::<31>(vreinterpretq_u32_s32(a));
let lane0 = vgetq_lane_u32::<0>(shift);
// ... lane1, lane2, lane3
lane0 | (lane1 << 1) | (lane2 << 2) | (lane3 << 3)
```
A shift-and-accumulate formulation should beat this. **Blocked, deliberately**: house rules forbid landing a perf change on measurement from a different machine class, and there is no ARM silicon in the loop here. Do not land this from a QEMU number or from reasoning about instruction counts.
- [ ] Measure the current form on M-series or Neoverse
- [ ] Only then evaluate a replacement
## §5. `min` / `max` do not say which one they are
IEEE 754 `minNum`/`maxNum` and the x86 `MINPS`/`MAXPS` family disagree on NaN and on signed zero, and our method names do not distinguish them. Naming the IEEE-semantics variants `min_ieee` / `max_ieee` and leaving the bare names for the native op makes the choice visible at the call site. Nothing named `min_ieee` or `max_ieee` exists today.
This is related to an already-recorded open divergence: scalar `f32x16` min/max differs from the narrower scalar vectors on NaN. Fixing the naming and fixing that divergence should be one decision, not two.
- [ ] Decide the naming, including what the bare names promise
- [ ] Settle the scalar `f32x16` NaN divergence in the same pass
- [ ] Update the ISA quirks page
## §6. Every x86 token stubs out on 32-bit x86
`src/tokens/generated/mod.rs:8-17` gates the real x86 tokens on `target_arch = "x86_64"`; 32-bit x86 gets `x86_stubs`, where every `summon()` returns `None`. So on `i686-unknown-linux-gnu` — a **primary CI target** for this project — nothing dispatches above scalar, silently.
Runtime detection on 32-bit x86 is possible; the stubs are a decision, not a limitation. The question is whether it is a deliberate one.
- [ ] Confirm whether i686 SIMD is intentionally out of scope
- [ ] If intentional: say so in the docs, because "our CI covers i686" currently implies more than it delivers
- [ ] If not: the registry already has the feature sets; this is a cfg and detection-path change
## §7. Generation emits per-backend × per-width where it could share
Operations with identical shape across backends are emitted separately for each backend and width, across 84 generated files. Deriving the shared-shape implementations from common generic code instead cuts metadata volume — a comparable project measured −25%. Our generation is already the slow part of `just ci`.
Speculative until measured here. Listed so it is not lost, not because it is ready to do.
- [ ] Measure current generated metadata volume and `just generate` wall time
- [ ] Prototype on one op family before committing to the shape
## §8. The macro surface has never had a soundness audit pass
`archmage-macros` exposes `#[doc(hidden)]` items that user code can technically name. The existing soundness verifier covers intrinsic call sites against target features; it does not ask what a caller can do by invoking hidden macro internals directly with hostile input.
- [ ] Enumerate the `#[doc(hidden)]` surface of `archmage-macros`
- [ ] For each, ask what a caller naming it directly can construct
- [ ] Add compile-fail cases for anything that turns out to be reachable
---
## Recorded, not open
**One reversal.** `dec6a463` made x86 `recip()`/`rsqrt()` exact IEEE division at every width. Measurement then showed that ~1.9x/~3.6x slower on Zen-class x86 (`benchmarks/recip_x86_zen5-9950x3d_2026-09-03.md`), which would have silently regressed every caller using `.recip()` as a speed idiom. `d523b40e` replaced it with the tier split that shipped: bare `recip()`/`rsqrt()` are the working tier — ≤4 ULP with exact IEEE rails, branchless — and `recip_portable()`/`rsqrt_portable()` are the precise tier. Both are on `main`; the second supersedes the first. Do not re-propose exact-everywhere without re-reading that benchmark.
**One parked branch.** `origin/tf-inner-experiment` (`c8f18fbe`, "compiler-verified intrinsic safety via nested target_feature fns") is not on `main` and was superseded: the storage-helper approach landed instead, and `magetypes/src/` is now down to **9** `unsafe` blocks — 8 in `simd_storage.rs`, 1 in the `x86_v3.rs` trampoline macro. Either merge the branch's remaining ideas or delete it; leaving it as an undated experiment invites someone to rediscover it.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.