Skip to content

cc Updates - #725

Merged
jgarzik merged 32 commits into
mainfrom
updates
Oct 6, 2026
Merged

jgarzik merged 32 commits into
mainfrom
updates

Conversation

@jgarzik

@jgarzik jgarzik commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

No description provided.

jgarzik and others added 30 commits October 6, 2026 00:55
The harness skipped about 250 tests by name across twenty lists
(post-C17, GNU-only, nested functions, __label__, vector ABI, 64-bit
frames, gcc-internal builtins, "gcc fails too", ...), and skipped every
test whose dg-options carry a -m flag. Both are gone: -m flags are
forwarded like any other option, and only the suite's own directives (.x
files, dg-skip-if, dg-do target selectors) keep a test from running.
TEMPLATE_NOT_ASSEMBLED stays -- it stops two tests at -S, as gcc's
harness does, rather than skipping them.

All four sub-suites, default levels:
  x86-64   7381 pass / 7702 (95.8%), 283 CFAIL, 19 RFAIL, 19 suite skips
  aarch64  7372 pass / 7702 (95.7%), 285 CFAIL, 17 RFAIL, 28 suite skips
No regressions; the baselines gain 45 (x86-64) and 43 (aarch64) entries
that the lists had hidden: __label__, vectors across calls, va-arg-7/8,
the *printf-chk tests and the link-at--O0 group.

DECISIONS.md's "Torture tests skipped by decision" section and TODO.md's
reference to NEEDS_64BIT_FRAMES described the deleted lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One SKIP_BY_NAME table (`<sub-suite>/<name>  <target>  <reason>`) replaces
the deleted per-category lists, holding only what is out of scope or what
gcc rejects too: nested functions (including the non-local-goto
__label__ tests), VLA struct members, post-C17 features, -fgimple,
another target's assembly, 64-bit frames, __builtin_apply, #cpu
assertions, -mavx, -finstrument-functions, and gcc-rejected tests
(dll, pr117432, pr123864 under gcc 14; pr27528 on aarch64 only). Two
tests on the old lists that pass are not in it. Old "gcc fails too"
claims for bcp-1 and eeprof-1 were stale: gcc 13 passes both.

x86-64 7380 pass, 105 fail, 217 skipped; aarch64 7371 pass, 105 fail.
uabs-2@-O0 (C2y) leaves the baselines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under -fgnu89-inline (or gnu_inline), gcc accepts an `extern inline`
body followed by a `static` definition of the same name -- the static
one being the function -- where c17 reported "static declaration of
'bar' follows non-static declaration" (gcc.c-torture compile/20021120-1,
-2). Not caused by d095fa1; the bug predates it.

LinkedName tracks gnu_extern_inline: set by an `extern inline`
declaration under GNU semantics, cleared by a plain `inline` declaration
or a real definition. While it is set, a static declaration takes the
name over (internal linkage, its own definedness) instead of drawing
6.2.2p7's error; the 6.2.7p2 type check (now check_linked_type) still
runs. An inline-only body needs external linkage, so `extern inline`
after a `static` declaration is an ordinary emitted static function.
Matched against gcc 13 on 25 orders in -fgnu89-inline, gnu_inline and
C99 modes.

Also fixed on the way: `static int f(void); int f(void) {..}` emitted a
global f; FunctionDef.is_static now comes from the function's linkage
(C17 6.2.2p4/p5), giving a local symbol as gcc does.

Torture: compile/20021120-1/-2 pass on both targets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A filtered run that picked only tests the baseline does not hold yet --
checking a fix for tests that never passed -- died with "FATAL: the
baseline and this run have no test in common" although every test
passed. The fatal check exists to catch a changed tag format, which an
unfiltered run still does; a filtered run now notes that nothing can
regress and reports the newly passing tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C17 6.9.2p1 (and 6.9.2p4's `extern int i3 = 3;`): c17 skipped every
extern declarator, so `extern int y = 5; int *p = &y;` left y undefined
at link, and `static int x; extern int x = 7;` read 0. An initialized
extern declarator now takes the definition path, with internal linkage
when a prior static declaration gave it (6.2.2p4).

Two defects found on the way: a tentative definition after an
initialized one pushed a second global (`int y = 5; int y;` failed to
assemble -- define_global now folds it in), and
`struct T; extern struct T t = {0};` compiled silently (now gcc's
storage-size error).

Diagnostics use gcc 13's wording: file scope warns "'y' initialized and
declared 'extern'", block scope errors "'z' has both 'extern' and
initializer"; a second definition is "redefinition of 'y'".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
c17 rejected constructs gcc accepts with a warning or only flags under
-pedantic. diag::Pedantic now owns both kinds of gcc pedwarn: pedwarn
(silent, warning under -pedantic, error under -pedantic-errors) and the
new pedwarn_default (warning, error under -pedantic-errors); -w and
system headers keep either hidden, as in gcc.

- A conditional with one void arm: pedwarn "ISO C forbids conditional
  expr with only one void side"; the result is still void, so using it
  as a value stays an error (torture pr26725, pr46309, 20000211-1).
- An external inline definition using, or declaring a modifiable, static:
  pedwarn_default in gcc's words ("'x' is static but used in inline
  function 'f' which is not static"), off the -fpermissive list
  (torture pr38857).
- Swept against gcc 13 (default / -pedantic / -pedantic-errors), keeping
  errors gcc 14 makes errors (implicit int, implicit function
  declaration, return mismatch): a member list missing its last ';', an
  out-of-range escape, an integer constant too large (C and #if), #line
  out of range, `int;` and `static;`-style empty declarations, an
  unnamed struct declaring nothing, a NUL between tokens, a label at the
  end of a compound statement, an enumerator outside int.
- #line expanded its operand as #if does, so `#line abc` became line 0;
  it is now ordinary text and an error, as in gcc.

Torture: 8 more passes per target, no regressions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner's decision: match gcc. gcc 13 -O2 deletes printf(""),
fprintf(fp, ""), fputs("", fp), printf("%s", ""), fprintf(fp, "%s", ""),
vprintf("", ap), vfprintf(fp, "", ap), their _chk and _unlocked forms,
and a choice between empty strings, when the result is unused; C17
7.21.2p4 has such a write orient the stream, which c17 kept. The stdio
fold (ir/libcall_fold/stdio.rs) now discards them (writes_nothing,
Folded::Discard), still evaluating arguments with side effects; a used
result and puts("") keep the call. c17 still folds stdio only from -O1
up, so -O0 keeps every call (gcc folds at -O0 too).

Torture: builtins/printf, fprintf, fputs and execute/printf-chk-1,
fprintf-chk-1, vprintf-chk-1, vfprintf-chk-1 now pass at -O2 on both
targets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gcc's lightweight non-local goto, which every gcc.c-torture builtins/*-chk
test reaches through chk.h. The existing Setjmp/Longjmp opcodes gain a
mode, JmpKind::{Library, Builtin}, so all returns-twice handling applies
unchanged.

- setjmp stores the frame pointer, the resume label (internal to the
  instruction's code, so no pass can remove it) and the stack pointer
  at words 0, 1, 2 -- gcc's non-CET layout on x86-64 and aarch64 --
  returns 0, and returns 1 at the resume label after re-latching an
  over-aligned frame's base register. longjmp loads all three before
  touching the frame pointer, restores fp and sp and jumps.
- Register allocation: the setjmp clobbers every allocatable register
  but its result; the function saves every callee-saved register in its
  prologue, as gcc does; values live across a setjmp keep their slots
  to the function's end.
- Locals of a function with a builtin setjmp stay in memory (gcc models
  an edge from every call to the receiver: pr60003), and such a
  function is never inlined, always_inline included.
- Prototypes as gcc's; a longjmp value other than constant 1 is gcc's
  error.

Torture: +33 per target (built-in-setjmp, pr60003, pr64242, pr84521,
complex-6, 20011029-1, pr89280, pr82337, and the *-chk tests at -O0);
the *-chk tests still fail at -O2 on __builtin___*_chk object-size
folding, which c17 does not do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Debug builds panicked on define_global's "tentative definition type
mismatch" assert for shapes 1de82ff made reachable -- `int u; extern int
u = 3;`, `static int x; extern int x = 7;` and the like: the parser
copied the storage class onto the declarator's type at every level, so
`static int`, `extern int` and `int` were three TypeIds for one object.
Release builds were unaffected (the assert compiles out), which is how
it got past 1de82ff's release-only validation.

bind_declarator now gives an object of static storage duration
without_decl_specifiers(typ) as its type (qualifiers kept), and
complete_tentative_arrays writes back the same. define_global takes the
TypeTable, debug-asserts the type carries no specifiers, checks a
tentative definition against its completion by compatibility rather than
identity, and records the definition's type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C17 6.2.7p3-4: after a later declaration the identifier has the
composite type. c17 replaced a symbol's type only when an unsized array
sat at its top level, so `int (*q)[]; int (*q)[3];` kept int (*)[] and
`sizeof *q` was 0 (gcc: 12); a block-scope repeat of an extern was
dropped; a function definition never composed with its declaration.

The existing TypeTable::composite_type now serves all three paths:
composite_with_prior_declaration composes any compatible pair of
declarations with linkage (incompatible ones are left for
check_redeclaration to report), declare_in composes a reused symbol's
type, and define_function gives the function symbol the composite while
the body is checked against the definition's own type. A block-scope
`int h();` under a file-scope prototype now has the prototype, as in gcc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
__builtin_issignaling is a new FpTest::IsSignaling: the operand is never
converted (a conversion quiets), stored at its own type and tested as
integer bits -- gcc's test for binary16/32/64, binary128 with the low
word folded into the high one, and for x87 also the pseudo-infinity /
pseudo-NaN / unnormal encodings gcc and glibc count as signalling. No
floating compare, no x87 load of a float or double. A constant operand
is an integer constant expression (constexpr.rs). Non-floating
arguments get gcc's error.

-fsignaling-nans / -fno-signaling-nans are accepted, last wins, and the
positive form defines __SUPPORT_SNAN__ as gcc does. c17's float folds
already refuse non-finite or signalling operands, so none quiets an sNaN
gcc would keep under the flag.

Torture: the issignaling tests pass on both targets (14 runs); the
bfloat16 test (no __bf16) and the _Float128x one (no target has it, gcc
included) join SKIP_BY_NAME.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A static initializer may now be the difference of two label addresses of
one function, plus or minus an integer constant, stored to an integer of
1, 2, 4 or 8 bytes. glibc's vfprintf builds its jump tables this way and
dispatches with `goto *(&&base + table[i])`. Before this change c17 said
"non-constant pointer expression in global initializer".

- linearize_label_diff.rs folds the expression into a sum of labels with
  coefficients and a byte addend. It follows +, -, unary -, casts to a
  pointer or to an integer at least as wide as the object, and integer
  constants, using C pointer scaling (a void/char difference counts
  bytes). What is left must be one label minus another. That becomes the
  new Initializer::LabelDiff { end, start, addend }; `&&a - &&a` folds
  to its constant, as gcc does. Every label is taken, so it is checked
  to exist and its block stays alive. The function is marked
  saves_label_in_static, so it is never inlined or cloned, matching gcc.
- The backend emits it as `.byte/.short/.long/.quad end - start +/- k`.
  Directive::LongDifference is now SymDifference { size, end, start,
  addend } and DWARF uses it too. Labels use the target's private
  spelling, `.L` on ELF and `L` on Mach-O. Mach-O `L` labels are not
  atoms, so llvm-mc resolves the difference for arm64 and x86_64. It
  needs no load-time relocation, so a const table goes to .rodata.
- gcc's diagnostics are matched. A _Bool, floating or __int128 object is
  "initializer element is not computable at load time". A pointer object
  stays a non-constant initializer. A label of another function is
  "used but not defined".
- When a static function that saves a label in a static object is
  removed as dead, the static objects that name its labels are removed
  with it, and so are statics that name those objects. Before, they
  stayed and named labels nothing defined. That broke the assembly for
  `&&a` tables already, and for the new difference tables too.

gcc 13 gets the sign of the addend wrong in `(&&b + 4) - &&a` (it emits
`.Lb-.La-4`). c17 emits the correct value, and the e2e test checks it
against the runtime difference.

Torture: compile/labels-3 and execute/pr70460 now pass at -O0 and -O2 on
x86-64 and aarch64 (+4 per target baseline).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Builtins gcc made for its own lowering that code also calls:

- __builtin_stack_save / __builtin_stack_restore with gcc's prototypes,
  lowered to the existing StackSave/StackRestore opcodes; locals are
  frame-pointer addressed and epilogues reset sp from fp, so a program's
  restore cannot break VLA bookkeeping.
- __builtin_clear_padding: ir/padding.rs records which bits of each byte
  belong to a member and zeroes the rest inline -- whole padding runs as
  a memset, a byte shared with a bit-field by load/and/store, a VLA
  pointee by a loop. gcc 13's rules: union padding is padding in every
  member, x87 long double's 6 bytes, an array argument clears its first
  element only; gcc's errors for non-pointer, incomplete, const and
  flexible-array pointees.
- __builtin_cexpi{,f,l} as cexp{,f,l}(+0 + xi) -- bit-identical to
  cexp(I*x) -- and __builtin_cpow{,f,l} as the library cpow.

Torture: compile/20071117-1, pr98087, pr110266, pr54428 pass on both
targets; x86-64 has no compile failures left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On x86-64 an unnamed non-zero-width bit-field raised the alignment of
its struct or union and reserved its type's access window, so `struct {
char a; int :5; char b; }` was 4/4 where gcc's is 3/1 -- a different
ABI for any such struct crossing to gcc-built code. The System V psABI
says unnamed bit-fields' types do not affect the aggregate's alignment.

One rule, TypeTable::unnamed_bitfields_align_aggregate(): only aarch64
other than Apple (AAPCS64) lets them; elsewhere an unnamed field is
placed by the unit rule but adds no alignment and spans only the bytes
its bits touch, in structs and unions. Apple arm64 follows clang
(DarwinAArch64TargetInfo: UseZeroLengthBitfieldAlignment = false), which
also changes a zero-width field there: `{ char; int :0; char; }` is now
5/1 (from clang's source; no Apple run here).

Also fixed: attributes after an unnamed field's width failed to parse;
a typedef's raised alignment did not place a bit-field; a written
alignment on a zero-width field was ignored; an unnamed field wider
than its carrier was rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A struct or union declared with a byte order other than the target's
stores its scalars byte-reversed. The order is a type property:
TypeModifiers::REVERSE_ORDER goes on the member types gcc counts as
scalars (integers, enums, floating and complex types, bit-fields, and
elements of arrays of them), never on pointers, vectors or nested
aggregates. Compatibility ignores it; lvalue conversion and typeof drop it.

Linearizer::emit rewrites every Load/Store at a reversed type into a
native access plus an explicit Bswap16/32/64 (floats and 16-byte scalars
via a frame temporary), so no later pass sees the attribute and load
forwarding/memloc stay correct. Bit-fields keep their layout and are
numbered from the most significant bit, as gcc does; static images hold
the reversed bytes; complex halves are reversed individually.

Parser: the attribute in all three specifier positions, gcc's variant
type on a reference (typedef struct S __attribute__((...)) T), and
#pragma/_Pragma scalar_storage_order big-endian|little-endian|default,
carried as a LayoutPragma marker beside #pragma pack and reproduced by -E.
Diagnostics in gcc's wording: "cannot take address of scalar with
reverse storage order", the array-address warning, bad/missing argument
errors, "attribute ignored" off a specifier, the pragma warnings, and
"sorry, unimplemented: reverse storage order for XFmode".

Torture: execute/20230630-2 and -4 now pass on host and aarch64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three defects behind bcp-1, which gcc passes at every level:

- A string literal's start ("hi", &"hi"[0], "hi"+0, L"wide", through
  representation-keeping casts) answered 0; gcc answers 1. The parser's
  constant_p helper now applies gcc's order: constant -> 1, string
  literal start -> 1, pointer/array/struct/union/function type or -O0 ->
  0, else deferred to sccp.
- The pre-inlining sccp round answered an unproved ConstantP of a
  parameter with a final 0, so inlined copies (bad3(1), bad6(1)) could
  never become 1. sccp::run_before_inlining leaves it open; ConstantP of
  a float constant now answers 1 too.
- A deferred operand had to pass is_pure_expr, the speculation question,
  which rejects division, FP-raising operations and dereferences -- but
  ConstantP's operand never runs. is_pure_expr takes an Evaluation mode
  (Speculative / Discarded); Discarded still refuses writes, calls and
  volatile reads.

New fold, as gcc does: a one-byte load at a constant offset into a
string literal or const char array folds to the byte (constglobal.rs,
via KnownGlobals' ConstBytes), so "hi"[0] is a constant once optimized.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
emit_ret loaded a stack-resident __int128 into X0/X1 with one ldp, and for
every other location moved the low half to X0 and zeroed X1. A constant
operand is an immediate, so `return (i128)(~(u128)0 >> 1);` returned
2^64 - 1 on aarch64 at -O0. Materialize both halves of an immediate.

Test: codegen_int128_return_of_a_constant_keeps_its_high_half (failed on
aarch64 at -O0 before the fix).

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

C17 6.3.1.4p1 leaves the conversion undefined. gcc's constant folder
saturates (maximum above the range, minimum or 0 below it, 0 for any NaN)
in every context, at -O0 too; c17 refused, so the run-time instruction ran
and x86-64 gave INT_MIN. gcc.c-torture execute/20031003-1 failed on the
host for this.

One rule, FloatVal::to_integer_saturating, now serves every folder:
- constexpr: enumerators, case labels, _Static_assert, bit-field widths,
  _Alignas and __builtin_constant_p take the saturated value. A new
  ConstScope::ArrayBound keeps refusing it, so the array is a VLA as in gcc.
- linearizer: a floating constant cast or implicitly converted to an integer
  type in code folds at every level (fold_float_to_integer). Implicit
  conversions that saturate warn as gcc does; -Wno-overflow silences.
- constfold::eval_fcvt (instcombine): FCvtS/FCvtU of a constant saturates.
  eval_fcvtf now also folds an infinity or quiet NaN, which raise nothing,
  so `float x = -INFINITY; (short)x` folds from -O1.

Run-time conversions stay hardware instructions. Known gap: a 128-bit
conversion becomes a libgcc call before the optimizer runs, so a constant
reaching one through a variable is still converted at run time.

Tests updated from "not folded" to gcc's answer: instcombine
an_out_of_range_conversion_is_left_alone (now ..._saturates) and
test_parser test_out_of_range_float_cast_is_not_an_integer_constant (now
split into a saturating test and an array-bound VLA test). New: e2e
float_to_int_saturation (gcc-validated on x86-64 and aarch64 at
-O0/-O1/-O2), constfold/float/linearizer/diagnostic unit tests.
DECISIONS.md and BUILTIN.md record the choice. Torture baseline gains
execute/20031003-1 at -O0 and -O2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A floating GNU vector of four bytes or fewer was refused at every call
boundary on aarch64 ("c17 does not pass or return this vector type on
this target"), and v2hf on x86-64 as well. gcc.c-torture ieee/fp-cmp-cond-1
failed on aarch64 because of it.

Each platform compiler has its own rule, probed with gcc 13 -O2 -S and,
for Darwin, clang's i32 coercion lowered by llc -mtriple=arm64-apple-macos:

- gcc AAPCS64: on the stack in an eight-byte slot whatever registers are
  left; every later general-register argument goes to the stack too (NGRN
  becomes 8) while the V registers stay available. Returned in W0
  (zero-extended at two bytes). Like no C type, so the carrier is a struct
  of its own (TypeTable::vector_stack_carrier) classed by a new
  ArgClass::Stacked, handled by param_layout, the caller's register
  assignment, the prologue tally and va_arg. The callee reads the
  parameter by address, so an inlined body reads through the address the
  call passes.
- clang on Darwin: passed as i32 (unsigned int carrier, four bytes on the
  stack), returned in V0: S0, or H0 for v1hf.
- gcc System V: v2hf is SSE class, the low four bytes of XMM0 both ways
  (float carrier), also as a struct member; one-lane shapes stay in memory.

gcc's own va_arg reads such a vector from the general save area while its
caller stacks it; c17's va_arg follows gcc's caller.

Tests: interop c17/gcc in every pairing on the host and on aarch64 under
qemu (arguments, returns, mixed with ints and floats, more than eight
vector arguments, struct members, inlined static callees), a variadic
aarch64 test (c17 both, gcc caller), asm tests for aarch64 Linux, aarch64
Darwin and x86-64, ABI and linearizer unit tests. The refusal tests now use
a three-lane vector, the one shape still without a carrier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Debian builds with _FORTIFY_SOURCE, so glibc's headers turn memcpy,
strcpy, sprintf and friends into __builtin___*_chk calls with
__builtin_object_size. gcc decides them at compile time; c17 called the
_chk functions and, since __builtin_object_size of a wrapper's parameter
folded to (size_t)-1 at parse time, checked nothing. The 15
gcc.c-torture builtins/*-chk tests failed at -O2.

- __builtin_object_size is deferred at -O1+ (ExprKind::ObjectSize ->
  Opcode::ObjectSize), as ConstantP is: ir/objsize.rs answers a
  placeholder once its object is known through copies, addresses,
  displacements, selects, phis and constant allocas (types 0/1 the
  largest arm, 2/3 the smallest); once the optimizer converges the rest
  answer unknown and the passes run to a fixed point again. -O0 answers
  unknown at once, as gcc. constexpr's deferred hook now serves both
  builtins.
- One _chk rule (ir/libcall_fold/fortify.rs, a 14-row table): an unknown
  size, a constant count or the longest of several that fits, a string
  plus terminator that fits, an empty append, or a printf format with no
  directive / "%s" of a known string becomes the plain call or block op;
  a provable overflow keeps the check, which aborts at run time.

Not matched (DECISIONS.md): type 1/3 through IR has no subobject info,
no alloc_size, no -Wstringop-overflow warning.

Torture: every non-skipped test passes on both targets (x86-64 7481,
aarch64 7472).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C17 6.8.5p3 lets the declaration in a `for` declare only objects with
auto or register storage. c17 already refused a typedef, a tag and the
static/extern/thread-local storage classes there, but accepted a
function declarator -- `for (int g(void);;)`, also through parentheses,
a typedef or an old-style list -- and an enumeration constant introduced
by the specifiers, `for (enum { A } e;;)`.

Both now get gcc 13's error, "declaration of non-variable 'g' in 'for'
loop initial declaration". The typedef case shares the same reporter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gcc 13 does not fold `1 << -1` or `1 >> -1`: it warns that the count is
negative and then refuses the expression wherever C requires a constant
-- "initializer element is not constant" for a static initializer at
file or block scope, and likewise for an enumerator, a `case` label,
`_Static_assert` and a file-scope array size. c17 folded the shift with
the hardware's count mask and accepted all of them. A count past the
width (`1 << 40`) and a negative value shifted (`-1 << 1`) remain
constants, as in gcc, and an unevaluated operand (`0 ? 1 << -1 : 2`)
is still fine.

The constant walk now declines a negative count, and the diagnostics
take gcc's words:
- a non-constant static initializer leads with "initializer element is
  not constant", keeping c17's note of which part is to blame;
- an enumerator: "enumerator value for 'E' is not an integer constant",
  at the enumerator, instead of "enum value must be constant";
- `_Static_assert`: "expression in static assertion is not constant";
- a `case` label blames the program ("not an integer constant
  expression") rather than the compiler's folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An array type recorded its extent as Option<usize>, so `int[]` and
`int[n]` were both None and interned to one TypeId. Nothing holding only
a type could say whether sizeof of it was a run-time value or a
constraint violation, and each consumer guessed: check_sizeof_expr_operand
looked only at a bare identifier and a flag on its symbol, so
`int (*q)[]; sizeof *q` (and sizeof through __typeof__, or of a flexible
array member) compiled to 0 where gcc 13 says "invalid application of
'sizeof' to incomplete type 'int[]'"; check_pointer_steps skipped every
unsized pointee, so `q++`, `q + 1` and `q[1]` stepped by zero.

The type now carries types::ArrayExtent -- Known(n), Variable or Unknown
-- in place of array_size, and every consumer matches on it:

- the declarator builds the extent it parsed directly (its private
  Extent enum, which already had the three cases, is gone);
- is_incomplete_array() answers "unknown size", and type_name_is_incomplete,
  the sizeof/_Alignof/va_arg/_Generic checks, flexible array members,
  tentative definitions, "array size missing" and linkage completion ask it;
  type_name_is_incomplete loses its `extents` argument, and with it the
  sizeof expression check its identifier-only special case;
- variable_array_levels() replaces unsized_array_levels for what counts
  size expressions (record_vm_extents, sizeof_type_is_runtime,
  vm_extent_count, parameter VM dims, clear_padding, the constant walk);
- composite_type takes a known size, then a variable one (C17 6.2.7p3);
  compatibility still only rejects two different known sizes;
- arithmetic on a pointer to an array of unknown size is refused with
  gcc's "invalid use of array with unspecified bounds"; a pointer to a
  VLA still steps.

A VLA is still spelled `[]` in diagnostics: the type does not hold its
size expression.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gcc.dg/sso-1.c: gcc 13 will not statically initialize any member of a
scalar_storage_order struct or union in the reverse of the target's
order with an address constant -- an object's or function's address, a
string literal, an address plus an offset -- whether the member is a
pointer (which gcc stores natively), an array of pointers, or an integer
holding `(long)&i`. It says "initializer element is not constant".
c17 accepted the pointer cases outright, and for an integer member said
"not computable at load time" at line 0, having no position for a
global.

A struct of pointers alone kept no trace of its order, since the order
lived only on the member types gcc reverses. CompositeType now records
`reverse_order`, set by the attribute, the pragma and a typedef variant
in the other order (a variant differing only in this is now a type of
its own). The static struct/union initializer refuses a member holding
an address when its aggregate is reversed -- unless the member is a
struct or union, or an array of them, which keeps its own order -- and
ast_init_to_ir refuses one for a reversed scalar at the expression's
position. Automatic objects, null and integer-valued pointers, and
nested native-order structs are still accepted, as gcc accepts them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f99cc2f's named_sizes_skip_weak_and_unbounded_objects set
Type::array_size = None, a field 51b8cf4 replaced with ArrayExtent
(the two were developed in parallel); it now interns
Type::array_of(char, ArrayExtent::Unknown). Both objsize test fixtures
also name their target instead of the host.

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

c17 accepted `short __attribute__((vector_size(6)))`, which gcc rejects at the
declaration. c17 then refused the type at every call boundary ("c17 does not
pass or return this vector type on this target"). Now the declaration is
rejected with gcc's wording, along with the related shapes gcc rejects:

- "number of vector components N not a power of two" (3 shorts, 3 doubles,
  384 ints, ...), on a typedef, an object or a member
- "zero vector size" for vector_size(0)
- "'vector_size' attribute argument value '-16' is negative"
- "vector size not an integral multiple of component size"
- "invalid vector type for attribute 'vector_size'" for a complex or _Bool
  lane, which c17 used to accept

Each remaining vector size is a register width or travels as an aggregate,
so Abi::vector_carrier and vector_return_carrier now return a TypeId instead
of an Option. The refusal (check_not_vector_value, at four parser sites)
is deleted, along with uncarried_vector_class, the linearizer's
`unwrap_or(vec)` fallback and System V's `None => Memory` eightbyte arm.

Making the refusal unreachable surfaced a second hole. Carriers were keyed by
the vector's written alignment, so `typedef v8si w __attribute__((aligned(64)))`
had no carrier and was refused. Vectors declared with `vector_size(32),
aligned(64)` or `vector_size(4), aligned(16)` were accepted, but on x86-64 they
started on their own boundary on the stack. Probing gcc 13 (-O1 -S) shows it
passes a vector as its main variant: a raised or lowered alignment moves no
argument (aligned(64) v8si after six longs and an int lands at offset 32, as a
plain v8si does). Carriers are now keyed by the natural alignment, and
vector_of interns them for the natural shape.

Tests: gcc-wording diagnostics for every shape on all three declaration
forms, a parser unit test for lane counts, ABI unit asserts that a written
alignment keeps the natural carrier, and a new interop test
(vector_abi_aligned_interop_host/_aarch64) of nine aligned shapes as
arguments and returns in every c17/gcc pairing. Before this change it failed
on x86-64 (a8si, t8si, w1sf, a1sf) and on aarch64 (w1sf). The two refusal
tests that used a 6-byte vector now check that a one-lane float vector and
both realigned shapes pass on aarch64 Linux, aarch64 Darwin and x86-64.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clang on Darwin coerces every vector of four bytes or fewer to i32
(AArch64ABIInfo::coerceIllegalVector). c17 did this only for floating
vectors. It passed v2qi, v1hi and v1qi as the unsigned integer of their own
size. In a register that makes no difference. On the stack Darwin packs an
argument at its own size, so once the general registers ran out these took
1 or 2 bytes instead of 4, and every stacked argument after them was
misplaced.

Evidence: clang's IR for the call, `declare void @take(i32 x 11)`, lowered
by `llc -mtriple=arm64-apple-macos`, stores the stacked arguments at [sp],
[sp, #4] and [sp, #8], and the callee reads them back at the same offsets.
c17 stored them at [sp], [sp, #2] and [sp, #4]. Linux is unchanged: gcc
gives each one an eight-byte slot.

Aapcs64Abi::vector_carrier now answers `unsigned int` on Darwin for every
vector of four bytes or fewer. Two consumers had relied on the carrier being
no wider than the vector:

- vector_to_carrier loaded the carrier's full width from the vector's
  address, reading past a one- or two-byte vector. It now loads the
  vector's own bytes and zero-extends them. This also covers the existing
  Darwin v1hf.
- An asm register operand took the ABI carrier as its type, so an "=r"
  output stored four bytes into a v1qi. It is now the unsigned integer of
  the vector's own size for every vector of four bytes or fewer. That also
  replaces the struct carriers that a v1sf operand used to get on x86-64
  and aarch64 Linux.

Tests: an asm test for aarch64 Darwin and Linux with eleven such arguments,
pinning the caller's stores and the callee's loads at 4-byte and 8-byte
strides (it fails without the fix: [sp, #8] is missing); a linearizer unit
test for the narrow load and the asm output store; an ABI unit assert of
the carrier per OS; and an interop test with thirteen arguments in every
pairing on the host and on aarch64 under qemu. On Apple arm64 the host
pairing is against clang.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under __attribute__((ms_abi)) gcc passes a one-lane floating vector (v1sf,
v1df, v1hf, v1tf) by reference to a copy, whatever its size. c17 passed it
by value in a general register, as the integer of its size.

Every shape of sixteen bytes or fewer was probed with gcc 13 -O2 -S, as
argument and as return value:

- v1sf, v1df, v1hf: the callee reads the argument through the pointer in
  its position (`cvttss2sil (%rdx)`, `pinsrw $0, (%rdx)`). A stacked one is
  a pointer in the stack slot (`movq 40(%rsp), %rax; cvttss2sil (%rax)`),
  and the caller passes the address of a copy. They come back in RAX.
- v1tf (and v1xf): by reference, and returned through the hidden pointer
  in RCX, not in XMM0, where v2df, v4si and v1ti come back.
- v2hf, v4hf, v2sf and every integer vector of eight bytes or fewer (v1qi,
  v2qi, v4qi, v1hi, v2hi, v4hi, v1si, v2si, v1di): by value in the integer
  position, returned in RAX. c17 already did this.
- A struct holding a v1sf follows the aggregate rule, by value. c17 already
  did this.

Win64Abi::vector_carrier gives a one-lane floating vector the struct that
wraps it (TypeTable::vector_wrapper_carrier), which classify_param sends by
reference (new TypeTable::is_vector_wrapper_carrier). The new
vector_return_carrier returns it as the unsigned integer of its size, or,
at sixteen bytes, as the wrapper through the hidden pointer.

Tests: an interop test against gcc-compiled ms_abi code
(codegen_ms_abi_small_vectors_interoperate_with_gcc) covers all sixteen
shapes as arguments, including by-reference ones past the fourth position
and a callee writing its copy, and as returns, in every pairing at -O0 and
-O2. A one-unit run at -O1..-O3 exercises inlining. Before this change the
gcc caller / c17 callee pairing failed at the first check. ABI unit asserts
cover the class of each one-lane width and of v2hf and v1si.

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

Fixes from review of the torture-tail series:

- An _Atomic member of a reverse-storage-order struct was read and
  written in native byte order. gcc rejects it as an operand anywhere,
  even unevaluated ("cannot take address of scalar with reverse storage
  order"), except as the whole operand of sizeof/_Alignof/typeof/a
  _Generic controlling expression; the parser now holds each such
  access and reports what no such operator claims. An element of an
  _Atomic array member stays a plain swapped scalar, as in gcc.
- scalar_storage_order written on a reference to an existing tag made a
  fresh incompatible type at every use, so `x = y` failed. As in gcc it
  is now ignored with a warning on variables, parameters, members,
  returns and casts, makes a variant (with gcc's warning) in a type
  name, and a distinct variant per typedef declarator.
- __builtin_constant_p of a GNU vector answered 0 at once; it now defers
  and asks each lane (Linearizer::vector_constant_p), 1 for a vector of
  constants from -O1 as in gcc.
- A decimal constant too large for any type also drew "so large that it
  is unsigned" about its truncated value; gcc gives only "too large".

Checked, nothing lost: ba90091's FoldCtx.bytes come from the same
ConstBytes::build at the same point in the pipeline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gcc's __builtin_setjmp buffer layout on x86-64 depends on
-fcf-protection. Under return protection (=return, =full, and the bare
flag) it is [0] rbp, [8] resume, [16] shadow-stack pointer (rdsspq into
a zeroed register), [24] rsp. Its longjmp then unwinds the shadow stack
with an incsspq loop that pops 255 entries at a time, and does nothing
when rdsspq reads 0. Without return protection (none, branch, check)
the layout is [0] rbp, [8] resume, [16] rsp. Under branch protection
(=branch, =full) the resume label starts with endbr64. Measured with
gcc 13.3 at -O0 and -O2.

c17 accepted the flag with an "unrecognized option, ignored" warning
and always used the three-word layout. Mixing it with gcc-built code
under -fcf-protection=full failed. A gcc longjmp read c17's rsp as a
shadow-stack pointer and hit incsspq (SIGILL). A c17 longjmp loaded
gcc's shadow-stack word, 0, as rsp.

The driver now folds -fcf-protection[=level] and -fno-cf-protection
into one option, and the last one wins. The bare flag means full. An
unknown level is an error in gcc's words. With no flag the setting is
none, which matches Debian's gcc (Ubuntu's defaults to full).
target::CfProtection carries {branch, ret} to CodeGenBase. Only the
x86-64 setjmp/longjmp read it. This adds no endbr64 at function entry,
no .note.gnu.property and no __CET__. aarch64 is untouched. The
library setjmp/longjmp needs no change because glibc saves and unwinds
the SSP inside its own jmp_buf.

Tests: c17/gcc interop in every pairing at -O0/-O2 under
-fcf-protection=none and =full. A second interop test turns the shadow
stack on in-process with arch_prctl(ARCH_SHSTK_ENABLE) and longjmps 601
frames deep, which exercises the incsspq loop for real. Both tests
failed before this change. Also added: asm tests for both layouts and
for the endbr64 landing pad on an explicit x86_64 triple, a driver unit
test for last-flag-wins, and a driver integration test for accepted
spellings and the bad-level error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jgarzik
jgarzik requested a balanced review from Copilot October 6, 2026 08:32
@jgarzik jgarzik self-assigned this Oct 6, 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 spans compiler-wide behavior (preprocessor, lexer, diagnostics, ABI, and multi-architecture codegen) with subtle gcc-conformance semantics that warrant final human review.

Review effort: Balanced
Findings: None

What changed in this PR

This PR is a broad batch of correctness and conformance updates to the cc C compiler crate, aligning several behaviors with gcc. It refines preprocessor and lexer handling (#line out-of-range numbers now warn-and-wrap instead of erroring, inter-token NUL bytes are treated as whitespace with a single warning per run, and #pragma scalar_storage_order survives -E), reworks over-wide integer and character-escape handling onto a two-kind pedantic-warning model, and expands test coverage (gnu89 extern inline→static takeover, -fcf-protection flag acceptance, interop/object test helpers gaining an extra-flags variant). A documentation note in TODO.md is also updated.

Changes:

  • Preprocessor/lexer conformance: #line numbers beyond the 32-bit counter warn by default and wrap (pedantic-only past C17's limit); NUL between tokens is whitespace; scalar_storage_order pragma is carried through as a layout marker.
  • Diagnostics: over-wide integer constants and out-of-range numeric escapes now use pedwarn_default (default warning, error under -pedantic-errors); new integer_digits_value/report_too_large_integer helpers keep the low 64 bits.
  • Tests/helpers: added coverage for storage-order pragmas, #line wrapping, gnu89 inline/static interaction, -fcf-protection; test helpers gain *_with variants threading extra compiler flags.
File Description
cc/​token/​preprocess_directive.rs #line operand rewrite with line_number_operand (warn-and-wrap); scalar_storage_order pragma emission; #line operands now macro-expanded as text.
cc/​token/​literal.rs New integer_digits_value/report_too_large_integer; over-range escapes switched to pedwarn_default.
cc/​token/​lexer.rs skip_whitespace treats NUL as whitespace, warning once per run.
cc/​token/​test_preprocess.rs Test for scalar_storage_order pragma becoming a layout marker.
cc/​token/​test_literal.rs Test for integer_digits_value low-64-bit truncation semantics.
cc/​token/​test_lexer.rs Test that NUL between tokens is whitespace.
cc/​tests/​preprocessor/​directives.rs Test that the storage-order pragma survives -E.
cc/​tests/​preprocessor/​conformance.rs Replaces the #line out-of-range error test with a warn-and-wrap test.
cc/​tests/​driver/​mod.rs gnu89 extern inline→static takeover cases and symbol-binding checks.
cc/​tests/​driver/​gcc_flags.rs Test that -fcf-protection levels are accepted and bogus levels error.
cc/​tests/​common/​mod.rs Adds c17_object_with/host_link_and_run_with/interop_host_with extra-flag variants.
cc/​tests/​codegen/​mod.rs Registers new bitfield_layout, float_to_int_saturation, label_diff test modules.
cc/​TODO.md Updates the 64-bit-frame torture note to drop the removed skip list.

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

jgarzik and others added 2 commits October 6, 2026 12:44
…eld ABI

Four test expectations were written for Linux targets and failed on the
macOS arm64 CI; c17's code was right in each case.

- c89 zero-width bit-fields: Apple arm64 follows the x86-64 rule (an unnamed
  bit-field does not affect aggregate alignment), not AAPCS64. All 40
  size/alignment values c17 gives for Darwin were checked against clang's
  record layout via libclang.
- __builtin_clear_padding of struct { char; long double; }: long double is
  double on Apple, so the struct is 16 bytes with padding at 1..7.
- (long double) of a double sNaN is no conversion where long double is
  double, so it raises nothing; that check is skipped there.
- float_to_int_saturation: Apple has no _Float128; those rows use long
  double instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
float_suffix decided long double's rtlib mode from the architecture alone,
answering "tf" on every non-x86-64 target. Apple arm64's long double is
binary64, so (long double) <-> __int128 called __fixtfti, __fixunstfti,
__floattitf and __floatuntitf, which Apple's runtime does not provide: the
link failed on macOS (codegen::float_to_int_saturation).

The suffix now follows TypeTable::fp_format, the one place that knows each
target's long double format, so Darwin gets __fixdfti and friends while
aarch64 Linux keeps tf and x86-64 keeps xf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jgarzik
jgarzik merged commit 26e5784 into main Oct 6, 2026
20 checks passed
@jgarzik
jgarzik deleted the updates branch October 6, 2026 16:35
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