Skip to content

v0.2.0

Choose a tag to compare

@tanakh tanakh released this 02 Oct 03:46
· 116 commits to master since this release

0.2.0 — 2026-10-02

Rust 1.99 made c_variadic stable, and this release is built on it: a
variadic function definition is translated on every supported compiler, and
1.99 is the minimum. The rest is what real C asked for — the x86 SIMD
intrinsics up to AVX-512, GCC's inline assembly, a relooper that gives
goto-heavy code its loops back, and cinrs presenting itself as GCC 14.2 —
measured against SQLite, BLAKE3 and xxHash in the repository and ten more
programs outside it.

Upgrading from 0.1

  • Rust 1.99 or later is required.
  • <unistd.h>, <fcntl.h>, <strings.h> and <sys/types.h> are no longer
    bundled.
    A unit that includes one needs #pragma cinrs system_include (or
    CINRS_SYSTEM_INCLUDE=1), which brings the platform's complete headers; see
    Changed.
  • __GNUC__ is 14, up from Clang's 4.2.1, so code that tests the
    compiler's version takes GCC 14's branches; __CINRS__ is what says it is
    cinrs.
  • For direct users of cinrs-core: C_VARIADIC_SUPPORTED and
    Options::c_variadic are gone, since every supported compiler has the
    feature.

Conformance and speed, as measured for this release

  • c-testsuite: 215 of the 218 cases c99! is eligible for are correct
    (98.6 %)
    , 218 of 220 under c23! and every GNU dialect — up from 214 and
    217, because 00140, which defines a variadic function, passes on every
    supported compiler now. See doc/c-testsuite.md.
  • GCC's C torture tests: 1,643 of the 1,769 run are correct (92.9 %) under
    gnu11!, up from 1,515, and 1,636 (92.5 %) under gnu89!. Sixty cases that
    were refused on their inline assembly now run, execute/bitfld-5 with them,
    the four _FloatN issignaling cases with the keywords, and the sixty-one
    that needed only c_variadic with the new minimum Rust; execute/pr117432,
    which calls C23's one-argument va_start, passes under gnu23! and is the
    conforming refusal it is under gnu11! and gnu89!. See
    doc/gcc-torture.md.
  • Clang's C conformance tests: 167 of the 203 revisions run are correct
    (82.3 %)
    , and 557 of the 620 expected-error lines land on the right line,
    as in 0.1.0.
  • Not one case in any of the three is tagged [bug].
  • 48 whole C programs, up from 39, built as gcc -O2, clang -O2 and a
    cinrs block: median cinrs/gcc -O2 ratio 1.01×, 42 of 48 within 10 %
    of gcc -O2 or faster, every program's output identical across the three
    builds (doc/benchmarks.md, measured on Rust 1.98.1).
    The one row slower than both compilers, spectral-norm-sse2, is the price of
    signed arithmetic that wraps, which is a decision rather than a gap; see
    doc/translation.md.
  • Real programs: SQLite 3.53.4, BLAKE3 1.8.7 and xxHash 0.8.4 build, run and
    pass their own checks under scripts/check-*.sh, and CRoaring, lz4, cJSON,
    libdeflate, stb_image, chibicc, cmark, brotli, Kissat and Wren were compiled
    unedited outside the repository; each has its gcc and clang timings in
    doc/real-programs.md.

Added

  • The x86 SIMD intrinsics. #include <immintrin.h> and write Intel's
    intrinsics the way real C does: __m128, __m128i, __m128d, __m256,
    __m256i and __m256d are types, and 881 functions — SSE, SSE2, SSE3, SSSE3,
    SSE4.1, SSE4.2, AVX, AVX2, FMA, AES, PCLMUL, SHA, and the BMI1, BMI2, POPCNT
    and LZCNT scalar ones — are declared. There is no vector language and no new
    syntax: the mapping is by name. A call to _mm_add_ps(a, b) becomes
    ::core::arch::x86_64::_mm_add_ps(a, b), which works because core::arch was
    generated from the same Intel data Intel's own headers are. The bundled
    headers' prototypes were read out of core::arch's source by a maintainer
    tool that writes both the header and the table driving the mapping
    (crates/cinrs-core/tests/x86_intrinsics.rs), so a declaration and the
    function it resolves to cannot drift apart — and a test that needs no
    toolchain checks the two committed files against each other on every run.
    <xmmintrin.h>, <emmintrin.h>, <pmmintrin.h>, <tmmintrin.h>,
    <smmintrin.h>, <nmmintrin.h>, <wmmintrin.h>, <avxintrin.h>,
    <avx2intrin.h>, <x86intrin.h> and <popcntintrin.h> are bundled too, in
    the layering GCC and Clang give them, so code written for an older compiler
    finds its instruction set under the header name it expects. The macros come
    with them: _MM_SHUFFLE, _MM_SHUFFLE2, _MM_TRANSPOSE4_PS, _MM_HINT_*,
    _MM_FROUND_*, _CMP_*, _SIDD_* and the rest, with the values GCC's
    headers give them.

    The vector types are ordinary objects of a known size and alignment —
    sixteen or thirty-two bytes, aligned to themselves — so they work as locals,
    parameters, return values, statics, array elements, struct members and the
    members of the union { __m128i v; int32_t i[4]; } that most code reaches a
    lane through. An immediate operand is a const generic in core::arch,
    so the argument is folded and written into a turbofish
    (_mm_slli_epi32::<{ 3i32 }>(v)); a non-constant is a diagnostic naming the
    intrinsic, which is what Intel's own compilers say too. Taking the address
    of an intrinsic works — GCC's are static inline functions, so real code does
    — through a private extern "C" shim per intrinsic; an intrinsic with an
    immediate operand has no address, and that is a diagnostic. Nothing is
    exported
    : those prototypes add not one item to the unit's extern block
    or to its glob re-export, so a use core::arch::x86_64::* in the surrounding
    Rust cannot clash with them.

    See SIMD intrinsics for the coverage per
    instruction set and the intrinsics whose core::arch signature C cannot
    spell; tests/simd.rs runs every family, with the expected values
    computed in scalar C in the same block and cross-checked against gcc -O2.
    What stays refused, now with a diagnostic that names the intrinsic to write
    instead: the GNU vector extensions,
    __attribute__((vector_size)) and __builtin_ia32_*. What is not here:
    MMX and __m64, which core::arch dropped — <mmintrin.h> is an #error
    that names the SSE2 form of each intrinsic.

  • AVX-512, and what arrived with it. The same mapping now covers
    AVX-512F, BW, CD, DQ and VL, the VBMI, VBMI2, VNNI, BITALG, VPOPCNTDQ, IFMA,
    BF16 and FP16 extensions, GFNI, VAES and VPCLMULQDQ at every width, AVX-VNNI
    with its INT8 and INT16 forms, AVX-IFMA, F16C, SHA-512, SM3 and SM4: 6,075
    intrinsics in all, 5,194 more than the SSE-to-AVX2 set above. The new types
    are __m512, __m512i and __m512d (sixty-four bytes, aligned to
    sixty-four), the bfloat16 vectors __m128bh/__m256bh/__m512bh and the
    half-precision __m128h/__m256h/__m512h; the masks __mmask8 to
    __mmask64 are plain unsigned integers, as in GCC, and the _MM_*_ENUM
    immediates are int, with _MM_CMPINT_*, _MM_MANT_* and _MM_PERM_* —
    in GCC's lower-case spellings (_MM_MANT_SIGN_src) as well as core::arch's.
    <immintrin.h> pulls in thirty-five new headers under GCC's names, from
    <avx512fintrin.h> and <avx512vlintrin.h> to <sm4intrin.h>, and each one
    included on its own works too. A function that passes or returns a 512-bit
    vector by value needs __attribute__((target("avx512f"))), as a 256-bit one
    needs avx; rustc refuses it otherwise (tests/ui/simd_abi_512.rs). target
    and __builtin_cpu_supports know every one of the new names — the fourteen
    avx512*, gfni, vaes, vpclmulqdq, avxvnni, avxvnniint8,
    avxvnniint16, avxifma, avxneconvert, sha512, sm3, sm4, kl and
    widekl, spelled as GCC 15 spells them — and a feature list such as
    gfni,avx512bw,avx512vl is read out as the instruction sets it names in a
    diagnostic. A memory operand takes any object pointer: a pointer parameter
    is void * or const void * exactly where GCC 15.2's headers declare it so
    — 335 of them, _mm_prefetch and the SSE2 _mm_loadu_si16 family included
    — and the generated call casts every pointer argument to core::arch's
    type, so _mm512_loadu_si512(p) takes an int * without a cast, as in GCC.
    Still out, because core::arch keeps them unstable: AVX512-VP2INTERSECT,
    and the fifty FP16 and BF16 intrinsics that take or return a scalar f16 or
    bf16.

  • GCC's vector operators, subscripts and brace initialisers on the Intel
    vector types.
    In GCC __m128d is a vector of two doubles, and real code
    writes a * b + c, v * 2.0, 1.0 / v, -v, v += w, v[1] and
    (__m128d){x, y} on it. Each is now lowered to the intrinsic that does the
    same, a call of the function the header declares, so the target,
    pointer and [[cinrs::safe]] rules apply unchanged: + - * / and & | ^
    on the float and double vectors at every width, + - and the bitwise
    operators on the integer vectors as GCC's 64-bit lanes, unary - as a
    sign-bit flip, the comparisons on the 128- and 256-bit float and double
    vectors typed as the same-size integer vector, a scalar on either side
    converted to the lane type and broadcast, and the compound assignments.
    v[i] is a lane of the lane type — an lvalue when the vector is an object
    — with a constant index out of range an error; {a, b} lists the lanes in
    memory order (_mm_setr_pd), the missing ones zero. Refused, each naming
    the intrinsic to write: integer *, /, %, shifts and comparisons, the
    512-bit comparisons, every operator on the bfloat16 and half-precision
    vectors, designators in a vector's braces, ~ on a float vector, the
    reversed i[v], and lanes for a vector with static storage duration, which
    would be a call in a Rust static (the message says to assign them in a
    function). __attribute__((vector_size)) on a type of the program's own
    stays refused.

  • <mm_malloc.h>, bundled: _mm_malloc and _mm_free as static inline
    functions in portable C over malloc, with GCC's alignment rules (1, 2 and
    4 mean a pointer's; zero or a non-power of two is a null pointer), since
    GCC's calls posix_memalign, which the Microsoft runtime lacks.
    <xmmintrin.h> includes it as GCC's and Clang's do, and with it
    <stdlib.h> — programs call exit and atoi with only the intrinsics
    header included — so every unit that includes an intrinsics header carries
    <stdlib.h>'s declarations and the two functions.

  • An aligned variable length array. double v[n] __attribute__((aligned(32))) or _Alignas(32) on a variable length array
    used to be refused; the heap storage is now over-allocated and the array
    starts at the first multiple of the alignment, which is what an aligned AVX
    load of it needs. An _Alignas weaker than the element type's is still the
    error it is on any object.

  • __attribute__((target("avx2"))) and #pragma GCC target. GCC's way of
    telling one function which instruction sets it may use becomes
    #[target_feature(enable = "avx2")] on the generated item, with GCC's name
    for an instruction set translated to LLVM's (bmi is bmi1, pclmul is
    pclmulqdq, cx16 is cmpxchg16b, sse4 is both halves of SSE4, abm is
    LZCNT and POPCNT together). Several names in one string, several attributes on
    one declaration, and successive pragmas all accumulate, as GCC's do; #pragma GCC push_options and pop_options bracket a region and an attribute on a
    function wins over the pragma. A name rustc has not stabilised, a processor
    (target("arch=haswell")) and turning an instruction set off
    (target("no-avx")) are each refused with the reason, because an ignored
    target would be a function compiled for the baseline and a program that
    faults on the first instruction it has not got. On a [[cinrs::safe]]
    function it is refused too: #[target_feature] makes a function unsafe to
    call, which is the opposite of what safe promises.

  • __builtin_cpu_supports and __builtin_cpu_init. Since a procedural
    macro cannot see rustc's -C target-feature, only the architecture baseline is
    predefined — __SSE__, __SSE2__, __SSE_MATH__ and __SSE2_MATH__ on
    x86-64, and nothing above them — so #ifdef __AVX2__ takes the other branch
    and the run-time question is the one to ask.
    __builtin_cpu_supports("avx2") is std::is_x86_feature_detected!("avx2"),
    reading the same cpuid leaves GCC's own builtin does and answering as an
    int; it takes GCC's spellings. __builtin_cpu_init() is accepted and does
    nothing, Rust's detection being lazy. Both need std, so
    __builtin_cpu_supports under #pragma cinrs no_std is a located error.
    __builtin_cpu_is is deliberately absent — it names a microarchitecture,
    which std_detect cannot answer — and __has_builtin says so.

  • Inline assembly. GCC's extended and basic asm — asm, __asm,
    __asm__, with volatile and inline — becomes ::core::arch::asm! on x86
    and x86-64, with the template handed over as AT&T and
    options(att_syntax). The operands are mapped rather than guessed at: every
    one is named (o0, o1, …), "r", "q" and "x" are register classes
    (reg_byte for an 8-bit value), "a" … "D" are that register at the
    operand's width, "i" and "n" are a folded const, a tied "0" becomes
    inout … =>, =/=&/+ are lateout/out/inout, the k, w, b,
    h and q modifiers are asm!'s e, x, l, h and r, a bare %0
    keeps the operand's width, and register clobbers are out("…") _ while
    "memory" and "cc" are what asm! assumes anyway. An output goes straight
    into its place, whose side effects happen once. What asm! cannot say is
    refused by name, with the rewrite: memory operands (pass "r"(&x) and write
    (%0)), an rbx clobber (rustc keeps rbx for LLVM), %=, asm goto, flag
    outputs, "A", the x87 and MMX constraints, the range-checked immediates,
    Intel syntax, and asm in a [[cinrs::safe]] function or for another
    architecture. The "b" constraint itself — "=b"(out[1]) in every
    hand-written cpuid — is accepted: the value travels in a scratch register
    that an xchg on either side of the template swaps with rbx, and a %N
    naming it is written as %ebx or %rbx; a second "b" operand, or an
    rbx clobber beside one, is refused. See
    Inline assembly;
    tests/inline_asm.rs checks every operand kind against the values gcc -O2
    gives.

  • <cpuid.h>, bundled: GCC's __cpuid, __cpuid_count, __get_cpuid,
    __get_cpuid_count and __get_cpuid_max, and the bit_* and signature_*
    macros, in C on inline assembly that saves and restores rbx in the template
    (xchgq %rbx, %q1; cpuid; xchgq %rbx, %q1) rather than naming it as an
    operand, which rustc refuses. __get_cpuid passes subleaf 0, which GCC's
    leaves undefined. An #error on a target that is not x86.

  • SQLite compiles. scripts/check-sqlite.sh downloads the SQLite 3.53.4
    amalgamation — 9.5 MB, 269,649 lines of C in one file — verifies it against
    the SHA3-256 sqlite.org publishes, and builds and runs
    tests/sqlite-fixture/: one include_gnu11!, in both of SQLite's threading
    configurations, with an in-memory database, a CREATE TABLE, three inserts, a
    SELECT through sqlite3_prepare_v2/step/column_int and a Rust
    extern "C" function called from SQL. The 9 MB is not committed and the check
    is in scripts/ci.sh --full only, since it needs the network. It is the
    largest single C translation unit anyone ships, and what it
    exercises that no small program does — and what it costs — is in
    doc/testsuites.md.

  • BLAKE3 compiles, and picks AVX-512. scripts/check-blake3.sh downloads
    the BLAKE3 1.8.7 release, verifies it against a pinned SHA-256, and builds
    and runs tests/blake3-fixture/: the seven upstream C files, unedited, one
    unit each — blake3.c, the dispatcher, the portable code, and the SSE2,
    SSE4.1, AVX2 and AVX-512 implementations under the #pragma GCC target
    that stands in for the -m flag upstream compiles each with. The
    dispatcher reads cpuid and xgetbv with inline assembly and, on a
    processor with AVX-512, chooses it (blake3_simd_degree() is 16); the 35
    official test vectors pass in hash, keyed_hash and derive_key modes,
    and each SIMD implementation agrees with the portable one. Two things it
    wrote were refused before this release and are not now: the "=b"
    constraint on its cpuid, and always_inline under a target feature (see
    Fixed). It is in scripts/ci.sh --full only, since it needs the network.

  • xxHash compiles, and picks AVX-512. scripts/check-xxhash.sh downloads
    the xxHash v0.8.4 release, verifies it against a pinned SHA-256, and builds
    and runs tests/xxhash-fixture/: xxhash.c four times, unedited, with
    XXH_VECTOR set to scalar, SSE2, AVX2 and AVX-512 under the matching
    #pragma GCC target, and xxh_x86dispatch.c as shipped, whose cpuid
    dispatcher chooses AVX-512 on a processor that has it. Every unit passes all
    45,771 checks of upstream's generated sanity table, the four agree on every
    length up to 4,096, and the streaming API matches the one-shot one. Four
    things it wrote were refused or wrong before this release and are not now:
    #ifdef __has_include, the feature macros under #pragma GCC target, the
    {att|intel} dialect braces in its cpuid, and aligned(1) on a typedef
    (see Fixed). It is in scripts/ci.sh --full only, since it needs the
    network.

  • The Benchmarks Game's SIMD programs in the benchmark suite.
    fannkuch-redux-ssse3, n-body-sse, n-body-avx, mandelbrot-sse2,
    spectral-norm-sse2, spectral-norm-sse41, spectral-norm-avx and
    spectral-norm-avx2 are vendored under
    benches/cinrs-bench/programs/benchmarksgame/, and all eight print, byte
    for byte, what the gcc -O2 and clang -O2 builds print; the timings are
    in doc/benchmarks.md. A program's row can now name the
    instruction sets it is written for, which become -mNAME for the native
    compilers and -C target-feature=+NAME for the cinrs build.

  • C23's va_start (N2975). In c23! and gnu23! the bundled
    <stdarg.h> defines va_start(...) as GCC 15 does, over
    __builtin_c23_va_start: the list alone is enough, so a function with
    nothing before its ... — long long first(...) — can read its arguments,
    and whatever follows the list is not evaluated, so va_start(ap, n) still
    works. Below C23 the macro is the two-argument one it was, and the
    one-argument form is refused as GCC refuses it.

  • TS 18661-3's _FloatN types, as keywords. glibc uses them as the
    compiler's own from GCC 7 on, and declares strtof32, sinf64,
    csqrtf32x … under _GNU_SOURCE. _Float32 is float, _Float64 and
    _Float32x are double, and _Float64x is long double — double
    here, across the platform boundary as long double is: strtof64x and the
    f64x twins of the ISO l functions link to their double siblings, and
    another declared-only function taking one is refused at its call. Each takes
    _Complex. _Float128 and __float128 can be named — prototypes,
    typedefs, pointers, sizeof, _Generic — but no value of them is
    accepted: an object, a cast and a call of a function whose prototype
    mentions one are refused with the reason. A _Generic that lists both
    float and _Float32 (glibc's __MATH_TG) keeps the first. The
    builtins glibc's HUGE_VAL_F32, SNANF64 and their relatives expand to
    (__builtin_huge_valf32, __builtin_inff64x, __builtin_nansf32x, …) are
    there for the four types that have one. A typedef of one of the names —
    typedef float _Float32;, which glibc's bits/floatn-common.h writes for a
    compiler without the keywords — is accepted too; in 0.1.0 those names were
    refused outright, which cost 1,671 errors on one #include <math.h> under
    _GNU_SOURCE. doc/system-headers.md now measures the platform's headers
    with _GNU_SOURCE as well as without: sixty-six of the sixty-seven go
    through either way.

  • Atomic operations on a function pointer. Loading, storing, exchanging and
    compare-exchanging an object of function-pointer type — _Atomic(void (*) (void)), <stdatomic.h>'s atomic_store on one, and the __atomic_*,
    __sync_* and __c11_atomic_* builtins — used to be refused with "a
    function pointer is an Option<fn> in Rust, which no atomic holds". It goes
    through the same AtomicPtr<c_void> as any other pointer now, with a
    transmute at each end: an Option<unsafe extern "C" fn(…)> is
    pointer-sized and uses the null pointer as its None, which is exactly the
    representation C gives a function pointer. Arithmetic (__atomic_fetch_add
    and friends) is still refused, because C has none on a function pointer
    either. See tests/atomics.rs.

  • A byte order mark opening a file — the unit's own text or an
    #included header — is skipped, as GCC and Clang skip it, with every
    column still counted from the start of the file. One anywhere else is still
    a stray character.

Changed

  • The minimum supported Rust version is 1.99, up from 1.88. 1.99 is the
    release that made c_variadic stable, so a variadic function definition,
    a va_list object and a va_list * — which below it were refused with
    "requires Rust 1.99 or later (this toolchain is older)" — are now always
    translated, and the ? lines the conformance lists kept for them are gone.
    cinrs_core::C_VARIADIC_SUPPORTED and cinrs_core::Options::c_variadic,
    which only said whether the compiling toolchain had the feature, are
    removed, and cinrs-core no longer depends on rustversion. The bundled
    intrinsics headers are generated from the oldest supported compiler's
    core::arch, so that nothing is declared that it lacks, and AVX-512 is
    stable there from 1.89; regenerated from 1.99's, they are unchanged.

  • cinrs presents itself as GCC 14.2, and as __CINRS__. __GNUC__,
    __GNUC_MINOR__ and __GNUC_PATCHLEVEL__ are 14, 2 and 0, up from Clang's
    4.2.1, because that is the version real programs gate on: libdeflate
    #errored out on 4.2 ("gcc versions older than 4.9 are no longer
    supported") and switched off its VPCLMULQDQ CRC-32 and AVX-VNNI Adler-32,
    xxHash's dispatcher left AVX2 and AVX-512 off (__GNUC__ > 4), and glibc's
    <math.h> sent isnan through its slow __MATH_TG fallback instead of
    __builtin_isnan. __VERSION__ is "14.2.0 (cinrs <version>)", and the
    identity a program asks for when it wants to know who really compiled it is
    __CINRS__ (with __cinrs__, both 1) and __CINRS_MAJOR__,
    __CINRS_MINOR__, __CINRS_PATCH__. Newly predefined as GCC 14 has them:
    __GNUC_STDC_INLINE__ (__GNUC_GNU_INLINE__ in c89!/gnu89!),
    __GCC_IEC_559 and __GCC_IEC_559_COMPLEX as 0 (Annexes F and G are not
    claimed, and leaving them undefined would make glibc's <stdc-predef.h>
    claim both), __BIGGEST_ALIGNMENT__, __FLT_EVAL_METHOD_TS_18661_3__,
    __FLOAT_WORD_ORDER__ and __ORDER_PDP_ENDIAN__. Left out on purpose:
    __GCC_ASM_FLAG_OUTPUTS__ (flag outputs are refused), __SIZEOF_FLOAT128__,
    __OPTIMIZE__, __NO_INLINE__ and __PRAGMA_REDEFINE_EXTNAME. glibc's
    <tgmath.h>, the one platform header that did not go through, now does.

  • A goto no longer costs the loop it was written in. The functions whose
    jumps Rust cannot make directly are still lowered into a control-flow graph,
    but that graph is now read back into Rust's own loops, ifs and matches
    by a relooper — Emscripten's
    algorithm — instead of being emitted as one flat loop { match state { … } }
    over block numbers. A switch comes out as a match with the case bodies in
    its arms and fallthrough as the code after it, a loop comes out as a Rust loop
    named after the C label its head stands at, and a jump forwards is a break
    of a labelled block. A state variable is left only where the C really is
    irreducible — a cycle with two heads, which is what a goto into a loop body,
    Duff's device and two loops that jump into each other's bodies each make —
    and it is one u32 per such region rather than one per function. A computed
    goto is the next entry.

    What it is worth, measured on one machine:

    before after
    the statemachine kernel (a lexer written as a dozen gotos) 2.16× gcc -O2 1.00×
    one small SQLite query loop against a gcc -O2 build of the same amalgamation 4.24× 0.97×
    sqlite3VdbeExec, instructions for that loop under callgrind (GCC's: 0.35 G) 1.97 G 0.36 G

    Over SQLite's 2,610 defined functions, 13 were whole-function state machines
    and none is now: 12 are relooped outright and sqlite3VdbeExec has one
    irreducible region, abort_due_to_error, which the progress-callback loop at
    vdbe_return jumps back to. Over the GCC torture corpus 44 functions needed
    the machine, and 25 came out relooped at once; the other 19 were every one a
    computed goto. execute/medce-1, which asks the optimiser to delete a call
    to an undefined link_error() inside if (0), now compiles and passes.

  • A computed goto is a switch over the labels whose address is taken.
    That is GCC's own lowering: &&label is the label's number among them,
    from 1 (so never a null pointer), and every goto *e of a function stores
    e and goes to one shared match on it, whose default is
    unreachable!(). The graph then holds only ordinary jumps, so the relooper
    reads it like any other, and the whole-function state machine is left only
    for a graph nested deeper than rustc parses. A dispatch table — a
    static or an automatic array of distinct label addresses — that is only
    ever read is folded away: its labels are numbered in its
    order, so goto *table[op] is a match on op. An invalid target panics
    in a build with debug assertions and is unreachable_unchecked otherwise,
    as in GCC. Wren's interpreter, whose DISPATCH() is
    goto *dispatchTable[*ip++] at the end of every handler, went through the
    central match twice per opcode on the machine and ran its benchmarks at
    1.3–5.6× of gcc (8.6 G instructions on fib); it now runs at 0.89–1.28×
    of gcc and 0.93–1.09× of its own switch build (2.37 G instructions,
    against 2.34 G and gcc's 2.08 G).

  • The bundled headers are ISO C; POSIX comes from the platform. <unistd.h>,
    <fcntl.h>, <strings.h> and <sys/types.h> are no longer bundled. They were
    small incomplete copies — no access, fsync or sysconf, no struct flock
    or F_RDLCK, no ETIMEDOUT — and worse, a program that switched the
    platform's own headers on with #pragma cinrs system_include still got the
    bundled ones, which is the opposite of what it had asked for. The story is one
    sentence now: the bundled set is the headers ISO C describes (plus
    <alloca.h>, which is this crate's own), and POSIX comes from the platform,
    complete and consistent with <sys/stat.h> and <pthread.h>.

    This is breaking for a program that included one of those four without the
    pragma.
    Add one line:

    #pragma cinrs system_include
    #include <unistd.h>

    or set CINRS_SYSTEM_INCLUDE=1 for the whole crate. A program that asks for a
    POSIX header with the switch off now gets a diagnostic that says so and names
    the pragma, rather than only the directories that were searched.

    Two things follow. Apple's platforms get a default at last: the SDK's
    include directory is asked of xcrun --show-sdk-path, once per process, so a
    macOS user switches the platform on exactly as a Linux user does instead of
    having to set CINRS_SYSTEM_INCLUDE_PATH. And the bundled <errno.h> is
    complete
    : the whole POSIX.1-2017 E* set, at each platform family's own
    values — glibc and musl's from the kernel's asm-generic, Apple's from xnu,
    Windows' from the Universal CRT, with EDEADLK 36 and ETXTBSY 139 there
    rather than the Linux 35 and 26, and Darwin's ENOTSUP 45 distinct from its
    EOPNOTSUPP 102. MIPS and SPARC on Linux get only what ISO C requires, their
    kernels renumbering everything above 34.

  • A bundled header and a platform header may now define the same type. In
    plain system_include mode a unit holds both sets at once — the bundled
    <time.h> for struct timespec, glibc's <pthread.h> for
    pthread_cond_timedwait — and that used to be three errors: "redefinition of
    'struct timespec'", "redefinition of 'struct tm'" and "macro 'CLOCKS_PER_SEC'
    redefined". Every definition in the bundled <time.h> now tests and then
    claims the per-type guard macro glibc, musl and mingw-w64 use for the same
    type (_STRUCT_TIMESPEC, __struct_tm_defined, __time_t_defined,
    __clock_t_defined, and the __DEFINED_* and _*_DEFINED spellings), the way
    mingw-w64 and musl coexist with each other; and where a bundled header and a
    platform header define the same macro differently, the platform's definition
    wins without a diagnostic, since the two are descriptions of one C library.
    tests/system_headers.rs compares every shared layout against cc's over the
    platform's headers alone, because claiming a guard is a promise about layout.

  • A block under rust-analyzer no longer searches the crate from scratch.
    Where the host gives a procedural macro no source positions, a raw-token block
    finds its own text by walking the crate's .rs files and proving which
    invocation is its own — and it used to do the whole walk again for every block.
    That is a hundred directories and five thousand entries here, 25 ms a block in
    the unoptimized build a proc-macro server runs, and some thirteen seconds of
    that server's time each time an editor re-analysed this repository's own five
    hundred blocks. The search now remembers, per process: the file list for two
    seconds, since one edit provokes a burst of expansions and nothing on disk
    moves between the first and the last; and each file's text for as long as the
    file still reports the modification time and length it was read at, which the
    search asks for anyway before reading it. A block costs 0.95 ms instead of
    25 ms, a file that was just saved is read again by the very next expansion, and
    nothing about which invocation is found can change — every candidate is still
    charged against the same byte budget, and a text that is out of date fails to
    match token for token exactly as an unsaved buffer's does. cargo build never
    went near any of this: it has the positions. See
    crates/cinrs-core/src/locate.rs.

  • register int x asm("eax") says what to write instead. An asm label on
    a local variable — GCC's register variable — is still refused, and the
    message now says to write the register as a constraint of the asm that
    uses it: "a"(x).

Fixed

  • An implicitly declared C library function has the library's prototype.
    Dhrystone is K&R C that never includes <string.h>, so its strcpy and
    strcmp were declared by being called — int strcpy() — and each call went
    through a transmute of that int () to the argument types. It worked by
    the grace of the x86-64 ABI, and LLVM could not see a library call in it: no
    strcmp of known strings folded, and the kernel ran at 2.5× clang's time
    with 97 % of it in libc's strcmp. Now, as in GCC, a call to an undeclared
    function that is one of GCC's built-in library functions — or a declaration
    of one with no prototype, char *strcpy(); — declares it with the real
    prototype (the table __builtin_strcpy already used): the arguments are
    converted, strcpy returns char * (with GCC's "incompatible implicit
    declaration of built-in function" warning), and the call is a direct, typed
    call of the extern item. A later #include <string.h> merges with it; a
    program that defines the function itself keeps its own. Any other name is
    still int f(), and C99 and later still refuse an implicit declaration.

  • __builtin_prefetch prefetches. It was evaluated for its operands and
    dropped, which left xxHash's streaming XXH3_64bits_update in 4 KiB pieces
    at 1.45× the time of gcc's and clang's builds: its accumulate loop prefetches
    384 bytes ahead, and without that it waits on memory (a gcc build with
    -DXXH_NO_PREFETCH was exactly as slow). On x86 and x86-64 it is now
    core::arch's _mm_prefetch with GCC's hint for the locality, on AArch64 a
    prfm through asm!; elsewhere, and in a safe function, the operand is
    still only evaluated. rw and locality must be integer constants in
    range, as GCC requires; one that is not is an error.

  • A complex product's infinity recovery is out of line. Annex G.5.1's
    fix-up for a product that comes out NaN + iNaN was inlined into every
    * of two complex values, and in complexmandel's z = z * z + c it made
    LLVM pack the real and imaginary parts into one vector for the sake of the
    path never taken, with the shuffles on the loop-carried chain: 1.47× gcc's
    time and 1.35× clang's. As in GCC and Clang, whose inline product calls
    __muldc3 only on a NaN, cinrs_rt::complex::mul_f64 and mul_f32 now
    inline the four products and the NaN test and call a #[cold] function
    for the rest; the quotients do the same with theirs. The row is now 1.06×
    gcc and 0.96× clang, and the results are the same bits as before.

  • A variable length array in a loop is a pointer bump, not an
    allocation.
    Each one was a hidden Vec of its own, so int tmp[n];
    inside a loop was a calloc and a free per iteration, where GCC and
    Clang move the stack pointer: in the vla benchmark that was 28 % of the
    time, and the row was 1.28× gcc's and 1.24× clang's. A function that
    declares a variable length array or calls alloca now opens with a bump
    arena of its own — a private type of the unit, built on Vec so that
    #pragma cinrs no_std still takes it from alloc — whose chunks never
    move and are only allocated when one fills up. An array is a bump and a
    memset of its zeroed elements, and a frame guard whose Drop at the end
    of the block moves the arena back down, on every way out of it; the next
    iteration gets the same place. aligned(N) pads the position by address,
    alloca shares the arena and raises a floor no array's end of scope goes
    below (GCC's rule: its memory lives until the function returns), and a
    function lowered through the control-flow graph keeps one mark per array
    and gives a declaration's space back when a goto reaches it again. The
    vla row is now 1.00× gcc and 0.97× clang (0.542 s → 0.422 s), level with
    vla-hoisted, and matmul-vla, spectral-norm and fannkuch-redux, which
    declare theirs once, are unchanged.

  • A stringified macro argument is spaced the way GCC and Clang space it.
    brotli stringifies BROTLI_MAKE_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) through two macros, and with #define V(a,b) ((a)+(b)) the string S(V(1, 2)) came out as "((1)+( 2))": the space
    before 2 in the invocation travelled into (b). The first token of a
    substituted argument, and of a macro's replacement, now takes the white
    space of the parameter or name it replaces (C11 6.10.3.2p2 — white space
    around an argument is not part of it), giving "((1)+(2))". A macro or
    argument that expands to nothing, and an empty operand of ##, leave their
    white space for the next token as GCC's padding does, so [ EMPTY] and
    [ a b] with two empty arguments stringify to "[ ]"; sixty-odd such
    shapes are checked against gcc -E.

  • The variable arguments of GNU's , ## __VA_ARGS__ are not
    macro-replaced before they are substituted.
    They are an operand of ##,
    which C11 6.10.3.3p2 substitutes unexpanded, leaving the replacement to the
    rescan; cinrs expanded them first, so with #define E(f, ...) f(0, ## __VA_ARGS__) and a #-stringifying S, E(S, ONE) gave "0, 1" where
    GCC and Clang give "0, ONE". Where nothing stringifies or pastes them the
    rescan still replaces them, so the result is the same as before.

  • A function declaration may name an incomplete return type. Kissat's
    kimits.h declares changes kissat_changes (struct kissat *); with
    struct changes defined nowhere, and every unit including it stopped at
    "function cannot return an incomplete type". C wants the type complete only
    at a definition (C11 6.9.1p3) and at a call (6.5.2.2p1), which is where the
    check now is: a definition keeps the old message, and a call — direct or
    through a pointer — is refused with "calling 'f' with incomplete return type
    'struct changes'". An incomplete parameter type in a prototype was already
    accepted.

  • A declarator may hide the typedef its own declaration is of. Kissat's
    links *links = solver->links, *l = links + idx; and its
    all_stack (watch, watch, S) macro gave "unknown type name 'links'" at the
    second declarator: the specifiers' type name was looked up again for each
    declarator, after the first had hidden it. The specifiers now denote one
    type for every declarator (C11 6.7p1), while the name is the variable in the
    initialisers after its declarator (6.2.1p7) — so, as in GCC,
    links *links = (links *) p; is still a syntax error.

  • Without the complex feature a complex type may still be named. A
    declared-only prototype, a typedef, a pointer, sizeof and _Generic
    accept double _Complex and its relatives, so glibc's <complex.h> and
    <tgmath.h> go through with the feature off; the diagnostic naming the
    feature moved to where a complex value would exist — an object, a cast, a
    call of such a function (by name).

  • The bundled <math.h> has C99's classification and comparison macros.
    isnan, isinf, isfinite, isnormal, signbit and fpclassify (with
    FP_NAN … FP_NORMAL), and isgreater, isgreaterequal, isless,
    islessequal, islessgreater and isunordered (C99 7.12.3, 7.12.14) were
    missing, so Wren's VM stopped at "implicit declaration of function
    'isnan'". Each is now GCC's definition, one builtin per macro and so
    type-generic over float, double and long double; HUGE_VALL,
    MATH_ERRNO, MATH_ERREXCEPT and math_errhandling are defined too, and
    NAN and INFINITY are GCC's __builtin_nanf("") and __builtin_inff(),
    so NAN is a positive quiet NaN on every host. Like glibc's header, the
    macros are left out of strict c89!, where the names are the program's.

  • The long double boundary check no longer fires in code that cannot
    run.
    Under #pragma cinrs system_include first, glibc's isnan(x) for
    a compiler claiming GCC 4.2, as cinrs then did, is __MATH_TG, a sizeof chain naming
    __isnanf, __isnan and __isnanl; the __isnanl arm is dead for a
    double, but it was refused as a call across the boundary — Wren's VM
    again. An operand a constant condition excludes — of ?: (and GNU's
    ?: without a middle operand), of && and || after a constant left
    operand, or a branch of an if with a constant condition that has no label
    or case inside it — is still checked and still generated, but its uses of
    the boundary are not recorded, as GCC diagnoses nothing in dead code.

  • long double at the platform boundary is redirected or refused, never
    silently wrong.
    long double is double here, which is self-consistent
    inside a unit but was silently wrong at a call into the platform's library,
    whose long double is x87's eighty bits on x86-64 System V (a 128-bit quad
    on AArch64 Linux): chibicc's tokenizer calls glibc's strtold under
    #pragma cinrs system_include first, the call was bound with the double
    convention, and every floating literal it read came back as garbage;
    printf("%Lf", 2.5L) printed -nan. Now a declared-only ISO C function
    whose only difference from a double sibling is the type — strtold,
    wcstold, every <math.h> and <complex.h> l form, and nexttoward — is
    linked to the sibling (#[link_name = "strtod"]), which is what "long double is double" means; any other declared-only function with a
    long double or a long double * in its prototype is refused where it is
    called or its address taken, and so is a long double or a pointer to one
    handed to a declared-only function's ..., with the rewrite (cast to
    double and use %f). A variadic function the unit defines reads back the
    double it was passed and is unaffected. On a target whose long double is
    double already (MSVC, 32-bit Arm, Apple arm64) nothing is refused, and the
    twins are linked to the sibling all the same: the UCRT has no powl, sinl,
    fabsl … symbol at all, only <corecrt_math.h>'s inline wrappers over
    pow, sin, fabs, so a declaration linked by its own name failed with
    lld-link: error: undefined symbol: powl. The
    bundled <stdlib.h> declares strtold. tests/ui/long_double_abi.rs

  • "x" and "v" asm operands at 256 and 512 bits. libdeflate's Adler-32
    templates keep their AVX2 and AVX-512 accumulators live with an empty barrier,
    __asm__("" : "+x"(v)) or "+v"(v) on a __m256i or __m512i, which was
    refused ("a 32-byte operand cannot live in an SSE register"), and "v" was
    an unknown letter. As in GCC, "x" is now a vector register as wide as the
    operand — asm!'s xmm_reg, ymm_reg or zmm_reg — and "v" maps the
    same way, since asm! itself opens registers 16–31 to the three classes by
    the function's target features. %x0, %t0 and %g0 (the operand's xmm,
    ymm, zmm name) are {o0:x}, {o0:y} and {o0:z}. A 256-bit operand in a
    function without avx gets rustc's own error naming the target feature.

  • _Pragma's operand is macro-expanded. CRoaring and simdjson open a
    target region from a macro with _Pragma(STRINGIFY(GCC target(T))), which
    was refused with "'_Pragma' takes one string literal" and three cascade
    errors from the operand's tokens reaching the parser. The operand is now
    macro-replaced before it is destringized, written directly or produced by a
    macro, as GCC and Clang do (even the ( may come out of a macro), and the
    result goes through the same #pragma path a literal operand does. An
    operand that is still not one string literal is one error, and the whole
    parenthesised operand is consumed with it.

  • aligned(1) on a typedef is honoured. xxHash's idiom for an unaligned
    read, typedef __attribute__((__aligned__(1))) uint64_t xxh_unalign64; return *(const xxh_unalign64 *) p;, was compiled as an aligned u64
    dereference because the attribute was dropped on a typedef that was not an
    anonymous record — a silent misaligned read, undefined behaviour in Rust and
    a panic in a debug build. aligned(N) on a typedef of a scalar or a pointer
    now makes the variant GCC makes: a pointer to it carries alignment N, so an
    access through it is a read_unaligned/write_unaligned; _Alignof is N; a
    member of the type is N-aligned; and a stronger N over-aligns an object or a
    member declared with it. The shapes whose layout cannot follow yet (a member
    aligned to more than one byte but less than its type, an array member, a
    weaker alignment on a record, array or _Atomic typedef) are refused
    rather than dropped. may_alias stays accepted and ignored.

  • always_inline under a target feature compiles, and is inlined. A
    static inline __attribute__((always_inline)) helper in a function set
    compiled under #pragma GCC target("avx2") — BLAKE3's INLINE helpers, and
    the shape of every SIMD kernel written for one -m flag — became
    #[inline(always)] beside #[target_feature], which rustc refuses
    (rust-lang/rust#145574). Such a helper, when it is static and nothing
    takes its address, is now a Rust #[inline(always)] unsafe fn without
    #[target_feature] and without extern "C": GCC only ever inlines one
    into a caller with the same features, and once it is inlined there the
    intrinsics in it are too. Plain #[inline] with the feature, the first fix,
    left BLAKE3's round_fn16 out of line in its extended-output kernel, which
    put blake3_hasher_finalize_seek at 1.44× gcc's time. A helper whose
    address is taken, or that is not static, is #[inline] with the feature;
    without a feature, #[inline(always)] as before.

  • {0.0, -0.0} keeps its sign. An initialiser whose values were all zero
    was emitted as a zero fill, and -0.0 == 0.0 counted it as one, so
    double d[2] = {0.0, -0.0} lost the sign of its second element. A zero fill
    is now only for values whose bits are all zero.

  • Assigning to a lane of a vector value is refused. (a * b)[0] = 1
    wrote into the hidden local that holds the product; it is now "expression is
    not assignable", as f().x = 1 is, and as GCC's "lvalue required" says.

  • A wide bit-field keeps its width under a cast, _Generic and
    __builtin_choose_expr.
    A value computed in the width of a bit-field wider
    than int — s.b - 8 with unsigned long long b : 40 — wraps at forty bits,
    as GCC's does, and the width travels on the expression that computed it. An
    explicit cast, _Generic and __builtin_choose_expr each rebuilt that
    expression without it, so (unsigned long long) (s.b - 8) with s.b == 2
    wrapped at sixty-four bits and gave 0xfffffffffffffffa where GCC gives
    0xfffffffffa. GCC's execute/bitfld-5 is the case; the inline-assembly
    refusal in front of it had been hiding it.

  • __USER_LABEL_PREFIX__ is predefined. GCC defines it as nothing on ELF
    and as _ on Mach-O and 32-bit COFF, and glibc builds every large-file
    redirection out of it:

    #define __ASMNAME(cname) __ASMNAME2 (__USER_LABEL_PREFIX__, cname)
    #define __ASMNAME2(prefix, cname) __STRING (prefix) cname
    extern int open64 (…) __asm__ (__ASMNAME ("open64"));

    With the macro undefined, __STRING stringified the token itself and the
    symbol came out as __USER_LABEL_PREFIX__open64 — which compiled and then
    failed to link, the worst kind of wrong. It bit every program that reached
    glibc's <fcntl.h> or <sys/stat.h> with #pragma cinrs system_include.

  • An address constant may be offset by an expression. C99 6.6p9 lets the
    integer a static initialiser's address constant is offset by be any integer
    constant expression, not only a literal; only a literal was recognised, so
    static const unsigned char *p = &table[256 - OP_Ne]; — and the
    ((void *)(intptr_t)(flags | MASK)) a table of descriptors is full of — were
    refused as "initializer is not a compile-time constant expression". The
    integer parts of such an initialiser are now folded before the check, which
    also keeps the arithmetic out of the generated static. See
    tests/address_constants.rs.

Toolchain and platforms

Rust 1.99 or later, tested on 1.99.0; CI builds on stable only.

As in 0.1.0, developed and fully tested on x86-64 Linux, with the examples and
the portable tests built, linked and run in CI on macOS (arm64) and Windows
(x86-64, MSVC); other targets are compile-checked. The SIMD intrinsics and
inline assembly are x86 and x86-64 only, and refused by name elsewhere.

Full Changelog: v0.1.0...v0.2.0