Conversation
jserv
force-pushed
the
language
branch
2 times, most recently
from
September 14, 2026 10:38
d5167fa to
da28a08
Compare
jserv
force-pushed
the
language
branch
2 times, most recently
from
September 15, 2026 16:07
5df3981 to
7f27413
Compare
Records gain complete file-scope declarators, whole-record copies on assignment, record parameters, and passing by value in direct and indirect calls. Initializers follow C99: nested record and union members, designators at file and block scope, and compound literals of record, array and scalar type in either scope, with static array and record initializers emitted as data at their declared store widths. The static storage class is added, and a function redeclaration inherits its static linkage. const is enforced on writes, conversions, casts and arguments, and is kept on typedefs, trailing qualifiers and continuation declarators. signed and the C99 long spellings are accepted, pointer differences are scaled by the element size, sizeof void and void pointer arithmetic are diagnosed, array decay keeps its metadata, and string literal warnings are available on request.
Unsigned types work end to end, through the parser, the SSA folds and every backend, including high-bit software division on Arm, unary and conditional operators, narrow updates and reads, the integer promotions and conversions to bool. Integer literals take the C99 suffixes with checked ranges, repeated or contradictory type specifiers are diagnosed, and long keeps a rank of its own. long long is added on x86-64 and AArch64: typedefs, literals, wide global initializers and their operations, comparisons and division at the operand width, and ABI spill slots; 32-bit targets permit sizeof of it and pointers to it. Enumerations are declared in every scope, block static storage is complete, global pointers are initialized with addresses, unsized character arrays take their extent from a string, logical negation yields int, and a global object may not reuse a function's name. Arm sign-extends byte array loads, 32-bit targets keep narrow widths, and the driver gates wide cases on the target width.
The preprocessor defines the C99 implementation macros, evaluates every conditional directive, expands header names and #error operands, includes nested quoted headers, and concatenates adjacent string literals. extern, register, restrict, inline and volatile are accepted, const survives pointer levels and pointer typedefs, and signed char is a distinct type. __func__ is provided, multi-character and hexadecimal escape constants decode, sizeof gains complete expression semantics and type names in constant expressions, flexible array members work in one or more dimensions, nested designators and string-initialized char members are supported, and extern declarations are allowed in blocks and for initializers. Pointer subtraction and arithmetic are constrained and excess scalar initializers rejected. 64-bit integers are lowered to register pairs on 32-bit targets, though not yet admitted there. Paired global arithmetic folds, the ELF32 data segment is aligned, token streams are freed, and CFG traversal marks its epochs correctly.
Calls go through function pointer expressions and arrays of function pointers. Bit-fields and the C99 record layout rules are implemented, record tags may be forward declared or declared in a block, and typedef aliases keep record metadata. Arrays reach four dimensions with three-dimensional initializers, and sizeof keeps member arrays and rows. The preprocessor gains reproducible __DATE__ and __TIME__, canonical quoted include paths, angle headers resolved through include paths, #line, _Pragma and the stdbool and iso646 headers, and the lexer reads universal character names. Parenthesized pointer expressions can be subscripted, __func__ arrays are addressable, global aggregate pointer members are initialized, and COMPLIANCE.md is reconciled with the implementation.
auto is accepted, comma expressions sequence in every context, assignments lower in nested contexts, compound literals are lvalues whose members and elements can be updated, and conditionals lower inside controlling expressions. Function prototypes are checked for compatibility, qualified array parameters are supported, inline is rejected on objects, implicit enum overflow is diagnosed, and bit-field updates and compound values are preserved. Typedef array bounds are retained and composed, and array compound literals may have several dimensions. The lexer recognizes floating literals and keywords, wide character constants and strings, digraphs and trigraphs. The limits, stddef, stdint, assert and stdarg headers are provided, #if evaluates in intmax_t, and a strict mode diagnoses extensions and rejects forms outside C99. Global constants fold sizeof operands, and sizeof of a flexible array member is rejected.
Function designators decay in call arguments, value contexts and scalar conversions, and survive dereference and grouping. Function pointer typedefs, unnamed and abstract prototype parameters, unprototyped redeclaration promotions and internal aggregate returns are supported, and functions returning typedef arrays are rejected. Pointer-to-array declarators, pointer-array va_arg type names and void pointer decay depth are handled, and sizeof parses abstract array, function pointer and callback array type names and keeps grouped rows. Labels are statements, default may appear anywhere in a switch, case labels take constant expressions, enum constants evaluate at their C width, and out-of-range enumerators are diagnosed. Pointer-valued record members dereference, incomplete records cannot define objects, external inline linkage is constrained, postfix operators apply to array call results, global compound arrays lower their addresses, and UCN wide character constants decode.
Typedef aliases are scoped to their block, including pointer-row, function and callback typedefs with their own slots, and function and array typedef declarators compose. Pointers to array rows are lowered, elements update through grouped array pointers, and one fixed-array shape descriptor serves them all. One sizeof postfix walker is shared, offsetof takes fixed array designators, and global constants fold wide conditionals and comparisons while keeping unsigned literals and address offsets. for clauses take full expressions under the C99 declaration constraint, switch controls must be integers, void objects and elements are rejected, x86-64 selects right shifts by the left operand, aggregate va_start slots are reserved, and block static initializers resolve their addresses.
A detached Arm branch now jumps to both of its targets, and x86-64 initializer spills stay in the data area. Records are copied whole through conditionals and va_arg, braces may be elided for array members in initializers, and static initializers share address designators. Type name and joined string buffers are wide enough for the forms the parser now accepts, -2147483648 is typed as int on 32-bit targets, and every sizeof operand is walked by one scanner.
Internal failures print a diagnostic instead of aborting silently, and a compile-error test fails when the compiler crashes rather than reporting an error. Driver cases that need 64-bit values are gated on the pointer width, and check-all-targets runs the full check on arm, arm64, riscv and x64, as CI does.
The lexer records that a source buffer has no trigraph and no line splice, so translation phases 1 and 2 are skipped for it, and type lookup compares first bytes before whole names.
parser.c had grown past 18,000 lines. First keep one copy of each shared helper, remove the unused ones, and move the parenthesized operand, lvalue tail, block declarator and typedef paths of the longest functions into named helpers. Then move all but the parser core into eight files that parser.c includes, in order, at its end. They are fragments of one translation unit rather than modules: each sees every definition before it, so the include order matters. Generated code is unchanged.
Preprocessing a source that used volatile, static, extern, register, auto, restrict, inline, signed, unsigned or long stopped with "Unknown token kind", because token_to_string() had no spelling for them. A dynamic build could not compile shecc, whose elf_write_all() passes a const buffer to the fwrite declared in lib/c.h without const. assert called __assert_fail with three arguments, but glibc's handler takes the function name as a fourth. Spell those keywords in -E output, declare the fwrite buffer const, and pass __func__ to __assert_fail, with the driver expecting the wording of whichever libc reports the failure.
The AArch64 backend spelled every instruction as a hexadecimal constant ORed with register and immediate fields, inline in the code generator, so reading what it emitted meant decoding each word by hand. The Arm, RISC-V and x86-64 backends already keep their encoding in a file of their own. src/arm64.c now holds one helper per instruction form, each returning its 32-bit word, with the register and condition-code names. The code generator composes them and keeps everything that reads the IR, plus the range checks. The size field of a load or store is computed as an unsigned int, so it does not rely on -fwrapv. Generated code is unchanged.
Several backend paths produced wrong code once shecc compiled itself under dynamic linking or on 32-bit targets. A dynamic program's global frame began with the loader's leftovers. x86-64 casts between types of one width and different signedness did not re-extend the register. An eight-byte record could be placed in a register pair. Arm wide equality took the ordering sequence and emitted more instructions than the size estimate charged, wide slots at the top of the 12-bit range and halfword accesses past 255 bytes used unreachable immediates, a variadic function saved its parameters by source parameter rather than by ABI word, and the ELF32 section header offset ignored the static page padding. Clear the global frame with an inline loop, re-extend casts that change signedness, keep eight-byte records out of pairs, give Arm equality its own four-instruction sequence and choose one offset form for the emitter and the estimator alike, save variadic parameters word by word, and count the padding in e_shoff.
Several preprocessing and lexing rules differed from C99 and gcc. A
directive after a spliced line was missed or misread, a nested
conditional inside a skipped group was counted twice, white space
between # and the directive name and the null directive were rejected,
and _Pragma("once") was dropped. __DATE__ reached through a macro alias
expanded as __TIME__, trailing white space in a replacement list broke
#include of a macro, and __VA_ARGS__ was unbound when a variadic call
supplied no extra argument. A literal could contain an unescaped
newline, and wide character escapes kept only their low byte. The
Makefile read the translation epoch twice, so __DATE__ and __TIME__
could disagree.
In the library, memcmp compared signed bytes, a failed assert printed
to standard output, and fwrite took a char buffer. COMPLIANCE.md also
claimed restrict was retained, which it is not.
AArch64 loaded unsigned narrow objects with sign extension, so an unsigned char of 200 indexed an array at -56. The Arm wide multiply read its operands after UMULL could overwrite them if the result pair shared an operand register, which the allocator does not produce today. Redundant move elimination kept the dropped instruction's width, so an address could be cut to 32 bits on x86-64, and a cast widening to long long moved the register without extending it, which the RISC-V size estimate had also got wrong for the two byte forms. Load unsigned narrow values with the zero-extending forms, sum the Arm cross terms before UMULL, keep the surviving instruction's flags, and extend integers cast to long long by their own signedness.
Designators and brace elision lost values: a member designator did not end a pending array sequence, array member braces refused designators, reordered rows were zeroed, an inferred outer bound ignored partial rows, and brace-elided record elements were rejected. Declarations mishandled long long case labels, volatile file-scope records, enum tags reusing record tags, extern and register records and untagged local records in blocks, assignment expressions in for declarations, several prototypes in one declaration, block prototypes without extern, and qualifiers after the type specifier. Constant expressions ran out-of-range shifts in the compiler, rejected sizeof of strings, dereferences and arrays of pointers, took sizeof 1LL as int, and could not fold casts in file-scope initializers. A call accepted a trailing comma. Fix each at its reader, sharing one abstract declarator and one sizeof evaluation between the constant and expression paths, and one block declarator lowering between records, for declarations and ordinary objects.
Many expressions lowered incorrectly. A row of a two-dimensional array was loaded instead of decaying, member access on a call result did not parse, compound assignment and casts converted operands wrongly, stores and ++ on _Bool did not keep 0 or 1, unary minus was dropped before a parenthesis, sizeof mistyped many operands, and records reached through pointers were copied as one word. Postfix operators, address-of and array subscripts accepted only a few operand shapes, casts bound looser than pointer arithmetic, arrays did not decay in pointer differences, and arrays and records were accepted as operands of scalar operators. Declarations mishandled trailing commas in parameters, redeclarations in one block, storage classes after the type, enum and array typedefs, block pointer typedefs, nested and parenthesized declarators and tag redefinitions, and initializers mishandled strings in character rows, static address constants, function designators and strings that fill their array. Lower each through shared paths: one lvalue tail for postfix operators, one conversion for stored values, one record copy through an address, and declarators that compose typedef and array shapes.
The review fixes left parallel mechanisms and some costs behind. Name and tag lookups each had their own scope walk, a scope scan for enum constants ran for every identifier, scalar specifiers were read by six ladders and rewritten by a token pass, enum and record typedef bodies had two readers each, and initializers repeated zero fill, designator and record alias code. A member read through a dereference copied the whole record, so a large record took minutes; nested unary operators could exhaust the stack; a pointer plus a product was grouped as a sum times the factor; a function pointer returning _Bool was converted to _Bool; a cast with a qualifier among its specifiers looped forever; and type table overflow aborted. Share one ordinary and one tag lookup, one scalar specifier reader for declarations, casts, sizeof and va_arg, one enum reader and one record body reader. Read records through pointers in place, bound operand nesting, scale pointer arithmetic in the binary reducer, and report table overflow.
Random programs compared against gcc on all four targets exposed silent miscompiles in integer lowering. A file-scope long long initializer lost its high word on Arm and RISC-V; a store to an unsigned int global wrote eight bytes on x64 and AArch64; narrowed constants kept stale high bits; x64 right shifts and division clobbered or misextended their operands; an int shifted by a long long count was shifted in 64 bits on AArch64; a record kept its allocation register mapped, so a spill overwrote its first member; RV32 argument staging reused a register already holding an argument and widened a signed char to short without sign extension; and a 32-bit pair move was fused while its source was still read. Lower global setup operations as pairs, keep narrow loads and stores at their width, extend and fold constants by the target type, keep staged argument registers until the call, and refuse the unsafe fusions.
C99 makes an access to a volatile object a side effect, but shecc dropped such accesses at every stage. A discarded name such as gv; was never read, dead code elimination and CSE removed or reused reads through pointers, the allocator served reads from register copies and dropped writes to objects nothing read by name, parameters stayed in their incoming register, and peephole and the x64 slot cache skipped reloads and stores. Emit a read for every evaluated volatile lvalue, keep volatile objects and parameters in memory, reload them before each use, store every write, and carry a volatile flag into phase-2 IR so the peephole rules, the consecutive-store merges included, and the slot cache leave those loads and stores alone. A bit-field store keeps the read that preserves the rest of its unit, but its read-back is optional like any other assignment reload. Tests count the emitted reads and stores.
Several C99 6.7.8 forms were rejected or stored wrong values. A row
designator such as .a[1] = 3, 4 wrote a scalar over the whole row,
positional values after a nested designator went to the outer object,
later elided values wiped earlier designated members, chained
designators like [1].fn = f did not parse, a second designator into a
union member was refused, and an automatic union was not cleared. A
string literal was accepted as a single element of a non-character
array. Static initializers refused "ab"[1], "abc" + 1 and &(int){8}, and
stored a pointer cast into a _Bool without converting it.
Continue positional values inside the object a designator entered, keep
designated members when elided values follow, clear unions and gaps,
reject misplaced strings, and fold string elements, string offsets and
file-scope compound literal addresses as address constants.
Declarators and conversions diverged from C99 in several places. Compound literals of long types, parenthesized declarator names, file- scope pointer-to-array and function typedefs, arrays of function typedef pointers, function-type parameters and variadic or pointer returning function typedefs were rejected. Nested pointers to arrays lost their bounds, sizeof of an array parameter gave the array size, a qualified typedef of an incomplete record lost its qualifier, an enumerator could reuse a block typedef, and strict mode accepted a function declarator after the first one in a for declaration. Integer to pointer and incompatible function pointer conversions were accepted without a diagnostic. Accept each form with the representation its simpler spelling already had, keep array and qualifier shape through derived declarators, and diagnose the constraint violations of C99 6.5.16.1 while keeping null pointer constants and compatible prototypes valid.
Postfix updates were applied at the end of the full expression rather than at the sequence points of &&, ||, ?: and the comma, so x-- == 7 && x == 6 was false. Pointers compared as signed values, ++ and compound assignment stepped selected pointers by the wrong size, a record- defining pointer typedef was treated as a record, and a string literal assigned to a char pointer was not stored. Many function pointer forms were rejected or miscompiled: calls through dereferenced elements, pointers to function pointer arrays and their arithmetic, casts to written-out function pointer types, functions returning function pointers, compound literals of them, callbacks returning records, slots deeper than two stars and qualifiers on inner slot levels. Old-style function definitions and string literal subscripts were not accepted. Apply each update where its operand is evaluated, compare pointers as unsigned, step by the pointee, and model a callback slot by its depth so declarators, casts, calls, dereferences and conversions agree at any level.
The static library's printf family lacked %u, the long and long long conversions, and most C99 flags, widths, precisions and length modifiers, so shecc's own output could not match glibc's. Adding them made the inlined library exceed the DCE worklist, since the inliner put the whole library into one function. Separately, f(fmt, ##__VA_ARGS__) was treated as an invalid paste instead of the GNU comma elision. Implement every C99 integer and character conversion without floating point, split the inlined library into bounded functions, and follow the GNU extension for a comma pasted to an empty variadic argument.
Under dynamic linking the PLT follows the code and read-only data, and every call to a shared library function, like the entry point's call to __libc_start_main, was a JAL, which reaches only 1 MiB. A large enough program, shecc itself built with --dynlink, put the PLT out of that range and the code generator aborted with "Offset too large". Emit a PLT call as AUIPC and JALR, which reach any offset, and count the extra instruction both at the entry point, moving the main wrapper and the global function four bytes further, and for each call to a function without a body, so the offset estimate matches the emitted code.
Only the first declarator of a block declaration had its initializer
checked and converted, so int *a = 0, *b = 7; was accepted and
int a = 1, b = (int[]){2, 3} stored the literal's address in b. A
file-scope function pointer accepted a plain integer, an object pointer
accepted a cast to a function pointer type, a static aggregate element
lost the type of an explicit cast, a cast such as (int (**)(void)) 4
was rejected as a non-constant, and a zero enumeration constant was not
a null pointer constant.
Share one conversion path for every declarator, reject an integer for a
function pointer unless it is a null pointer constant or explicitly
cast, reject a function pointer for any object pointer destination,
recognize casts to pointers to function pointers as object pointer
casts, carry the cast type of an element, and mark enumeration
constants as constants.
A pointer-to-array typedef whose element came from a pointer or callback typedef was rejected or lost the callback element, at block scope even for typedef arr_t *rows_t, a call to a function returning a callback slot typedef lost the slot, and a function returning a function pointer could not be qualified or declared through a typedef. A cast to a callback typedef followed by a star was taken for a function pointer, and a static initializer rejected a cast to a function typedef followed by two stars. Constness was lost in several lvalues: stores through a pointer to a const callback typedef, through a const element of a row pointer, to members of a const record reached through (*ps).a, and through a decayed array member of a const record or pointer arithmetic on a pointer to const were accepted, while some stores that only change a pointer whose pointee is const were rejected. Keep typedef element depth and slot metadata on call results, build a row of callbacks as the pointee of its pointer typedef through one helper at either scope, accept the qualified and typedef forms, and carry each level's qualifier from the object or record that the lvalue selects.
An int constant folded at parse time recorded only its low word, so a cast to a wide type read a zero high word and (long long) (0 - 42) was 4294967254 on every target. A narrow conversion that changes signedness left the value extended by its source type, so unsigned short v = c for a signed char c read as ffffffef on arm, arm64 and riscv. An array operand of a comparison was integer promoted, which sign extended the address of a char array and made p == buf false. On x86-64 a zero test, a conditional move, a logical not and a fused mask test read the whole 64-bit register although an int result only defines its low word, so (~v) ? 1 : 2 chose the wrong arm, and a signed right shift replicated the sign bit of that garbage. Sign extend a folded constant by its own result type, truncate a narrowing conversion in the shared lowering rather than in each backend, leave pointer-like operands out of integer promotion, and test a value at the width its instruction carries.
A file-scope array compound literal read its element type as a single
identifier, so (const int[]){1, 2} and (unsigned char[]){1, 2} were
rejected although the same literals worked inside a function. An array
declared without a bound could not be completed later: extern int a[]
followed by int a[3] reported conflicting types. A typedef declared
inside a function was invisible to a for initializer and to the label
lookahead, so for (T i = 0; ...) was rejected while a label followed by
a declaration through such a typedef was accepted in strict mode, and
either form shadowing a global name resolved to the wrong kind. Inside
a function sizeof yielded a signed int, so (sizeof(int) - 5) > 0 was
false and -1 < sizeof(int) was true.
Read the full type name of an array literal, compare an outer bound
only when both declarations give one, look up a type through the
enclosing block, and give a block-scope sizeof the size_t type, folding
the unsigned and wide constants that null pointer constant checks then
need.
A pointer to an array kept its remaining bounds only for the first subscript, so p[0][1] for an int (*p)[2][3] loaded an element instead of the row it selects and compared unequal to the same row of the object, while a row of pointers lost one indirection and crashed. A call returning such a pointer lost the shape entirely: partial subscripts gave a plain pointer with the wrong stride, sizeof measured a pointer, a record element was loaded as one scalar and lost the bytes past the first word, and a row of pointers reached through a typedef counted its element stars twice and crashed. A member selected through a pointer typedef was rejected after a call, an assignment whose left operand starts with a call was an unexpected token, and a partial row of an array compound literal measured a pointer. Carry what the subscripts leave, take the element depth from the alias that spells the stars, keep a record element at its address so the record copy path applies, select a member through its effective pointer depth, and route a call-rooted assignment through the postfix path.
A file-scope brace initializer read an element that begins with a
number or a character constant through the runtime expression reader,
which emits a branch for a conditional or a logical operator and moved
the global setup entry block, so int a = 7; int d[] = {1 ? 2 : 3, 4};
left both a and d[0] reading zero. A discarded conditional arm was
skipped to the first colon, which dropped the declarator after
int a = 1 ? 2 : 3, b = 4; and rejected a nested conditional, and an
address constant could not be an operand of a conditional or a logical
operator at all. Narrow operands of a wide initializer skipped integer
promotion, so ~(unsigned char)0 was 4294967295; the minimum of a signed
wide type divided by minus one was folded instead of diagnosed; an
array bound or a bit-field width overflowed the compiler's own int
accumulator, giving int a[3000000000U / 1000000000U] one element; and a
cast address such as (int *) 0x100000010 lost its high word.
Route every one of those forms through the constant evaluator, count
brackets and pending question marks when skipping a discarded arm,
promote narrow operands, diagnose the division overflow, and keep both
words of a wide constant or address. An address operand is true, and
one offset by an integer constant, in either order, advances by the
stride its designator recorded. That stride is one more var_t member,
so the member limit rises to 128, above the 127 C99 requires.
An argument was macro replaced with the invoked macro already hidden,
so A(A(1)) left the nested use unexpanded, although C99 6.10.3.1
replaces an argument outside the macro it belongs to. A character
constant in a scoped constant expression carried no width, so
enum { A = 'a' << 1U } failed as a shift count out of range. The #line
directive accepted a hexadecimal, suffixed or out of range operand
where it takes decimal digits alone. An angle header name was resolved
from its physical spelling, so a line splice or a trigraph inside it
was never found.
Expand an argument with the invocation's own hide set, give a character
constant int width, accept only a decimal digit sequence after #line,
and read an angle header through the same splice and trigraph handling
as the rest of the source.
With --dynlink the standard streams were the descriptors 0, 1 and 2 cast to FILE pointers, which the host libc then dereferenced, so fprintf(stderr, ...) crashed. The address of an imported function was never patched, because the reference was recorded only for a function that has a body, so &strlen was null and a call through it crashed; on arm an indirect call into a PLT entry also clobbered the register that holds the global base. memcpy was declared with char pointers rather than the void pointers C99 gives it, and SOURCE_DATE_EPOCH reached a shell command line before it was checked to be a number. Look the stream objects up in the host image, naming libdl.so.2 as well when dlsym is called because glibc before 2.34 keeps it there, resolve the address of a body-less function to its PLT entry and reload the global base after an indirect call, declare memcpy with void pointers, and accept only a decimal epoch. COMPLIANCE.md now calls a static for initializer an extension, which is what it is.
This was referenced Sep 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This branch takes shecc most of the way to the C99 language. It adds signed, unsigned and long integer types, with 64-bit long long on every target, lowered to register pairs on 32-bit Arm and RISC-V and passed as AAPCS32 and the RV32 psABI require; const, volatile, restrict, inline and the static, extern, register and auto storage classes; nested and designated initializers, compound literals, bit-fields and flexible array members; arrays of up to four dimensions, pointers to arrays, and function pointer, callback and block-scope typedefs; sizeof in constant expressions; wide characters and strings, universal character names, digraphs and trigraphs; #line and _Pragma; the stdarg, stdbool, iso646, limits, stddef, stdint and assert headers; and a strict mode that rejects forms outside C99. Floating point types are recognized and diagnosed as unsupported. COMPLIANCE.md records where each feature stands.
Along the way it fixes miscompiles that the new source forms exposed on the backends (record-valued conditionals, brace elision for array members, detached Arm branches, x86-64 initializer spills), fixes the peephole, SSA and CSE miscompiles reported in #336, #337 and #340, scopes struct and union tags per block with C99 kind checks, reports invalid input through a diagnostic instead of aborting, closes the C99 gaps found in review (designators and brace elision, sizeof and constant expressions, declarations and scopes, _Bool conversions, postfix and address-of on any lvalue, preprocessor directives) and consolidates the parser lookups those fixes duplicated, and splits parser.c, which had grown past 18,000 lines, into ordered fragments that parser.c includes.
Verified on x86-64 Linux, with qemu-user for the other targets, at 56833c0:
make check-all-targetspasses on arm, arm64, riscv and x64 at stages 0 and 2 (3026 driver cases on each target, plus each target's ABI suite),make check DYNLINK=1passes on x64, arm and arm64 (3014 cases),make check-sanitizerpasses statically and dynamically, andmake check-stylepasses.shecc -E src/main.cpreprocesses the compiler and the result builds a stage 1 compiler on all four targets. Every commit in the series builds on x64 and arm, and every commit from the reformat onward passesmake check-style. A differential fuzz of about 1000 random integer programs per target against gcc on x64, arm, arm64 and riscv finds no remaining shecc miscompile. Under Valgrind, the minimal program from #319 ends with no memory in use at exit, where master loses 496 bytes in 5 blocks.Left out, beyond variable-length arrays and floating point: C99's translation limit of 127 parameters and arguments (shecc allows 8, 16 on AArch64, because parameters are stored inline in each function record and variadic prologues save a fixed number of words); a typedef naming a callback slot with a qualifier on its innermost callback pointer; comparisons between pointers to different object types, which are accepted without a diagnostic; and
longstays 32 bits on every target, which C99 permits but which differs from the LP64 ABI of x86-64 and AArch64.Closes #278
Closes #319
Closes #312
Closes #336
Closes #337
Closes #340