Skip to content

v0.9.30

Choose a tag to compare

@lilith lilith released this 07 Oct 19:42
· 107 commits to main since this release

One release of the three crates (archmage, archmage-macros, magetypes); archmage pins archmage-macros exactly and magetypes requires archmage 0.9.30. Full changelog: CHANGELOG.md.

archmage

Fixed

  • Token detect() no longer loses a race against disable(): the cold-path probe published its result with an unconditional cache store, so a summon() that entered *_detect() while the cache read "undetected" could overwrite a dangerously_disable_token_process_wide(true) landing mid-probe and leave summon() returning Some for a disabled token. The generated x86 and aarch64 detects now publish with compare_exchange(0, ..) and recheck the disabled flag. tests/token_permutations.rs gained detect_cannot_resurrect_a_disabled_token, a two-thread reproducer that trips the old code.
  • archmage depends on archmage-macros at an exact version, so the archmage-macros changes below arrive with this release.

Changed

  • The testable_dispatch docs say what it costs and where it belongs: it makes every summon() read a cache, even X64V1Token's on x86-64, which is otherwise a constant, and Cargo unifies it across the whole build, so enable it from [dev-dependencies] or a dev-only feature, never [dependencies] (aea4d41).

archmage-macros

Changed

  • The #[target_feature] function #[arcane] and #[rite] emit keeps the parameters as written ((lo, hi): (f32, f32), _: f32); only the #[arcane] wrapper, which forwards its arguments by name, names non-identifier patterns (__archmage_arg_N), and only a wildcard token is renamed in the feature-enabled function, for the nested incant! rewrite. A #[magetypes] variant that already carries the #[cfg(target_arch = ...)] the inner macro would add is no longer gated twice.
  • syn floor raised to 3.0.6, the tested version (27f814c).

Added

  • #[magetypes(rite, ...)] accepts functions without a token parameter, which 0.9.29 rejected ("rite requires a token parameter or a tier name"). Each SIMD variant becomes a tier-based #[rite(<tier>)] function, and the scalar and default variants call the covered scalar callees directly. Token-taking fallbacks keep runtime dispatch (0833022, 3aef13f).
  • #[arcane(in_impl)], #[autoversion(in_impl)] and #[magetypes(in_impl)] handle an associated function without a receiver in an inherent impl: the wrapper or dispatcher calls Self::, which the macro cannot infer from the function alone (#123).
  • #[arcane(in_trait, _self = Type)] (in_trait is an alias of nested) and the new #[autoversion(in_trait, _self = Type)] work in trait impls with plain self in the body: the receiver becomes _self, and self and Self in the body are rewritten for the nested function. #[autoversion] previously had no trait path at all; its variants now nest inside the dispatcher instead of landing in the trait impl (#123).
  • Two token parameters on #[arcane] or #[rite] are a compile error that names both; 0.9.29 silently used the first. #[autoversion] rejects an impl Trait return type with a message instead of rustc's type error on generated code. #[rite(in_trait)] and #[rite(in_impl)] say what to use instead; rustc rejects #[target_feature] on a safe trait method, which #[rite] applies directly (#123).
  • The incant!(... with token) fallback panic names the token and the tier list when no tier matches (#123).
  • Fixes from an external review of #123 (gpt-6-astra): #[arcane] rejects a parameter named like the function it generates (__arcane_<fn>, __simd_inner_<fn>), the one binding that could shadow it inside the wrapper's unsafe call; nested receivers keep their lifetime, mutability or explicit type (&'a self, self: Box<Self>) in both #[arcane(in_trait)] and #[autoversion(in_trait)], through one shared lowering; #[autoversion(in_trait, _self = T)] substitutes Self across the whole signature; the self rewrite leaves nested impl/trait bodies alone; a nested incant! keeps a covered tier's cfg(feature) gate as an arm (it was an unconditional call, E0425 with the feature off), replaces the caller's named token in the scalar fallback (E0061), and threads a wildcard token under its generated name (E0425); #[autoversion] passes the token at the user's parameter position (E0308 when it was not first). Diagnostics: the two-token message names a working alternative, the #[rite] trait-method text is qualified by architecture, and the rite rustdoc no longer lists -Ctarget-cpu as a safe-call context (#123).

Fixed

  • #[arcane] on a function that takes ScalarToken compiles; 0.9.29 emitted #[target_feature(enable = "")], which rustc rejects (c766c23).
  • #[magetypes] no longer replaces the Token marker in a nested incant! or dispatch_variant! argument list with the tier's token type, which broke those calls in 0.9.29 (c766c23).
  • The package includes the MIT and Apache-2.0 license texts (683bef7).
  • The #[rite] unknown-argument error no longer lists stub, which the parser rejects as removed (6f2bd1c).
  • #[arcane] and #[rite] no longer rebind a wildcard parameter with an impl Trait bound as let _: impl Trait = ..., which rustc rejects (E0562); wildcards are not rebound at all, and #[rite] renames tuple and wildcard parameters the way #[arcane] does (#123).
  • #[autoversion] forwards a parameter written as a pattern, such as (lo, hi): (f32, f32); 0.9.29 kept the pattern in the dispatcher's signature and left the parameter out of the call (E0061). Wildcards and patterns appear as __archmage_arg_N in the dispatcher's signature (#123).
  • #[arcane] and #[rite] collect generic bounds from every inline and where position, so a token bound split across both, or declared after another parameter's bound, is found (#123).
  • #[arcane] and #[rite] share one argument parser, diagnostic set, parameter renamer and variant emitter, so the fixes above apply to the sibling, nested and WASM expansions and to single- and multi-tier #[rite] alike (#123).
  • #[arcane] on an unsafe fn keeps unsafe on the generated #[target_feature] function (the __arcane_<fn> sibling, or the nested inner function); 0.9.0 through 0.9.29 (sibling mode, the default since 0.9.0) emitted a safe one, which any function with matching target features in the same module could call without unsafe, dropping the preconditions the user declared on the wrapper. The #[autoversion] variants of an unsafe fn are #[arcane] expansions, so they had the same hole (0.9.13 fixed their dispatcher, which had been safe too, and left the siblings). The sibling is private and #[doc(hidden)], so only code in the defining module could reach it, by naming the generated function; no other crate can. A safe input still gets a safe sibling, so #![forbid(unsafe_code)] crates are unaffected. Pinned by tests/soundness/sibling_unsafe_bypass.rs and autoversion_unsafe_bypass.rs (E0133). Found by an external review before this release.
  • #[track_caller] on an #[arcane], #[autoversion] or #[magetypes] function reaches the body: the attribute now goes on the generated #[target_feature] function as well as the wrapper or dispatcher, so Location::caller() and panic messages name the user's call site. 0.9.29 put it on the wrapper only, which named the body's own line. Pinned by tests/boundary_attrs.rs.
  • #[expect(..)] on an #[arcane], #[autoversion] or #[magetypes] function is fulfilled by the body alone: the generated #[target_feature] function keeps the #[expect] and the wrapper or dispatcher gets #[allow] of the same lints (in nested mode the wrapper keeps the #[expect], which covers the inner function). 0.9.29 copied the #[expect] onto the wrapper, where every parameter is used, so the copy was reported unfulfilled (a warning, or an error under deny(unfulfilled_lint_expectations)). #[autoversion] variants also now carry the user's lint levels, which 0.9.29 left on the dispatcher only. Pinned by tests/boundary_attrs.rs, which denies unfulfilled_lint_expectations.

magetypes

Added

  • Add magetypes inherent methods such as splat_t(token, value), zero_t(token), and load_t(token, data) across token-taking constructors, conversions, slice helpers, and single-lane scalar types. The _t methods hold the implementations; existing names retain their signatures as deprecated forwarders (772ef50, d53d425).
  • Add raw-register interop to every native-backend vector type: raw() returns the platform register, from_raw_t(token, raw) wraps one for a caller that holds a token (generic code included), and from_raw(raw) takes no token and wraps one inside a matching or stronger target-feature context that rustc checks. That covers all NEON and WASM types, all AVX-512 types (X64V4Token, X64V4xToken) and the x86 V3 128- and 256-bit types, which already had raw(). The x86 platform names 0.9.29 shipped (from_m128, from_m128d, from_m128i, from_m256, from_m256d, from_m256i) are deprecated forwarders to from_raw_t, removed in 0.10. The platform names 0.9.27 removed with the concrete types (NEON, WASM, AVX-512) do not come back: the one published crate that used them, jxl-encoder-simd 0.3.0, does not build on those targets without changes of its own, and it is ours to migrate. No type has a _t twin of a platform name (772ef50, #125).
  • The constructor inventory lists the old and new signature, bounds and platform gates of every token-taking constructor for all 40 vector types (e619c59).
  • Add mul_add_portable and mul_sub_portable to every float vector type and to f32x1/f64x1: one rounding on every backend, the same result as f32::mul_add/f64::mul_add apart from NaN payload and sign. x86 v3/v4 and NEON use FMA, the same instruction as mul_add; so do the scalar backend and f32x1/f64x1 when the CPU has FMA (always on AArch64; on x86-64 through X64V3Token::summon(), a constant with -Ctarget-cpu from Haswell/Zen 1 on), where they cost the same as mul_add (record, #125). Without hardware FMA they fuse in software, WASM included (relaxed madd may round twice), at 2.7–29.5× the time of a * b + c on the scalar backend and 8.2–24× under wasmtime (#116; 6698614; benchmark, WASM).
  • Add bounds-safe AVX-512 gather and scatter to u32x16, i32x16 and f32x16 with X64V4Token, indexed by a u32x16: gather_wrapping reads a power-of-two table with wrapping indices, gather_or reads a slice and leaves out-of-range lanes at a fallback, and scatter_select writes enabled, in-range lanes. None of them panics; there are no versions for other widths or backends (guide; e354363). Their docs and the guide name each intrinsic they use and link to Intel's entry for it; the guide and simd_storage.rs quote Intel's description and pseudocode (5b5fd7a).

Deprecated

  • Deprecate legacy token-taking constructor and helper names in favor of _t(token, ...), preserving argument order and signatures. Builds using deny(deprecated) or deny(warnings) must migrate those calls or allow the warnings. _t remains supported in the planned 0.10 transition; native NEON names already ending in _t and feature-context from_raw(raw) are not deprecated (d53d425).
  • Deprecate cast::Upcast and cast::Downcast. Nothing has ever implemented either: a generic vector carries its token, so moving one to another context means rebuilding it with that context's token, from_array_t(token, v.to_array()) for any vector or from_raw_t(token, v.raw()) on native backends. Width changes use low(), high(), split() and from_halves_t() (b1b2ff6).

Changed

  • magetypes keeps all of its unsafe in one module, simd_storage.rs, and denies unsafe_code everywhere else; the TokenStorage impls there check each wrapper's layout at compile time. Generated code is unchanged on x86_64, aarch64 and wasm32, except that four V3 128-bit pairwise-widen-add kernels name the two operands of their final add in the other order: same instructions, same results (7e49cca).
  • On WASM built with relaxed-simd, mul_add and mul_sub use the engine's relaxed madd, which may round once or twice; 0.9.29 multiplied and then added. There is no runtime probe. Under wasmtime it takes 0.96–1.06× the time of a * b + c (#116; 11a35a8, 6698614). The generic transcendentals use mul_add, so in relaxed builds their last bits can differ from 0.9.29. Every other backend keeps the 0.9.29 contract: one rounding with hardware FMA (x86 v3/v4, NEON), multiply then add on the scalar backend and strict WASM.
  • The mul_add documentation and the ISA quirks page state what each multiply-add form costs on each backend: with hardware FMA, mul_add takes 24–38% less time than a * b + c in dependency chains and about the same in streams (d652f72, ea6e5e7).
  • The transcendental docs state accuracy measured over every f32 input instead of sampled estimates. midp: log2, ln and log10 at most 4.5 ULP, cbrt 3.2, exp2 1.9 below x = 127.5 and 134.1 above it, exp and pow growing with the exponent. lowp: exp2, exp and pow within 0.56% relative error, log2, ln and log10 within 8.5e-6 absolute error (record; eeb58da, 2819d03).
  • all_true and any_true test each lane's sign bit on every backend and width, as their documentation always said. In 0.9.29, AVX-512, NEON and WASM tested for nonzero lanes (NEON u32x4 for all-ones), and x86 V3 i16/u16 vectors tested the sign bit of every byte. Comparison masks give the same results as before; other inputs can change, for example a lane holding 1 is now false (050e25c, 8014e94).
  • On WASM, to_u8 and the RGBA stores use native rounding, saturating conversion and narrowing instead of the scalar default. Results are unchanged (050e25c).
  • The published magetypes crate no longer includes tests, benches or examples: 245 KiB instead of 0.9.29's 393 KiB (877a3e6).

Fixed

  • On AVX-512, the f32 block operations ran the scalar default instead of native code (#60). to_u8 (both widths) and f32x4::store_4_rgba_u8 now narrow with vpmovusdb, and the rest use the AVX2 implementations; results are unchanged. On Zen 5, f32x8::to_u8 took 118 ns per 4096 values instead of 6,042 ns, and store_8_rgba_u8 113 ns per 1024 pixels instead of 8,273 ns (benchmark; 3999a29, 2bc19d8).
  • On x86 V3 (AVX2), to_u8 and store_*_rgba_u8 returned 0 for +inf and for values at or above 2^31; they now saturate to 255 like the other backends, and NaN still gives 0. The clamp makes these calls take 1.25–1.6× as long as the 0.9.29 form (benchmark; ff9e0a5).
  • exp2_lowp clamped its input to 126, so every x in [126, 128) returned 2^126, up to 75% low; exp_lowp and pow_lowp inherited it. The clamp is now 127.99: at most 0.56% relative error below it and 1.23% above, at the same speed (64ae008).
  • cbrt_midp_precise returned NaN, or +inf for a few inputs, for every input from 1.1342859e38 up. It now covers the whole f32 range at 3.2 ULP and takes 15–17% longer with AVX2, 8–9% on the scalar backend. cbrt_midp and cbrt_lowp keep their speed and document that above f32::MAX / 3 they return NaN, or ±inf near the limit; every fix measured 8–47% slower (2f856d2, 96f85cd).
  • The package includes the MIT and Apache-2.0 license texts (683bef7).
  • The docs of log2_midp_precise, ln_midp_precise, log10_midp_precise and pow_midp_precise promised subnormal handling that they do not have: each is its plain _midp form, so log2_midp_precise(1e-42) returns about -127 (true -139.5). The docs now say so; only cbrt_midp_precise handles subnormal inputs. Behavior is unchanged (780e205).

Workspace

Added

  • Raw expansion snapshots under tests/expand-raw/: the macros' own output before cfg evaluation, so every architecture's variant, every feature gate and every diagnostic is visible (the cargo-expand snapshots show only the host's post-cfg view). Written and checked by archmage-macros/src/raw_snapshots.rs; just expand-snapshots refreshes both sets. New combinations/*_incant.rs inputs show the nested-dispatch rewrite calling the covered tier directly with no summon().
  • magetypes/tests/harvest_shapes.rs compiles every macro signature shape the local consumer crates use: 233 shapes from 2,043 #[arcane]/#[rite]/#[autoversion]/#[magetypes] functions in 50 crates, each with placeholder types and a stub body, so a macro change that stops accepting a shape some crate relies on fails here first. Regenerated by just harvest-shapes ROOT (xtask/harvest_shapes.py) from snapshots of the crates.
  • just ci ends with a table of every step's wall time, and runs Miri and the no_std gate as background jobs in their own build directories (target/miri-ci.log, target/nostd): 399 s → 162 s on the same box and load. Three sweeps that are safe lane code join MIRI_SKIPPED_TESTS with their retained coverage named (int_uniform_shift_saturating::scalar_backend, precise_reciprocals, exp2_lowp_range); native, SDE and cross-architecture CI still run them.
  • cargo xtask generate writes expansion snapshots for 40 signature shapes (receivers, wildcards, tuples, generics, bounds, impl Trait, placement flags) across #[arcane], #[rite], #[autoversion] and #[magetypes] under tests/expand/shapes/, and the shapes the macros cannot handle under tests/expand/should-fail/ (#123).
  • archmage-macros/src/lib.rs holds the macro entry points and their docs; its unit tests moved to archmage-macros/src/tests.rs (#123).
  • Enforce generated-backend size budgets to detect unintended growth (3999a29, a97decd). The AVX-512 tokens' W128/W256 f32 backends are now generated from the F32x4Backend/F32x8Backend definitions (xtask/src/simd_types/v4_delegation_gen.rs), so a trait method cannot be left to its scalar default body for those tokens; that replaces the hand-written x86_v4_f32_delegated.rs, its validate check and its private V4Proof trait. The three AVX-512VL pixel-packing helpers stay hand-written in x86_v4_f32_overrides.rs (#125).
  • Add tokenful/tokenless calling-convention fixtures on x86, ARM/QEMU, and WASM/Wasmtime (c766c23, 595d13e).
  • Record native compilation checks for 23 published consumer libraries, including optional SIMD feature checks; this audit does not establish ARM/WASM consumer compatibility or runtime equivalence (5bfae75, aac6160).
  • Record generic-vs-intrinsic codegen and compile-cost measurements (1a0ea59).
  • Record magetypes cold-build time against 0.9.29: 3.6–6.7% longer, about 40–60 ms on a Ryzen 9 9950X3D, measured with the new benchmarks/magetypes_compile_perf.py harness (ee27ecc; moved beside its results in #125).
  • magetypes/benches/mul_add_cost.rs times a * b + c, mul_add and mul_add_portable on each backend, in a streaming loop and in a dependency chain (97730e5, ca30e56).
  • xtask soundness keeps magetypes' hand-written unsafe in simd_storage.rs: it rejects unsafe blocks elsewhere (outside the generated backend impls) and gather/scatter intrinsics anywhere else, so every gather/scatter lane offset is bounded against the borrowed slice in that one file (e354363, 3fb0c91).
  • cargo xtask validate fails when archmage-macros or magetypes lacks a license text or its copy differs from the root file (683bef7).
  • Exercise tests for the NEON dotprod, fp16 and fhm intrinsics and the AVX-512 FP16 intrinsics, which became stable in Rust 1.94 and 1.98; CI runs the suite under Intel SDE's Sapphire Rapids model, the only lane where Avx512Fp16Token summons (916be54).
  • cargo xtask <command> works: the repository defines the alias its docs use (d98d2b4).
  • The guide explains the safety model of archmage and magetypes: how safe code gets a token, the one unsafe block in each #[arcane] expansion, magetypes' single unsafe module, the known limits and the checks. Both crate docs and READMEs link it (8e2b871).

Fixed

  • The Miri CI job runs on nightly-2026-10-06: zerocopy 0.8.61 calls align_of_val_raw under cfg(miri), stable from Rust 1.99, which the old nightly-2026-02-03 pin rejected (E0658) (02bc075). The pin lives in xtask/miri-nightly.txt, which cargo xtask miri, just ci and just miri-tokens read too, so a local run uses the toolchain CI uses.
  • The intrinsic database and browser listed the FHM vfmlal*/vfmlsl* intrinsics under Arm64V2Token; they also need fhm, so Arm64V3Token. The extractor now reads cfg_attr target features (1ed03ff).

Changed

  • The archmage and magetypes READMEs open with what each crate does, run their examples, add a direct-intrinsics example, Limits and (magetypes) What's included sections, and move release history to the migration guide (56d2dfb).
  • The READMEs put the first example ahead of its explanation, show features in a table with the avx512 forwarding lines, and link to the guide for detail (724e9e0).
  • The READMEs write the AVX-512 tier as v4(cfg(avx512)) and show the avx512 feature your crate needs for it. The AVX-512 guide page recommends the explicit form (47068cd).
  • The READMEs state the <name>_<tier> naming rule and show how each macro's functions are called. The incant! guide page gains a tested example of a hand-written _v3 variant beside generated ones (e637cb0).
  • The AVX-512 guide page says what the v4 context changes for f32x8 code, from assembly inspection recorded in benchmarks/v4_context_asm_2026-10-06.md (463ad6a).
  • The READMEs and the AVX-512 page say that f32x4 and f32x8 on the V4 tokens lack transcendentals, integer conversions and raw interop (463ad6a).
  • The magetypes README demos AVX-512 with an f32x16 kernel under Features, and its quick start lists v3, neon, wasm128, scalar (d71523b).
  • The READMEs and the polyfills guide page say what polyfills cost, from assembly inspection recorded in benchmarks/polyfill_asm_2026-10-06.md (d71523b).
  • The polyfills guide page and the magetypes README report timings of polyfilled kernels on a Neoverse-N1, an Apple M4 Pro and Zen 5, recorded in benchmarks/polyfill_timing_2026-10-06.md (584c957).
  • just diff-rs shows .rs changes since the last release tag without generated code or expansion snapshots (584c957, 3159652).
  • The dispatch-testing page shows the [dev-dependencies] entry before its caveats, and the safety pages split their longest sentences (78e1ada).
  • The AVX-512 page states which macros need the caller's avx512 feature (cae4844).
  • The two guide landing pages say what each crate is before their example (dae6f38).
  • The integer codegen probe (xtask/codegen.py --integer-ops) treats swapped source operands of x86 instructions that commute bit-exactly as equal, like its NEON multiply rule (f287f3b).
  • Miri skips int_widen_narrow::scalar_backend and the fused_arithmetic binary, about 95% of its work, and the new cbrt_range sweep, which took 432 s under Miri on its own and runs only safe code there. scalar_backend's exhaustive loops reach the same unsafe bitcast path as scalar_w512_bitcast_values, which still runs under Miri; fused_arithmetic is safe code only. Native, SDE and cross-arch CI run everything. The local Miri run takes 255–394 s, depending on load (ec9c66b, d570aa9, 4d48607, 6906185).
  • The publish workflow runs the complete CI matrix at the release commit, as a reusable workflow, and publishes only if every job passed; a missing, skipped or cancelled job fails the gate, except the PR-only codegen comparison (8444c63).
  • CI runs cargo-semver-checks for archmage and magetypes on every push and in the release gate; the separate PR-only workflow is removed (f209b89).
  • just ci runs the full cargo xtask validate set, the same checks as CI's Validate Token Safety job (f4cd76e).

Historical entries below describe bundled releases of archmage, archmage-macros,
and magetypes. Their original content is preserved.