Repository navigation
v0.2.0
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_SUPPORTEDand
Options::c_variadicare 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 underc23!and every GNU dialect — up from 214 and
217, because00140, which defines a variadic function, passes on every
supported compiler now. Seedoc/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 %) undergnu89!. Sixty cases that
were refused on their inline assembly now run,execute/bitfld-5with them,
the four_FloatNissignalingcases with the keywords, and the sixty-one
that needed onlyc_variadicwith the new minimum Rust;execute/pr117432,
which calls C23's one-argumentva_start, passes undergnu23!and is the
conforming refusal it is undergnu11!andgnu89!. See
doc/gcc-torture.md. - Clang's C conformance tests: 167 of the 203 revisions run are correct
(82.3 %), and 557 of the 620expected-errorlines 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 -O2and a
cinrsblock: mediancinrs/gcc -O2ratio 1.01×, 42 of 48 within 10 %
ofgcc -O2or 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 underscripts/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,
__m256iand__m256dare 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 becausecore::archwas
generated from the same Intel data Intel's own headers are. The bundled
headers' prototypes were read out ofcore::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,structmembers and the
members of theunion { __m128i v; int32_t i[4]; }that most code reaches a
lane through. An immediate operand is aconstgeneric incore::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 arestatic inlinefunctions, so real code does
— through a privateextern "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'sexternblock
or to its glob re-export, so ause core::arch::x86_64::*in the surrounding
Rust cannot clash with them.See SIMD intrinsics for the coverage per
instruction set and the intrinsics whosecore::archsignature C cannot
spell;tests/simd.rsruns every family, with the expected values
computed in scalar C in the same block and cross-checked againstgcc -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, whichcore::archdropped —<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,__m512iand__m512d(sixty-four bytes, aligned to
sixty-four), the bfloat16 vectors__m128bh/__m256bh/__m512bhand the
half-precision__m128h/__m256h/__m512h; the masks__mmask8to
__mmask64are plain unsigned integers, as in GCC, and the_MM_*_ENUM
immediates areint, with_MM_CMPINT_*,_MM_MANT_*and_MM_PERM_*—
in GCC's lower-case spellings (_MM_MANT_SIGN_src) as well ascore::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
needsavx; rustc refuses it otherwise (tests/ui/simd_abi_512.rs).target
and__builtin_cpu_supportsknow every one of the new names — the fourteen
avx512*,gfni,vaes,vpclmulqdq,avxvnni,avxvnniint8,
avxvnniint16,avxifma,avxneconvert,sha512,sm3,sm4,kland
widekl, spelled as GCC 15 spells them — and a feature list such as
gfni,avx512bw,avx512vlis read out as the instruction sets it names in a
diagnostic. A memory operand takes any object pointer: a pointer parameter
isvoid *orconst void *exactly where GCC 15.2's headers declare it so
— 335 of them,_mm_prefetchand the SSE2_mm_loadu_si16family included
— and the generated call casts every pointer argument tocore::arch's
type, so_mm512_loadu_si512(p)takes anint *without a cast, as in GCC.
Still out, becausecore::archkeeps them unstable: AVX512-VP2INTERSECT,
and the fifty FP16 and BF16 intrinsics that take or return a scalarf16or
bf16. -
GCC's vector operators, subscripts and brace initialisers on the Intel
vector types. In GCC__m128dis a vector of two doubles, and real code
writesa * 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 thetarget,
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
reversedi[v], and lanes for a vector with static storage duration, which
would be a call in a Ruststatic(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_mallocand_mm_freeasstatic inline
functions in portable C overmalloc, 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 callsposix_memalign, which the Microsoft runtime lacks.
<xmmintrin.h>includes it as GCC's and Clang's do, and with it
<stdlib.h>— programs callexitandatoiwith 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_Alignasweaker 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 (bmiisbmi1,pclmulis
pclmulqdq,cx16iscmpxchg16b,sse4is both halves of SSE4,abmis
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_optionsandpop_optionsbracket 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
targetwould 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_supportsand__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")isstd::is_x86_feature_detected!("avx2"),
reading the samecpuidleaves 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 needstd, so
__builtin_cpu_supportsunder#pragma cinrs no_stdis a located error.
__builtin_cpu_isis deliberately absent — it names a microarchitecture,
whichstd_detectcannot answer — and__has_builtinsays so. -
Inline assembly. GCC's extended and basic
asm—asm,__asm,
__asm__, withvolatileandinline— 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_bytefor an 8-bit value),"a"…"D"are that register at the
operand's width,"i"and"n"are a foldedconst, a tied"0"becomes
inout … =>,=/=&/+arelateout/out/inout, thek,w,b,
handqmodifiers areasm!'se,x,l,handr, a bare%0
keeps the operand's width, and register clobbers areout("…") _while
"memory"and"cc"are whatasm!assumes anyway. An output goes straight
into its place, whose side effects happen once. Whatasm!cannot say is
refused by name, with the rewrite: memory operands (pass"r"(&x)and write
(%0)), anrbxclobber (rustc keeps rbx for LLVM),%=,asm goto, flag
outputs,"A", the x87 and MMX constraints, the range-checked immediates,
Intel syntax, andasmin a[[cinrs::safe]]function or for another
architecture. The"b"constraint itself —"=b"(out[1])in every
hand-writtencpuid— is accepted: the value travels in a scratch register
that anxchgon either side of the template swaps with rbx, and a%N
naming it is written as%ebxor%rbx; a second"b"operand, or an
rbxclobber beside one, is refused. See
Inline assembly;
tests/inline_asm.rschecks every operand kind against the valuesgcc -O2
gives. -
<cpuid.h>, bundled: GCC's__cpuid,__cpuid_count,__get_cpuid,
__get_cpuid_countand__get_cpuid_max, and thebit_*andsignature_*
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_cpuidpasses subleaf 0, which GCC's
leaves undefined. An#erroron a target that is not x86. -
SQLite compiles.
scripts/check-sqlite.shdownloads 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/: oneinclude_gnu11!, in both of SQLite's threading
configurations, with an in-memory database, aCREATE TABLE, three inserts, a
SELECTthroughsqlite3_prepare_v2/step/column_intand a Rust
extern "C"function called from SQL. The 9 MB is not committed and the check
is inscripts/ci.sh --fullonly, 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.shdownloads
the BLAKE3 1.8.7 release, verifies it against a pinned SHA-256, and builds
and runstests/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-mflag upstream compiles each with. The
dispatcher readscpuidandxgetbvwith inline assembly and, on a
processor with AVX-512, chooses it (blake3_simd_degree()is 16); the 35
official test vectors pass inhash,keyed_hashandderive_keymodes,
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 itscpuid, andalways_inlineunder a target feature (see
Fixed). It is inscripts/ci.sh --fullonly, since it needs the network. -
xxHash compiles, and picks AVX-512.
scripts/check-xxhash.shdownloads
the xxHash v0.8.4 release, verifies it against a pinned SHA-256, and builds
and runstests/xxhash-fixture/:xxhash.cfour times, unedited, with
XXH_VECTORset to scalar, SSE2, AVX2 and AVX-512 under the matching
#pragma GCC target, andxxh_x86dispatch.cas shipped, whosecpuid
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 itscpuid, andaligned(1)on atypedef
(see Fixed). It is inscripts/ci.sh --fullonly, 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-avxand
spectral-norm-avx2are vendored under
benches/cinrs-bench/programs/benchmarksgame/, and all eight print, byte
for byte, what thegcc -O2andclang -O2builds print; the timings are
in doc/benchmarks.md. A program's row can now name the
instruction sets it is written for, which become-mNAMEfor the native
compilers and-C target-feature=+NAMEfor thecinrsbuild. -
C23's
va_start(N2975). Inc23!andgnu23!the bundled
<stdarg.h>definesva_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, sova_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
_FloatNtypes, as keywords. glibc uses them as the
compiler's own from GCC 7 on, and declaresstrtof32,sinf64,
csqrtf32x… under_GNU_SOURCE._Float32isfloat,_Float64and
_Float32xaredouble, and_Float64xislong double—double
here, across the platform boundary aslong doubleis:strtof64xand the
f64xtwins of the ISOlfunctions link to theirdoublesiblings, and
another declared-only function taking one is refused at its call. Each takes
_Complex._Float128and__float128can 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_Genericthat lists both
floatand_Float32(glibc's__MATH_TG) keeps the first. The
builtins glibc'sHUGE_VAL_F32,SNANF64and their relatives expand to
(__builtin_huge_valf32,__builtin_inff64x,__builtin_nansf32x, …) are
there for the four types that have one. Atypedefof one of the names —
typedef float _Float32;, which glibc'sbits/floatn-common.hwrites 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.mdnow measures the platform's headers
with_GNU_SOURCEas 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>'satomic_storeon one, and the__atomic_*,
__sync_*and__c11_atomic_*builtins — used to be refused with "a
function pointer is anOption<fn>in Rust, which no atomic holds". It goes
through the sameAtomicPtr<c_void>as any other pointer now, with a
transmuteat each end: anOption<unsafe extern "C" fn(…)>is
pointer-sized and uses the null pointer as itsNone, 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. Seetests/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 madec_variadicstable, so a variadic function definition,
ava_listobject and ava_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_SUPPORTEDandcinrs_core::Options::c_variadic,
which only said whether the compiling toolchain had the feature, are
removed, andcinrs-coreno longer depends onrustversion. 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>sentisnanthrough its slow__MATH_TGfallback 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__, both1) and__CINRS_MAJOR__,
__CINRS_MINOR__,__CINRS_PATCH__. Newly predefined as GCC 14 has them:
__GNUC_STDC_INLINE__(__GNUC_GNU_INLINE__inc89!/gnu89!),
__GCC_IEC_559and__GCC_IEC_559_COMPLEXas0(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
gotono 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 ownloops,ifs andmatches
by a relooper — Emscripten's
algorithm — instead of being emitted as one flatloop { match state { … } }
over block numbers. Aswitchcomes out as amatchwith 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 abreak
of a labelled block. A state variable is left only where the C really is
irreducible — a cycle with two heads, which is what agotointo a loop body,
Duff's device and two loops that jump into each other's bodies each make —
and it is oneu32per such region rather than one per function. A computed
gotois the next entry.What it is worth, measured on one machine:
before after the statemachinekernel (a lexer written as a dozengotos)2.16× gcc -O21.00× one small SQLite query loop against a gcc -O2build of the same amalgamation4.24× 0.97× sqlite3VdbeExec, instructions for that loop undercallgrind(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 andsqlite3VdbeExechas one
irreducible region,abort_due_to_error, which the progress-callback loop at
vdbe_returnjumps 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
computedgoto.execute/medce-1, which asks the optimiser to delete a call
to an undefinedlink_error()insideif (0), now compiles and passes. -
A computed goto is a switch over the labels whose address is taken.
That is GCC's own lowering:&&labelis the label's number among them,
from 1 (so never a null pointer), and everygoto *eof a function stores
eand goes to one sharedmatchon it, whosedefaultis
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 thanrustcparses. A dispatch table — a
staticor an automatic array of distinct label addresses — that is only
ever read is folded away: its labels are numbered in its
order, sogoto *table[op]is amatchonop. An invalid target panics
in a build with debug assertions and isunreachable_uncheckedotherwise,
as in GCC. Wren's interpreter, whoseDISPATCH()is
goto *dispatchTable[*ip++]at the end of every handler, went through the
centralmatchtwice per opcode on the machine and ran its benchmarks at
1.3–5.6× of gcc (8.6 G instructions onfib); it now runs at 0.89–1.28×
of gcc and 0.93–1.09× of its ownswitchbuild (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 — noaccess,fsyncorsysconf, nostruct flock
orF_RDLCK, noETIMEDOUT— and worse, a program that switched the
platform's own headers on with#pragma cinrs system_includestill 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=1for 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 ofxcrun --show-sdk-path, once per process, so a
macOS user switches the platform on exactly as a Linux user does instead of
having to setCINRS_SYSTEM_INCLUDE_PATH. And the bundled<errno.h>is
complete: the whole POSIX.1-2017E*set, at each platform family's own
values — glibc and musl's from the kernel'sasm-generic, Apple's from xnu,
Windows' from the Universal CRT, withEDEADLK36 andETXTBSY139 there
rather than the Linux 35 and 26, and Darwin'sENOTSUP45 distinct from its
EOPNOTSUPP102. 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
plainsystem_includemode a unit holds both sets at once — the bundled
<time.h>forstruct 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_*_DEFINEDspellings), 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.rscompares every shared layout againstcc'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.rsfiles 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 buildnever
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. Anasmlabel on
a local variable — GCC's register variable — is still refused, and the
message now says to write the register as a constraint of theasmthat
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 itsstrcpyand
strcmpwere declared by being called —int strcpy()— and each call went
through atransmuteof thatint ()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
strcmpof known strings folded, and the kernel ran at 2.5× clang's time
with 97 % of it in libc'sstrcmp. 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_strcpyalready used): the arguments are
converted,strcpyreturnschar *(with GCC's "incompatible implicit
declaration of built-in function" warning), and the call is a direct, typed
call of theexternitem. A later#include <string.h>merges with it; a
program that defines the function itself keeps its own. Any other name is
stillint f(), and C99 and later still refuse an implicit declaration. -
__builtin_prefetchprefetches. It was evaluated for its operands and
dropped, which left xxHash's streamingXXH3_64bits_updatein 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_PREFETCHwas exactly as slow). On x86 and x86-64 it is now
core::arch's_mm_prefetchwith GCC's hint for the locality, on AArch64 a
prfmthroughasm!; elsewhere, and in a safe function, the operand is
still only evaluated.rwandlocalitymust 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 incomplexmandel'sz = z * z + cit 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
__muldc3only on a NaN,cinrs_rt::complex::mul_f64andmul_f32now
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 hiddenVecof its own, soint tmp[n];
inside a loop was acallocand afreeper iteration, where GCC and
Clang move the stack pointer: in thevlabenchmark 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 callsallocanow opens with a bump
arena of its own — a private type of the unit, built onVecso that
#pragma cinrs no_stdstill takes it fromalloc— whose chunks never
move and are only allocated when one fills up. An array is a bump and a
memsetof its zeroed elements, and a frame guard whoseDropat 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,
allocashares 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 agotoreaches it again. The
vlarow is now 1.00× gcc and 0.97× clang (0.542 s → 0.422 s), level with
vla-hoisted, andmatmul-vla,spectral-normandfannkuch-redux, which
declare theirs once, are unchanged. -
A stringified macro argument is spaced the way GCC and Clang space it.
brotli stringifiesBROTLI_MAKE_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)through two macros, and with#define V(a,b) ((a)+(b))the stringS(V(1, 2))came out as"((1)+( 2))": the space
before2in 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 againstgcc -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#-stringifyingS,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.hdeclareschanges kissat_changes (struct kissat *);with
struct changesdefined 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
typedefits 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
complexfeature a complex type may still be named. A
declared-only prototype, atypedef, a pointer,sizeofand_Generic
acceptdouble _Complexand 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,signbitandfpclassify(with
FP_NAN…FP_NORMAL), andisgreater,isgreaterequal,isless,
islessequal,islessgreaterandisunordered(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 overfloat,doubleandlong double;HUGE_VALL,
MATH_ERRNO,MATH_ERREXCEPTandmath_errhandlingare defined too, and
NANandINFINITYare GCC's__builtin_nanf("")and__builtin_inff(),
soNANis a positive quiet NaN on every host. Like glibc's header, the
macros are left out of strictc89!, where the names are the program's. -
The
long doubleboundary check no longer fires in code that cannot
run. Under#pragma cinrs system_include first, glibc'sisnan(x)for
a compiler claiming GCC 4.2, as cinrs then did, is__MATH_TG, asizeofchain naming
__isnanf,__isnanand__isnanl; the__isnanlarm 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 anifwith a constant condition that has no label
orcaseinside it — is still checked and still generated, but its uses of
the boundary are not recorded, as GCC diagnoses nothing in dead code. -
long doubleat the platform boundary is redirected or refused, never
silently wrong.long doubleisdoublehere, which is self-consistent
inside a unit but was silently wrong at a call into the platform's library,
whoselong doubleis x87's eighty bits on x86-64 System V (a 128-bit quad
on AArch64 Linux): chibicc's tokenizer calls glibc'sstrtoldunder
#pragma cinrs system_include first, the call was bound with thedouble
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 adoublesibling is the type —strtold,
wcstold, every<math.h>and<complex.h>lform, andnexttoward— is
linked to the sibling (#[link_name = "strtod"]), which is what "long doubleisdouble" means; any other declared-only function with a
long doubleor along double *in its prototype is refused where it is
called or its address taken, and so is along doubleor a pointer to one
handed to a declared-only function's..., with the rewrite (cast to
doubleand use%f). A variadic function the unit defines reads back the
doubleit was passed and is unaffected. On a target whoselong doubleis
doublealready (MSVC, 32-bit Arm, Apple arm64) nothing is refused, and the
twins are linked to the sibling all the same: the UCRT has nopowl,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>declaresstrtold.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__m256ior__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!'sxmm_reg,ymm_regorzmm_reg— and"v"maps the
same way, sinceasm!itself opens registers 16–31 to the three classes by
the function's target features.%x0,%t0and%g0(the operand's xmm,
ymm, zmm name) are{o0:x},{o0:y}and{o0:z}. A 256-bit operand in a
function withoutavxgets 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#pragmapath 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 atypedefis 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 alignedu64
dereference because the attribute was dropped on atypedefthat was not an
anonymous record — a silent misaligned read, undefined behaviour in Rust and
a panic in a debug build.aligned(N)on atypedefof a scalar or a pointer
now makes the variant GCC makes: a pointer to it carries alignment N, so an
access through it is aread_unaligned/write_unaligned;_Alignofis 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_Atomictypedef) are refused
rather than dropped.may_aliasstays accepted and ignored. -
always_inlineunder 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'sINLINEhelpers, and
the shape of every SIMD kernel written for one-mflag — became
#[inline(always)]beside#[target_feature], which rustc refuses
(rust-lang/rust#145574). Such a helper, when it isstaticand nothing
takes its address, is now a Rust#[inline(always)] unsafe fnwithout
#[target_feature]and withoutextern "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'sround_fn16out of line in its extended-output kernel, which
putblake3_hasher_finalize_seekat 1.44× gcc's time. A helper whose
address is taken, or that is notstatic, 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.0counted 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", asf().x = 1is, and as GCC's "lvalue required" says. -
A wide bit-field keeps its width under a cast,
_Genericand
__builtin_choose_expr. A value computed in the width of a bit-field wider
thanint—s.b - 8withunsigned 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,_Genericand__builtin_choose_expreach rebuilt that
expression without it, so(unsigned long long) (s.b - 8)withs.b == 2
wrapped at sixty-four bits and gave0xfffffffffffffffawhere GCC gives
0xfffffffffa. GCC'sexecute/bitfld-5is 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,
__STRINGstringified 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 generatedstatic. 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