Skip to content

cc Updates - #716

Merged
jgarzik merged 20 commits into
mainfrom
updates
Oct 3, 2026
Merged

jgarzik merged 20 commits into
mainfrom
updates

Conversation

@jgarzik

@jgarzik jgarzik commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@jgarzik
jgarzik requested a balanced review from Copilot October 3, 2026 20:06
@jgarzik jgarzik self-assigned this Oct 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The change set is large and spans conformance-sensitive lexer, preprocessor, literal, ABI, and codegen behavior whose correctness warrants final human review despite no confirmed blocking defect.

Review effort: Balanced
Findings: None

What changed in this PR

"cc Updates" is a broad set of changes to the cc crate (the from-scratch C17 compiler) in posixutils-rs. It tightens lexer/preprocessor conformance, fixes handling of constructs left unfinished at end-of-file, improves universal-character-name (UCN) literal diagnostics, and adds substantial cross-target test coverage. The changes move several utility areas closer to the "feature complete"/"test coverage" lifecycle stages while keeping behavior aligned with gcc/clang where POSIX/C17 is silent.

Changes:

  • Lexer/preprocessor conformance: remove the non-C .. (DotDot) operator and lex ... via explicit two-dot lookahead; mark the stream-end token as beginning a line so unterminated directives/_Pragma at EOF no longer loop unbounded; extend ucn_is_forbidden to reject scalars above U+10FFFF.
  • Literal handling: add an IncompleteUcn escape variant so a \u/\U with too few hex digits is diagnosed, decodes as its letter, and consumes only its digits; report an empty character constant per C17 6.4.4.4p1.
  • Tests/docs: new preprocessor suites (end_of_file, target_predefines, pp_diagnostics) plus literal/lexer/preprocess unit tests; remove a closed TODO conformance item.
File Description
cc/​token/​lexer.rs Removes DotDot, lexes ... via lookahead, forbids UCNs > U+10FFFF, marks stream-end token as line-starting
cc/​token/​literal.rs Adds IncompleteUcn escape variant + diagnostic and empty-char-constant error; renders the letter in all byte/utf16/wide encodings
cc/​token/​test_lexer.rs Drops test_dotdot_operator, updates two-char-operator test, adds stream-end and dot-punctuator tests
cc/​token/​test_literal.rs Adds tests for empty char constant, UCNs beyond the code space, and incomplete-UCN digit retention
cc/​token/​test_preprocess.rs Adds tests for EOF survival, malformed _Pragma, stringify/paste spacing, #if ?:, PIC macros, and #line
cc/​tests/​preprocessor/​mod.rs Registers new end_of_file, pp_diagnostics, and target_predefines test modules
cc/​tests/​preprocessor/​end_of_file.rs New suite covering translation units ending mid-construct
cc/​tests/​preprocessor/​target_predefines.rs New cross-target predefined-macro tests (Darwin/FreeBSD/Linux, unsupported OS)
cc/​TODO.md Removes the now-resolved restrict-on-non-pointer conformance item

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

jgarzik and others added 20 commits October 3, 2026 21:16
…lues are diagnosed, not crashes

- The end of the stream now begins a line. A last line closed by a
  backslash-newline let `#define X \`, `#include "x" \` and friends read the
  end-of-stream marker as an operand, and the parser, never seeing the end
  of the file, looped allocating without bound.
- `_Pragma` peeks before it takes each operand token and reports gcc's
  "_Pragma takes a parenthesized string literal" when the operand is
  malformed or cut off, instead of silently swallowing the next token
  (the end of the stream included).
- C17 6.7.2.1p3: a member of incomplete or function type is an error and is
  dropped, so `struct A { struct A a; }` no longer builds a type that
  contains itself (a Rust stack overflow in initializer lowering).
- C17 6.7.2.1p13: a tagged struct/union specifier in a member list declares
  its tag, not an anonymous member ("declaration does not declare
  anything", as gcc), which closes `struct A { struct A; }` the same way.
- Reading, writing or read-modify-writing an object of incomplete type is
  "invalid use of incomplete type" (C17 6.3.2.1p2), checked where the
  linearizer reads and assigns objects; a forward-declared enum's
  zero-width value used to reach the IR validator as an ICE.
- C17 6.9.1p7 / 6.5.2.2p4: a definition's parameter of incomplete type, and
  a call through a prototype naming one, are errors with gcc's wording.
- Tests: `compile_bounded` runs c17 under a deadline so a regressed hang
  fails instead of hanging the suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ve diagnostics

Wrong output:
- `#__VA_ARGS__` keeps the spacing of the commas between variadic
  arguments: argument collection now keeps the real separator tokens, so
  `H(a , b)` stringifies as "a , b" (was "a, b"), and `-E` of a
  `__VA_ARGS__` expansion keeps them too.
- A `##` result is spaced as its left operand was, not as the invocation:
  `S(a[b##n])` gave "a[ b1]", which broke <sys/sdt.h> probe operands.
- `#if c ? x : y` evaluates only the arm taken (C17 6.5.15p4), so
  `1 ? 2 : (1/0)` is accepted, and the result has both arms' common type
  (`1 ? -1 : 0u` is unsigned).
- `..` is no longer a token: C17 has no such punctuator, so `.` ## `.` is
  now gcc's invalid-paste error instead of silently making one.

Diagnostics gcc gives:
- extra tokens after #ifdef/#ifndef/#undef/#include warn; after #line it
  is a warning (was an error) and the directive still applies.
- `#define A+1` warns "ISO C99 requires whitespace after the macro name".
- `__VA_ARGS__` anywhere but a C99 variadic macro's body warns.
- `#include stdio.h` is an error instead of a guessed "stdio.h".
- redefining a predefine or -D macro to something else warns (feature-test
  macros excepted, since c17 predefines those where gcc does not);
  `#undef` of a C17 6.10.8 name warns; `#undef defined` is an error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ected

- C17 6.7.2.1p13: an anonymous struct/union member counts as a named member
  before a flexible array member (<linux/bpf.h>, all of tools/lib/bpf);
  only an unnamed bit-field does not, as in gcc.
- An unnamed bit-field may begin a declarator list, `unsigned char :1, :1,
  x:1;` (<linux/ioam6.h>): the separate first-position branch that then
  demanded `;` is gone, and the declarator loop handles both positions.
- A stray `;` in a member list is skipped, as gcc does (<linux/nfc.h>).
- A `_Static_assert` message is a string literal after phase 6, so adjacent
  literals concatenate (`BUILD_BUG_ON_ZERO`'s `#e " is true"`), with any
  encoding prefix.
- `&&` and `||` fold without evaluating a right operand the left decides
  (C17 6.5.13p4, 6.5.14p4, 6.6p3): `1 || 1/0` is a constant, in static
  initializers, array bounds, `case` labels and `_Static_assert`.
- The bundled <stdint.h> hands a hosted glibc build to glibc's own, as
  gcc's does: <sys/eventfd.h>, <sys/inotify.h>, <sys/signalfd.h> and
  <sys/fanotify.h> include only <stdint.h> and expect <sys/cdefs.h> from it.

A sweep of every /usr/include header gcc compiles alone now finds none that
c17 rejects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…integers as double

- C17 6.4.2.2p1: `__func__` (and gcc's `__FUNCTION__`/`__PRETTY_FUNCTION__`)
  is a `static const char[]`, typed from the enclosing function's name. It
  was `char *`: `sizeof __func__` was 8, `_Generic` chose `char *`, and a
  write through it was accepted. It holds the name as written, not an asm
  label's (`int f(void) __asm__("g")` has `__func__` "f", as gcc); it is an
  address constant in a static initializer; outside a function gcc's
  warning and an empty name.
- C17 7.3.1p4: `_Complex_I` is `float _Complex`; the `double _Complex` one
  widened every float complex expression it touched.
- C17 7.25p3 in <tgmath.h>: an integer argument counts as `double` when
  choosing among several arguments, so `pow(i, f)` is `pow`, not `powf`
  (which rounded i); a real argument to a complex-only macro (`cimag`,
  `conj`, ...) picks its own precision's function.
- `_Complex` with a typedef name, `typeof`, a tag, `_Bool` or `void` is an
  error instead of being silently dropped (`ty _Complex z` was a double).
- `mode(byte)` and `mode(unwind_word)` select 1- and 8-byte integers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Flags that stopped c17 before it compiled anything:
- `-pedantic`, `-pedantic-errors`: accepted (the pedantic mode c17 has).
- `-ansi`: gcc's `-std=c90`, reported as any older revision is.
- `-x LANG` / `-xLANG` for c, cpp-output, assembler, assembler-with-cpp
  and none, applying to every later operand; another language is "language
  not recognized". Operands are classified through one `Args::lang_of`, so
  `.S`-style preprocessing and `.i` handling follow `-x` too.
- `-g0`..`-g3`, `-ggdb[N]`, `-gdwarf[-N]`: last one wins (`-g -g0` is no
  debug info); layout-only `-g` options (`-gsplit-dwarf`, `-gz`, ...) are
  ignored quietly, any other `-g...` with a warning.
- `-m` flags are judged once the target is known: choosing or tuning for a
  CPU (`-march=`, `-mtune=`, `-mcpu=`) and the target's own baseline
  (`-m64`, `-msse2`, `-mfpmath=sse`, `-mabi=lp64`, ...) change nothing c17
  emits and are accepted; an ISA extension or ABI change is still refused.

`__PIC__`/`__pic__` and `__PIE__`/`__pie__` (value 2) are now defined from
the same position-independence decision that drives code generation, for C
and `.S` alike; Mach-O always has `__PIC__`, as with clang. Hand-written asm
that tests them took the absolute-address path in PIC code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fused

- FreeBSD: `wint_t` is `int` (its `__ct_rune_t`), so `__WINT_TYPE__`, the
  bundled <stdint.h> limits and the system's typedef agree;
  `__FreeBSD_kernel__` (GNU/kFreeBSD's macro) and `__BSD_VISIBLE` (which
  <sys/cdefs.h> computes from the program's feature-test macros) are no
  longer predefined.
- Darwin: `__APPLE_CC__` is clang's 6000; the `__DARWIN__` and `__MACH_O__`
  that no Darwin compiler defines are gone.
- `--target` with an OS c17 does not support (windows, mingw, none,
  netbsd, ...) is "unsupported target" instead of a silent Linux with
  `__linux__` and `__ELF__` defined. A bare architecture still means Linux.
- The feature-test macros c17 predefines on Linux stay, now recorded in
  DECISIONS.md with what they cost (a visible GNU namespace, glibc's C2X
  `strtol`/`scanf` bindings); the comment claiming gcc does the same is
  corrected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… honoured

Linker flags:
- `-Wl,a,b` was split at its commas and every piece handed to the host
  driver bare, after all objects and libraries: `-Wl,--whole-archive` became
  a driver option `cc` rejects, `-Wl,-soname,x` two unrelated words, and
  nothing kept its place relative to the archive it governs. `-Wl,...` and
  `-Xlinker x` now travel as written, and `linkargs::scan` keeps every
  link-step flag (`-pthread`, `-rdynamic` too) in its position on the link
  line, as it already did for operands, `-L`, `-l` and `-R`.

Visibility:
- `-fvisibility=` was accepted and ignored, so a shared object built with
  `-fvisibility=hidden` exported every symbol, and calls between its own
  functions bound through the PLT to the executable's symbols of the same
  name -- CPython's test_peg_generator extension ran the interpreter's
  parser instead of its own. Every definition with external linkage that
  names no visibility now gets the flag's (`Module::apply_default_visibility`);
  an unknown value is an error, as in gcc.
- An object's definition now inherits the attributes of every declaration of
  it, before or after, as gcc does: CPython's PyAPI_DATA puts
  `visibility("default")` only on the `extern` declaration, so its objects
  were hidden and extension modules could not find `PyFloat_Type`.

CPython 3.12.9 at -O2: run=41,758 Result: SUCCESS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The error and warning counters were process globals. The compiler runs
on one thread of its own, but a unit test's translation unit runs on one
of the test harness's, so a count shared with every concurrent test made
"did this report an error" unanswerable: a parser test comparing the
count before and after saw other tests' errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rced

c17 checked a redeclaration's type only, and only in its own scope.
Linkage is now tracked (C17 6.2.2) per symbol and, for identifiers with
linkage, across the whole translation unit (parse/linkage.rs):
- 6.7p3: a second declaration of a no-linkage identifier in one scope is
  "redefinition of 'x'" -- it used to bind nothing, silently -- including a
  body declaration repeating a parameter; mixing a no-linkage declaration
  with an `extern` one in a scope is gcc's error in either order.
- 6.2.2p7: `static` after a non-static declaration, and a non-static object
  after a static one, are errors; `extern` and a function declaration still
  take the visible linkage.
- 6.2.7p2: block-scope `extern` declarations are compared with the file's
  and with each other's (`int x; ... extern long x;`).
- 6.9p3, p5: a second definition of an object or function is an error
  (it reached the assembler as a duplicate symbol), except beside a GNU
  inline-only (`extern inline` under gnu_inline) body.
- Tags (6.7.2.3): redefining a struct/union/enum in one scope, and naming a
  tag as the wrong kind (`struct S; union S *p;`), are errors.
- Storage classes: `_Thread_local` on a function or typedef, or alone at
  block scope (it was silently an automatic variable); `auto` at file scope;
  `register` at file scope without an asm register name (gcc's global
  register variables stay); `static` on a block-scope function.
- `for` loop declarations may not declare a typedef or a tag (6.8.5p3).
- A variably modified typedef may not be redefined (6.7p3).
- A function may not return an array or a function (6.7.6.3p1), and
  `restrict` qualifies only a pointer to an object type (6.7.3p2), including
  through a typedef and on a parameter -- the TODO.md entry is closed.
- gcc's warnings for `inline`/`_Noreturn` on an object, `inline main`, and
  an untagged struct/union declaring nothing.
- A file-scope array defined without an extent (6.9.2p2) is judged at the
  end of the unit: a later declaration in any scope completes it, every
  declarator takes the final type (so storage is the full size), and one
  still incomplete there is one element with gcc's "assumed to have one
  element" warning, once. An extent from a block-scope `extern` after the
  definition is gcc's "completed incompatibly" error.

The new check found a real duplicate in codegen_float16_mega, renamed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each of these compiled; several into wrong code.
- Designators (C17 6.7.9p7): `.y` naming no member was dropped with its
  value. Every designator chain is now resolved against the object it
  initializes, following nested lists through designators and by position
  (until brace elision makes the position unclear), for declarations and
  compound literals alike; a field designator on a non-aggregate and an
  index on a non-array are reported too.
- `s->a` on a structure (6.5.2.3p2) loaded through the structure's bytes;
  it is now gcc's "invalid type argument of '->'". An array still decays.
- Casts (6.5.4p2-4): to a structure, from a structure or union, and between
  a pointer and a floating type, with gcc's wording. A cast to the
  operand's own structure type, qualifiers aside, is gcc's extension and
  is accepted.
- Assigning a structure or union with a `const` member at any depth
  (6.3.2.1p1).
- Bit-fields: `&`, `sizeof`, `_Alignof` and `offsetof` of one; an
  `_Atomic` bit-field; a `_Bool` bit-field wider than 1.
- `_Generic` associations of function or incomplete type; `_Alignof` of an
  incomplete type (gcc's `void` extension stays; a VLA type is complete); `_Atomic(T)` of a
  qualified or atomic `T`; `_Alignas` weakening a member's alignment, or
  past the 2^28 ceiling.
- `static`/qualifiers only in the outermost dimension of an array
  parameter, whose name may be parenthesized (`int (a)[static 3]`); a duplicate member reached through an anonymous struct/union;
  an unnamed `void` among other parameters.
- Literals: `''`; a UCN past U+10FFFF; `u8` next to a wide literal, in either order; a
  string longer than its array (gcc's warning; filling it exactly is C).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- C17 6.7.6.2p4: `[*]` is allowed only in a prototype's parameter list. In
  a function definition the parameters are in the body's scope, and
  `void g(int n, int a[*]) {}` compiled; it is now gcc's error. A `[*]` in
  a nested prototype (a parameter that points to a function) stays legal.
  Each parameter list records its own `[*]`, so a function returning a
  function pointer is judged by its own parameters, not the return type's.
- A struct, union or enum first declared inside a parameter list has
  prototype scope, so no later declaration can name the same type; gcc
  warns, and c17 now does too, for tagged and anonymous ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…odifiers); #line maps positions

Builtins:
- `__builtin_fabs` and `__builtin_copysign` in their `_FloatN` and `q`
  spellings (`f32`, `f64`, `f128`, `q`), and `__builtin_infq`,
  `huge_valq`, `nanq`, `nansq`; on x86-64 the binary128 ones call libm's
  `fabsf128`/`copysignf128` (the backend has no XMM sign-bit form for it,
  and crashed). `__builtin_sqrtf128` and `__builtin_fmaf128` call libm.
  The f128 and q forms exist only where `_Float128` does.
- `__builtin_FILE()`, `__builtin_LINE()`, `__builtin_FUNCTION()`.
- `__builtin_expect_with_probability`, with gcc's check of the probability.
- `__builtin_dynamic_object_size`, answered as `__builtin_object_size`.
- x86-64 `__builtin_cpu_init`, `__builtin_cpu_supports`, `__builtin_cpu_is`
  (parse/cpu_builtin.rs), reading libgcc's `__cpu_model` and
  `__cpu_features2`; the tables hold every name gcc 13 accepts, with the
  encodings read off its own lowering.
- x86-64 asm operand modifiers `%z` (size suffix), `%p`, and `%x`/`%t`/`%g`
  (a vector register's XMM/YMM/ZMM name).
- All registered for `__has_builtin`; BUILTIN.md updated.

`#line`: it renumbered only `__LINE__`/`__FILE__`. It now establishes the
same mapping as a `# N "file"` linemarker, per physical stream (so a file's
mapping survives an `#include` in it): diagnostics read `renamed.c:77`, as
gcc's do, and so do the position builtins and the debug line table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…imals compile correctly; K&R, void and incomplete-type constraints enforced

Wrong code and an internal error fixed:
- A braced scalar initializer for a member of an automatic struct
  (`struct S s = {{1}, 2}`) reached the linearizer as a list and
  crashed. The initializer walker now strips scalar braces everywhere,
  warning on extra levels and excess elements as gcc does.
- A qualifier on an array typedef (`typedef int A[3]; const A x;`) was
  dropped; it now qualifies the elements (C17 6.7.3p10).
- A decimal constant above LLONG_MAX wrapped to a negative long long;
  it is now __int128 with gcc's warning.
- An identifier-list definition after a prototype read its parameters
  at the wrong type; argument count and promoted types are now checked
  against the prototype.
- A typedef name of a struct in a member list was taken as an anonymous
  member and changed the layout; it declares nothing, as in gcc.

Constraints now diagnosed:
- K&R declaration lists that name an unlisted, repeated or void
  parameter. An undeclared one warns "defaults to 'int'".
- Casting a void expression, or passing one as an argument
  (__builtin_va_arg_pack excepted).
- Arithmetic, ++/-- and subscripting on a pointer to an incomplete
  struct, union or enum.
- An incomplete return type in a definition or at a call.
- A function definition through a typedef.
- Casts between complex and pointer types.
- `void f(const void)`.
- An incomplete \u/\U universal character name.
- Excess elements in union and nested initializers, and nested scalar
  initializers of the wrong type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cc 7.5

C23's interchange and extended floating types (TS 18661-3) were aliases
of float/double, or absent. They are now distinct types, as in gcc:
- A FloatClass (standard / interchange / extended) on Type is part of
  the interning key and of compatibility, so `_Float32` is not `float`
  in _Generic or __builtin_types_compatible_p. The kind stays the
  format's, so layout, ABI and code generation are unchanged.
- Usual arithmetic conversions follow gcc for two names of one format:
  interchange over standard over extended (`_Float32 + float` is
  `_Float32`, `_Float32x + double` is `double`).
- `_Float32` is not promoted through `...`.
- `long _Float64` is now an error; the specifier tally used to record
  it as `double`.
- `_Float32x`/`_Float64x` keywords and `f32x`/`f64x` literal suffixes.
  `_Float64x` is long double's format and does not exist on Apple
  aarch64.
- f32x/f64x forms of the inf/huge_val/nan/nans/fabs/copysign builtins.
  The f32/f64 forms and library prototypes now use the _FloatN types.
- __FLT32_*, __FLT64_*, __FLT32X_* and __FLT64X_* predefines.
  __LDBL_*, __FLT128_* and these families are all generated from one
  per-format FormatLimits table.
- float.h exposes the FLTN_* families only under
  __STDC_WANT_IEC_60559_TYPES_EXT__, as gcc does.
- tgmath.h dispatches _FloatN arguments to glibc's sqrtf32 etc. where
  declared, and otherwise to the same-format standard function.

With that, __GNUC__ moves from 6.5 to 7.5. glibc now takes its
native-_FloatN paths: __MATH_TG with `_Float32:` beside `float:`, and
no typedefs or builtin macros from bits/floatn.h. 8.0 would need
__builtin_tgmath and the nonstring attribute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t, lowered lane by lane

vector_size types were storage only: every use as a value was an
error. They are now values, with gcc's rules (measured against gcc 13):
- Arithmetic and bitwise operators, shifts, unary - + ~, ++ and --,
  comparisons and ?:.
- A comparison yields a signed integer mask vector (-1/0). The mask is
  gcc's "opaque" type: it also assigns to any vector with integer lanes
  of the same shape.
- A scalar operand is spread across the lanes when the lane type holds
  it exactly. Otherwise c17 reports gcc's truncation or "cannot convert
  value to a vector" errors.
- Casts reinterpret bits between same-size vectors and integers.
- Vectors no longer decay to pointers. A vector is qualified as a whole.
- Diagnostics spell a vector type `__vector(N) T`.

Lowering: a vector travels by address, as a complex value does. Each
operation reads its operands' lanes, computes each lane with the scalar
opcode and stores it into a frame temporary. Assignment, initializers
(locals, members, array elements, unions), ternaries and subscripts of
rvalues all take the address path, so nothing below the linearizer sees
a vector. Output matches gcc at -O0/-O1/-O2 on x86-64 and on aarch64
under qemu.

Also fixed:
- A vector_size among the declaration specifiers now applies to the
  type they name: every declarator gets it, and a function returns the
  vector. It used to reach only the first declarator, or the function
  type itself.
- A vector value initializing a vector member or array element is no
  longer read as brace elision. The parser's walker now uses the shared
  elision predicate instead of its own copy.

Still rejected, with a message saying so: passing or returning a vector
(needs the vector ABI), plus __builtin_shuffle and
__builtin_convertvector.

Torture: 45 vector tests leave the skip list and pass. The harness
fixes:
- `dg-do ... { target <list> }` is now honoured, so other targets' tests
  are skipped. aarch64 was building x86 asm tests, and x86 was building
  aarch64 ones.
- New named aarch64 skip for pr27528, which needs non-PIC code; gcc
  rejects it under PIE too.

Both baselines are re-recorded. The header sweep, CPython (run=41,758
SUCCESS) and sparse are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…assified; x86 vector alignment matches gcc

The calling conventions now carry vector_size values. At each call
boundary a vector travels as a "carrier", a type whose own convention is
gcc's for the vector (new Abi::vector_carrier):
- SysV and AAPCS64:
  - 16 bytes as binary128: one XMM (SSE+SSEUP) or one Q register.
  - 8 bytes as double: one XMM, or D.
  - Integer-lane vectors of 4 bytes or fewer as the unsigned integer of
    that size.
  - Vectors over 16 bytes as a same-size, same-alignment struct (interned
    with the vector type): memory or by reference, returned through the
    hidden pointer.
  - SysV: a one-lane `float` or `double` vector as a struct holding it --
    MEMORY class, as gcc classes it and any struct holding one.
  - Darwin, whose compiler is clang: an integer vector of 4 bytes or fewer
    is passed as above but returned in V0, one lane in its low bits and
    several widened to fill D0 (Abi::vector_return_carrier,
    vector_return_widened).
- Win64: 8 bytes or fewer in a general register; 16 by reference and
  returned in XMM0, as __int128.
- Every classifier delegates a vector to its carrier, so va_arg agrees
  with the caller.
- The linearizer converts between a vector's address and its carrier for
  arguments, parameters, returns, call results and va_arg (including the
  Microsoft va_list). Neither backend changed for this.
- Parameters of vector type are no longer adjusted to pointers.
- Only a one-lane `float` vector on aarch64 stays refused: gcc passes it
  like no type c17 has.

Structs holding vectors:
- SysV: a vector member's eightbytes are classified by its carrier;
  struct { v4si } is one SSE+SSEUP register.
- AAPCS64: Homogeneous Short-Vector Aggregates use new
  HfaBase::ShortVector64/128, homogeneous only with each other.

Layout fix: on x86-64 gcc aligns a vector to its own size (up to 2^28)
in structs and when passing it, while _Alignof answers at most 16. c17
had taken the _Alignof answer for the layout, so struct { char c; v8si v; }
placed v at 16 where gcc places it at 32. The layout now matches, and
_Alignof still answers 16 (TypeTable::alignof_value).

Also: `mode` written with `vector_size` among the specifiers now sets the
element width before the vector is built. It used to replace the vector.

Verified: every gcc/c17 pairing as caller and callee matches gcc at
-O0/-O2 on x86-64 and on aarch64 under qemu, covering register and
stacked vectors, varargs, structs of vectors and ms_abi (new
codegen/vector_abi tests). 40 more torture tests leave the vector skip
list; six remain (__builtin_shuffle/shufflevector/convertvector, and one
4-byte float vector). Both baselines are re-recorded. The header sweep,
CPython (SUCCESS) and sparse are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ector; __builtin_ilogb

The three gcc builtins over GNU vectors, with gcc's constraints in gcc's
words (new parse/vector_builtin.rs), lowered lane by lane:
- __builtin_shuffle(a[, b], mask) (ExprKind::VectorShuffle with a mask):
  - Each lane is loaded at the run-time index, taken modulo the lanes
    the operands hold.
  - For two operands, a select chooses between a and b.
- __builtin_shufflevector(a, b, i...) (constant indices, -1 for any
  lane):
  - The operands may differ in length.
  - The result has one lane per index, a power of two of them.
- __builtin_convertvector(v, T) (ExprKind::ConvertVector): each lane
  converted as a cast does.

Also __builtin_ilogb / ilogbf / ilogbl, which pr108892 needed.

Output matches gcc at -O0 and -O2 on x86-64 and on aarch64 under qemu.
Five more torture tests leave the vector skip list; the one left is a
4-byte float vector passed by value. Both baselines are re-recorded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…3..-msse4.2; vector asm operands; narrow-lane compare fix

Intrinsic headers, all written in C over GNU vectors and c17's vector
builtins. There is no __builtin_ia32_*/aarch64_*, and no text from
gcc's or clang's headers.
- x86-64:
  - mmintrin.h, xmmintrin.h, emmintrin.h, pmmintrin.h, tmmintrin.h,
    smmintrin.h, nmmintrin.h and popcntintrin.h. These are every name
    gcc 13 defines there, including PCMPxSTRx in every _SIDD_ mode and
    CRC-32C.
  - immintrin.h stops at SSE4.2.
  - Also x86intrin.h (__rdtsc and bit helpers) and mm_malloc.h.
  - xmmintrin.h brings emmintrin.h, as gcc's does (mesa relies on it).
- aarch64: arm_neon.h, 3164 intrinsics across all 12 element types and
  the x2/x3/x4 structs. The estimate instructions are bit-exact.
- Registered per architecture in builtin_headers.rs, so on another
  target the name is not found, as with gcc.
- Checked differentially against gcc's headers on the hardware (x86-64)
  and under qemu (aarch64): byte-identical over about 2M result lines at
  -O0 and -O2. Five self-checking programs with gcc-derived expected
  values are now tests (codegen/simd/).

Driver:
- -msse3, -mssse3, -msse4.1, -msse4.2/-msse4, -mpopcnt, their -mno-
  forms and -march= (every 64-bit CPU gcc 13 names, and native) are
  accepted. They define __SSE3__ .. __SSE4_2__ and __POPCNT__ as gcc does
  (target::X86Isa): the last -march= gives the base, only the last of -mX
  and -mno-X counts, enabling X brings what it needs and disabling it
  drops what needs it, and SSE4.2 brings POPCNT unless an option named
  POPCNT.
- Every intrinsic is available whatever the flags; only the macros
  follow them.

Compiler bugs the header work found, fixed:
- Vector asm register operands:
  - An "x"/"v" (x86) or "w" (aarch64) operand of vector type was passed
    as its address in a general register and read back 8 bytes wide.
    Operands now travel as the vector's carrier.
  - Both register allocators class those pseudos as floating.
  - The 16-byte moves on both sides are full XMM/Q moves.
  - An unmodified aarch64 vector operand prints as vN, as gcc does;
    %d0 and the other modifiers still give the width forms.
- An 8- or 16-bit lane compare on x86-64 now extends both operands
  itself, as its signedness says. Inlining made one operand the constant
  -128, kept zero-extended in its slot, so `v < -128` was true in every
  lane (_mm_cvtps_pi8 at -O2).

Real-world check: ggml's quantization and SIMD dot products (llama.cpp
ggml-cpu x86 quants), built by c17, give output identical to gcc's at
the SSE2, SSE3, SSSE3 and SSE4.2 levels. mesa's util/ SSE users compile
wherever gcc's do.

Gates: fmt, clippy, release and debug suites, torture on both targets,
header sweep, CPython (SUCCESS) and sparse are all unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two vectors whose integer lanes differ in signedness compared at the
left operand's lane type, so `a < b` with a signed and b unsigned
compared signed while `b > a` compared unsigned. gcc compares unsigned
whenever either side's lanes are unsigned; arithmetic still computes at
the left operand's lane type, which is the result's.

Tests: IR unit test (SetB/SetA, not SetLt/SetGt, and signed divide
kept); runtime test of comparisons, divide, shift and result types,
values from gcc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… continuation

The initializer rule of C17 6.7.9p17-20 -- which subobject each element
lands in -- had partial copies that disagreed, giving wrong code and
false errors:

- A string literal never started brace elision, so `"abc"` was given a
  whole `struct { char s[4]; ... }` member and the next value was checked
  against the wrong member (`{"abc", 2.5, 0}` refused). At static storage
  `{L"ab", 3, L"c", 4}` was refused as not constant.
- Elision counted a slot's scalar fields, but a string fills a whole char
  array and a braced list a whole subaggregate: `{"abc", 7, {"de", 8}}`
  for two `struct In` members put everything in the first (wrong values
  at run time).
- The parser's walk never tracked elision in arrays: `struct Pt
  g[2][2] = {1, 2, {.y = 5}}` checked `{.y = 5}` against `struct Pt[2]`.
- Positional elements after a designator chain restarted at the outer
  list: `{.a.x = 1, 2, 3}` gave `1 0 2 3` (gcc: `1 2 3 0`), and
  `[1][0] = 5, 6` lost the 6. A continuation into an anonymous member
  given a braced list was zeroed.

Now `ObjectWalk` (parse/ast.rs) is the only statement of the rule. The
parser spells each chain continuation out once as explicit designators
(`Designator::Member(i)` names an anonymous member by position), so the
parser's checks, array sizing and the linearizer read landing places
instead of tracking the cursor at depth. Brace elision
(`brace_elision_span`/`designated_span`) and excess counting
(`initializer_list_end`, now also with designators present) use the same
walk; a string elides unless its slot is an integer-element array.
`count_scalar_fields` is deleted. A GNU range continues in its last
element only, as gcc does.

Tests: parse unit tests for the spans, the walk and the spelled chains;
c99/brace_elision.rs runtime cases (static and automatic, -O0/-O2,
aarch64); constraint_sweep accept/warn cases, including excess elements
after a chain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jgarzik
jgarzik merged commit ca7f07b into main Oct 3, 2026
20 checks passed
@jgarzik
jgarzik deleted the updates branch October 3, 2026 22:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants