Skip to content

Audit for release 0.1.2#346

Merged
kromych merged 67 commits into
masterfrom
fix/release-audit-1
Jul 2, 2026
Merged

Audit for release 0.1.2#346
kromych merged 67 commits into
masterfrom
fix/release-audit-1

Conversation

@kromych

@kromych kromych commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Fixes for the 2026-07-01 pre-release audit: all 32 release blockers and all 28 high-severity findings, each with a regression test. Validated per wave on the three local boxes (cargo test debug+release, nine demo smokes) plus the BADC_MAX_GPR=2/3 pressure matrix on x64.

Highlights:

  • Front end tracks type facts: typed constant evaluator, literal boundary typing, integer suffixes, shift/ternary/compound-assign conversions, case-label promotion, and volatile carried through the IR with pass gating.
  • Toolchain fails loudly: fatal missing includes, lex errors on stray bytes, data-binding/definition collisions rejected, dead libc bindings removed, _Alignas honored to 16 or diagnosed.
  • One linkage model across compiled output, --interp, and --jit: undefined externs are clean errors, the JIT reaches bound host data, atexit and exit-status semantics match libc.
  • Call-ABI corrections on both backends (variadic marshalling incl. fn-pointer dispatch tables, SysV eightbyte classes, va_copy/va_arg, setjmp barrier, atomics operand shuffle) and object-writer fixes (PE export sort, section alignment chain, Mach-O export trie, ELF version-section padding, shared-TLS rejection).

Release blockers: closes #271, closes #272, closes #273, closes #274, closes #275, closes #276, closes #277, closes #278, closes #279, closes #280, closes #281, closes #282, closes #283, closes #284, closes #285, closes #286, closes #287, closes #288, closes #289, closes #291, closes #293, closes #295, closes #298, closes #300, closes #302, closes #304, closes #306, closes #308, closes #311, closes #313, closes #315, closes #317.

High severity: closes #290, closes #292, closes #294, closes #296, closes #297, closes #299, closes #301, closes #303, closes #305, closes #307, closes #309, closes #310, closes #312, closes #314, closes #316, closes #318, closes #320, closes #321, closes #322, closes #323, closes #324, closes #325, closes #326, closes #327, closes #328, closes #329, closes #330, closes #331.

Medium severity: closes #332, closes #333, closes #334, closes #335, closes #336, closes #337, closes #338, closes #339, closes #340, closes #341, closes #342, closes #343, closes #344, closes #345.

The medium set adds six further batches, each with a regression test and box-validated per increment:

  • crash-class: octal-literal accumulator overflow, parser recursion-depth guard, untrusted-object reloc bounds.
  • aarch64 codegen: folded FP-load displacement, call-site SP adjustment past the 12-bit immediate.
  • object/linker: PE-executable exports, Mach-O data-import symbol, five silent fallbacks turned into diagnostics.
  • Windows/driver: POSIX setenv overwrite (inline wrapper across compiled/interp/JIT), on-demand archive members, freestanding-after-parse, JIT handle unification.
  • stale-invariant sweep (nine comments reconciled, one real F32-ternary mem2reg deopt fixed).
  • C99 variable-length arrays (single-dimension block scope; unsupported forms rejected with a clean diagnostic).

Not in scope here: #319 (aarch64 non-HFA struct ABI).

🤖 Generated with Claude Code

kromych and others added 30 commits July 1, 2026 16:44
dyld resolves an image carrying LC_DYLD_INFO exclusively through the
export trie; the executable's dynamic exports (the every-global set a
dlopen'd module binds against) only reached the classic symtab, so a
flat-namespace lookup from a plugin failed despite the nlist entry.
Append each dynamic export (text and data) to the trie at its
image-base-relative address, matching the shared-library export path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both backends lower _Thread_local access with the local-exec TLS
model, whose TP-relative offsets are valid only in the executable's
static TLS block; baked into ET_DYN they silently address another
module's TLS. Fail the link until the general-dynamic model lands
(TODO in the writer). Mach-O (TLV descriptors) and PE (TLS directory)
resolve per-module and stay unaffected. fmt_link_err moves to the
native-emit gate so the final-image writer can use it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Directive parsing truncated #define bodies at the first // even inside
a string literal ("http://x.com" recorded as "http:), and the #if
expression pre-pass had the same literal-unaware truncation. Comments
are removed once in translation phase 3 (C99 5.1.1.2) by the
literal-aware strip_c_comments; the redundant naive strips in
parse_directive and expand_for_if are removed, and the post-substitution
strip on #if expressions (which covers driver-predefined macro bodies
that never pass phase 3) now uses strip_c_comments as well. The
literal-unaware strip_comments helper is deleted.

A missing #include emitted a warning and continued with an empty body,
so the compile exited 0 on a miscompiled unit. It is now a hard error
naming the header and the search outcome, matching gcc/clang; the -H
trace still records the '! name (missing)' line first.

Regression tests: define_body_keeps_slashes_inside_string_literal,
if_string_comparison_keeps_slashes_inside_literal,
unknown_include_is_a_hard_error; quoted_include_form_is_recognised and
show_includes_records_resolution_trace adapted to the fatal behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PE/COFF orders the export name pointer table by name so the loader's
import binding and GetProcAddress can binary-search it; the writer
emitted declaration order, so a DLL whose exports were not declared
alphabetically resolved names data-dependently (a miss surfaces as
STATUS_ENTRYPOINT_NOT_FOUND). Emit the name pointers, the ordinal
table, and the name strings in lexical order; AddressOfFunctions
stays in declaration order and each name maps back through its
ordinal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Data-family sections were byte-concatenated with sh_addralign ignored
and the merged stream placed at an 8-aligned base, so a foreign
object's 16-byte constant pool (clang/gcc -O2 `.rodata.cst16`) could
land misaligned and legacy-SSE aligned loads fault at runtime. Pad
the family blob to each section's alignment when parsing, carry the
maximum through NativeObject / MergedNative / Build, align each
unit's base in the merged `.data`, and place the data section at
that alignment in the writers: ELF rounds `data_off` past `.got`
(p_vaddr tracks p_offset), Mach-O rounds `__data`'s in-segment
offset and records the section alignment; PE's `.data` RVA is
already page-aligned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
repack_struct skipped bitfield members without reserving their storage,
so `struct { int flags:8; char c; } __attribute__((packed))` got
sizeof 1 with both members at offset 0 and a store to `c` corrupted
`flags`. The repack now lays bitfields out at the bit level with no
storage-unit padding -- the gcc/clang packed layout; C99 6.7.2.1p11
leaves the unit implementation-defined -- places the next non-bitfield
member at the following byte boundary, and re-derives each bitfield's
addressable unit as the smallest 1/2/4/8-byte window covering its bits,
slid back from the struct's tail so the read-modify-write span stays in
bounds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sion

Two constant-initializer fixes sharing one member-dispatch path:

A designator chain naming a char-array member (`.outer.inner = "s"`)
stored the literal's address bytes with a mis-sized relocation instead
of copying the array (C99 6.7.8p14). The per-member dispatch of
fill_struct_fields is factored into fill_member_value and the chain now
resolves to the final member's StructField, so a chain takes the same
string / array / aggregate / bitfield shapes as a directly named
member; the runtime local path gains the matching string-copy branch.

An unbraced union member of a struct fell into the scalar path, which
wrote the raw integer bits over the whole union with no conversion to
the first named member's type (C99 6.7.8p17), and write_init_bytes
wrapped the byte shift past 8 bytes, repeating the pattern at offset 8
(a debug build panicked on the shift). The unbraced union now routes
through fill_struct_fields, which converts and stores one value into
the first named member and stops; struct_flat_init_slots counts a
union as its first member's slots; write_init_bytes zero-fills past
the value's 8 significant bytes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… Windows

Four libc-surface fixes:

* stdlib.h declares strtof as returning double (c5 aliases float to
  double), but Linux bound it to libc's float-returning strtof and
  Windows to a strtof msvcrt does not export. Bind to strtod on every
  OS, matching the prototype and the existing macOS binding.

* Darwin's ioctl and shm_open are variadic and read the third argument
  via va_arg -- from the stack on arm64 -- so the fixed-arity
  prototypes passed it in x2 where the callee never looks (EFAULT for
  FIONREAD, garbage permission bits for shm_open). Declare ioctl
  variadic (POSIX XSH; glibc agrees) and shm_open variadic on Darwin
  only (glibc uses the fixed POSIX shape). Add the Linux FIO* request
  codes.

* msvcrt's _snprintf/_vsnprintf return -1 and omit the NUL on
  truncation; C99 7.19.6.5p3 / 7.19.6.12p3 require the untruncated
  length and a NUL for size > 0. The runtime now defines conforming
  snprintf/vsnprintf over _vsnprintf + _vscprintf, gated on the new
  __BADC_C5_CRT__ (set for hosted executables and shared libraries;
  native/EFI and freestanding images stay CRT-import-free), and
  stdio.h drops the msvcrt bindings for the standard spellings so
  they resolve against the runtime. The underscored _snprintf and
  _vsnprintf spellings keep msvcrt semantics.

Regression fixtures: strtof_parses_float, ioctl_fionread_pipe,
shm_open_mode_arg (all pass on macOS arm64; fixed-arity ioctl
counter-check returns 3), snprintf_truncation_c99 (Windows lanes run
the runtime shim; POSIX lanes lock the libc contract), plus PE-level
tests for the import shape and the CRT-only gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One family of front-end fixes where a width/signedness/rank fact was
dropped instead of tracked:

* lexer: octal and binary literals route through the shared
  u/U/l/L suffix scanner instead of silently dropping the suffix.
* expr: integer-literal typing at the 2^31 / 2^63 boundaries
  (C99 6.4.4.1p5); the signed range test was off by one, so
  2147483648 stayed int and -2147483648 - 1 wrapped at 32 bits.
* const_expr: ConstVal carries the value's C type; one fold
  (const_int_binop) applies the 6.3.1.8 conversions to every
  operator, so unsigned 64-bit /, %, >>, and relationals fold
  unsigned. Division by zero in an evaluated constant expression
  is diagnosed (6.6p4) instead of folding to 0, unevaluated ?:
  arms and short-circuited operands stay accepted, and
  LLONG_MIN / -1 wraps instead of aborting the compiler.
* expr: signed >> takes the promoted left-operand type (6.5.7p3),
  not int, so a 64-bit shift result is not re-narrowed.
* expr: a ternary with differing arithmetic integer arms converts
  both arms to the usual-arithmetic-conversions common type
  (6.5.15p5) instead of taking the else arm's type.
* expr + walk: compound /= and %= select signedness from the
  common type of both operands (6.5.16.2p3) and mask narrow
  unsigned operands like the plain-binary lowering.
* walk: switch case labels convert to the promoted controlling
  type (6.8.4.2p5), so case 0x80000000: matches an int INT_MIN.
* decl_base + aggregate: base-first specifier orders (int long,
  int unsigned, char unsigned, double long) fold the trailing
  modifiers into the base type (6.7.2p2) instead of discarding
  them; trailing inline / _Noreturn set their pending flags.

Regression fixtures (compiled, executed, parity-checked) plus
lexer suffix and const-expr diagnostic unit tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The release-version bump changes OUTPUT_MARKER in every emitted
object, so the byte+reloc golden table pins to the new version (the
mismatch reproduces at the bump commit with no compiler change). New
snapshots cover the ten fixtures added with the front-end and
initializer fixes; math_classify / libc_fp_classify pick up the
return-narrowing the promoted shift type now requires.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… dir

The fatal missing-include diagnostic surfaced two latent gaps: macOS
code includes <TargetConditionals.h> under __APPLE__ (bundle the
platform-dispatch macros with macOS values, zeros elsewhere), and the
demo's stdin pipeline never could resolve shell.c's quoted
sqlite3.h include (pass the demo directory as -I, as any compiler
needs for stdin input).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fatal missing-include diagnostic surfaced two more corpus gaps:
C99 7.6 fenv (rounding-mode encodings are per-arch on Linux/macOS,
UCRT abstract values on Windows; legacy msvcrt predates C99 fenv so
the bindings target ucrtbase) and the underscored fd-level io.h
Windows sources include directly. fegetenv and the environment
object are TODO.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…de lookup

Windows sources reach <direct.h> and <intrin.h> under _MSC_VER, and
spell bundled headers in any case (<Windows.h>): resolve embedded
headers case-insensitively for Windows targets, matching the
platform's filesystems. intrin.h binds the msvcrt byte-swap exports
and defines the compiler-reordering fence as an opaque no-op call;
the remaining intrinsics stay undeclared so their use fails loudly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…c surfaced

X11/Xresource.h routes to Xutil.h, which already carries the Xrm
types and prototypes; minwinbase.h / minwindef.h / sdkddkver.h follow
the existing winuser.h forward-to-windows.h convention. All were
silently empty includes before missing headers became fatal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
curl's Windows RNG includes <bcrypt.h> (the bound CNG surface already
lives in the bundled <windows.h>) and defines __MINGW32__, whose
internal <_mingw.h> glue the demo resolves with an empty-guard shim.
All demo translation units now cross-compile for windows-x64 with no
missing includes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The volatile qualifier was parsed and discarded, so every optimization
level violated C99 5.1.2.3p2 / 6.7.3p6: an unused volatile read was
deleted even at -O0, mem2reg promoted volatile locals (reverting a
store made between setjmp and longjmp, 7.13.2.1p3), and store_forward
forwarded across volatile accesses.

The type tag gains VOLATILE_BIT, orthogonal to UNSIGNED_BIT and
stripped by strip_unsigned; the declaration-specifier, declarator,
pointer-level, and cast parses set it. Inst::{Load, Store, LoadLocal,
StoreLocal} carry a volatile flag the walker sets from the accessed
lvalue's tag (locals, globals, TLS, derefs, members, bitfields,
initializers, read-modify-writes). Gated consumers: the builder's
load-local CSE, is_pure_inst / is_dead_pure (an unused volatile load
emits at every level), mem2reg promotion, slot coalescing,
store_forward, index_fold, and struct_return_reg. A volatile
read-modify-write no longer re-reads the object for its result value;
it narrows the stored value in a register. An aggregate copy of a
volatile object stays unmarked (TODO on Inst::Mcpy).

Regression: volatile_setjmp_longjmp.c, volatile_ptr_alias_loop.c,
volatile_unused_read.c on the native and JIT parity lists, plus
SSA-level tests in mem2reg, store_forward, and reg_alloc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixtures that declare volatile now keep their accesses in memory as
6.7.3 requires; the drift is confined to those fixtures plus the
three new volatile regression fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
C99 6.5.16.1p1 lets the target add qualifiers; passing a plain struct
pointer to a volatile-qualified parameter warned as an incompatible
struct type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The walker's parameter-prologue loops masked only UNSIGNED_BIT off the
declared tag, so a VOLATILE_BIT-carrying tag never compared equal to
its band: a `volatile double` parameter was not marked FP and was
seeded as an integer ParamRef reading a stale integer argument
register while the caller passed the value in an FP register, a
`volatile float` lost its narrow entry copy, and a by-value
`volatile struct` lost its entry classification the same way. All
five sites now use strip_unsigned, and the mislabeled `is_pointer`
UNSIGNED_BIT test is renamed to what it checks: unsigned parameters
keep the full-width store/load; pointer tags land in the I64 default
arm by band value.

Regression: volatile_param_classes.c on the native and JIT parity
lists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
splice_multi_block emitted the caller blocks after the splice point in a
single pass, so a use of the inlined call's result (mapped only when the
callee's Return is spliced) resolved to NO_VALUE and the value was
silently dropped at -O. Run the emission to a fixed point, as the flat
single-block splice already does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The post-coloring Shl K / Shr K fold reads the Shl source's place when
the Shr is emitted, past the source's live range; an intervening
definition colored onto the same place (or an inst with fixed
emit-level clobbers) invalidates the read. Fold only when the pair
shares a block, every intervening inst is clobber-free, and no
intervening definition occupies the source's place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A DataReloc target of the &arr[N] address constant equals the next
object's start offset, so compaction marked and rebased the wrong
object; with bss segregation the neighbor moved and the stored end
pointer went stale. Carry the pointed-at object's start offset
(target_anchor) on the reloc and resolve intervals through it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The address-taken trampoline for a variadic libc binding re-pushed only
the declared prefix plus one register argument, dropping the variadic
tail the caller marshalled per the host convention (stack region on
AAPCS64-Darwin, al on System V). Route variadic bindings through the
frameless tail jump already used for FP-touching bindings; it forwards
every register class and the stack unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parse_block_typedef recorded the prototype only for the function-type
shape; a `typedef RET (*NAME)(args)` declarator left the typedef symbol
without params/is_variadic, so indirect calls through variables of that
typedef marshalled a variadic tail as fixed register arguments. Mirror
the file-scope branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
emit_atomic_cas / emit_atomic_rmw marshal operands into the borrowed
x9..x12 in sequence; an operand the allocator placed in one of those
registers could be read after an earlier move overwrote it (a
compare-exchange then reported success without exchanging). The values
are already saved at [sp..sp+32] before marshalling, so read such
operands from their saved slots.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The windows-arm64 setjmp lowering is an Inst::Intrinsic, which the
allocator's call-crossing tests did not match, so a value live across
the setjmp site could sit in a caller-saved register that longjmp does
not restore (the jmp_buf covers x19-x28, x29, sp, d8-d15 only),
violating C99 7.13.2.1p3. Treat the setjmp site like a call in
values_live_across_calls and promote_calls_after_def_to_classes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
marshal_args loaded a mixed SSE/INTEGER aggregate's integer eightbyte
into its target GPR before the scalar parallel move consumed that GPR
as a source (System V AMD64 3.2.3 classifies per eightbyte; the
integer targets are argument registers). Route the aggregate's base
through its first integer eightbyte register inside the parallel move
and load the eightbytes afterwards, base register last; all-SSE
aggregates keep the early scratch-based loads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Win64 and System V variadic call arms captured a <=16-byte
aggregate return from rax:rdx unconditionally; an SSE eightbyte
arrives in xmm0/xmm1 (System V AMD64 3.2.3) exactly as on the
non-variadic path. Share one classified store across all call shapes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The System V va_copy lowering materialized the destination pointer
into rcx, which sits in the allocator's caller pool and may hold a
value live across the intrinsic (va_copy is not a call). Route both
pointers through the reserved r10/r11 scratches and borrow the copy
temporary around a push/pop pair, as emit_mcpy already does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kromych and others added 29 commits July 1, 2026 22:06
Five conformance fixes plus one input-handling fix:

* Object-like alias chains (`#define A B` / `#define B C` / `#define
  C B x`) paint every intermediate name for the rescan (C99
  6.10.3.4p2); the chain walk previously painted only the head, so
  the rescan re-fired an intermediate and duplicated tokens. A
  revisited name now ends the walk instead of looping to the cap.
* `__has_include("h")` resolves its quoted form against the including
  file's directory and `__has_include_next` resumes past the current
  file's search-path entry, exactly as the matching directives do
  (C99 6.10.2p2); both previously ran a plain angle-include search.
* `#if` character constants decode hexadecimal and octal escape
  sequences (C99 6.4.4.4) and sign-extend a single-character constant
  on signed-plain-char targets, matching the lexer.
* `#if` ternaries apply the usual arithmetic conversions across both
  arms (C99 6.5.15p5), so `(1 ? -1 : 0u) > 0` compares unsigned.
* An `#elif` / `#else` / `#endif` met while joining macro-argument
  lines with no locally opened frame belongs to the conditional
  enclosing the macro call; it now updates the outer stack (shared
  helpers keep both walks identical) instead of being swallowed and
  leaving the block unterminated.
* A UTF-8 byte-order mark opening a file is skipped, following gcc
  and clang.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A byte outside the token grammar (`$`, `@`, backtick, any non-ASCII
byte outside a literal) was silently discarded, so the parse could
re-synchronize into a different program (`*$p` compiled as `*p`).
Such bytes are now a lex error quoting the offending byte. C99 6.4
white-space (VT, FF) and stray NULs stay ignored, following gcc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* `prologue_ends` is keyed by the function's merged entry offset
  instead of its bare name: two units' same-named static functions
  each keep their own post-prologue anchor, so Win-x64 .pdata and
  the DWARF CFA no longer describe a framed static as a frameless
  leaf copied from the first unit.
* An unresolved STB_WEAK UNDEF with no dylib routing resolves to
  address 0 (ELF practice) instead of becoming a required import
  against dylib 0: branches become no-ops (the GNU linkers' AArch64
  handling), address materializations produce 0 so the
  `if (fn) fn();` guard reads null, and `.data` pointer slots take
  0 + addend. Unsupported instruction shapes are a diagnostic.
* GNU version-name strings appended to `.dynstr` re-pad it to 8, so
  `.hash` / `.gnu.version` keep their claimed sh_addralign (gABI).
* Unit bss bases and the merged file image pad to 16 so each
  object's compaction-preserved alignment residue survives the link.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* `_Alignas(N)` / `__attribute__((aligned(N)))` / `__declspec
  (align(N))` on a file-scope or static block-scope object is
  honored up to 16: the object's data offset is padded, the unit
  records `data_align = 16` (threaded through `Program` into the
  writers and the ET_REL `.data` sh_addralign), and the compaction
  residue rules carry the placement into the final image. Larger
  requests, automatic objects above the 8-byte slot alignment, and
  member alignment above 8 are diagnostics -- never a silent drop
  (C11 6.7.5).
* `int x; extern int x;` keeps the tentative definition (C99
  6.2.2p4 / 6.9.2p2): an extern redeclaration after a file-scope
  definition -- tentative, initialized, or the array form -- no
  longer marks the symbol extern-only, so the TU still defines it
  and the link succeeds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gma warning

* Static-archive members join the link on demand: a member is
  included iff it defines a symbol the link still leaves undefined,
  iterated to a fixpoint (matching the documented model and SysV ar
  practice). Unreferenced members no longer fail a valid link with
  their unrelated undefined or duplicate symbols.
* The auto-include retry no longer overrides a user definition: when
  a multi-TU link defines a name the retry bound to a bundled-header
  libc binding, the referencing source is recompiled with the name as
  a C89 6.3.2.2 implicit `extern int name();` so the call resolves
  against the user's definition. Names nothing defines keep the
  auto-include.
* `-c` and `--ar` warn when `#pragma subsystem` / `#pragma
  entrypoint` is dropped: the pragma rides the in-memory program of
  the invocation that links, and an ET_REL object carries neither, so
  a GUI TU compiled to an object no longer silently links as a
  console image. `Program::entry_pragma` carries the literal pragma
  value, distinct from the resolved `entry_name`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Remove bindings whose target symbol the named library does not
  export, so a use fails loudly at link instead of aborting at load:
  macOS clock_nanosleep, fexecve, mremap, and the sched_setparam /
  sched_getparam / sched_setscheduler / sched_getscheduler /
  sched_rr_get_interval family (dlsym-verified against libSystem),
  and glibc ctermid_r.
* Windows: CharLowerW / CharUpperW bind to user32.dll and UuidCreate /
  UuidCreateSequential to rpcrt4.dll (their documented homes;
  kernel32 fails at load). _ftelli64 and the _byteswap_* family bind
  to ucrtbase (absent from the legacy msvcrt.dll); ctime_s binds to
  the 64-bit-time_t export `_ctime64_s` like its localtime_s /
  gmtime_s neighbors; the `_getpid` spelling is arch-gated to
  ucrtbase on arm64 like unistd.h's getpid.
* Windows x64 binds tzname / timezone / daylight as msvcrt data
  imports (the `environ` treatment), so a read after `tzset()` sees
  the CRT's values instead of dead zero slots; the runtime's local
  slots stay Linux-only (COPY-relocation targets). arm64 keeps safe
  local slots (empty names, zero offset) with a TODO until the
  msvcrt.dll surface is verified on an arm64 box.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Probing both boxes shows msvcrt.dll exports _tzname/_timezone/_daylight
as data on arm64 as well as x64, so the arm64 fallback slots go away
(they would now collide with the bindings), and the byte-swap trio
lives in the UCRT on both architectures. tzset outputs verified live
on kromyrzen and kromarm00.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Compiler::new follows the host; the environ binding shape is per-OS
(macOS GOT data import, Linux runtime definition, Windows per-arch),
so the test compiled differently on a Windows host and failed before
reaching the diagnostic under test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fatal missing-include diagnostic surfaced every silently-empty
include across the tcl / cpython / curl lanes. Each is resolved by
providing the platform-faithful surface -- sys/file.h flock,
linux/{auxvec,fs,limits,memfd,sched,wait}.h and sys/{auxv,pidfd}.h
subsets, net/ethernet.h, mach/* and sysctl, the Windows framework
forwards -- never by turning a HAVE_ config claim off, so the demos
exercise exactly what they did before. Adds the macOS deployment-target
predefine and Annex-K memset_s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The octal digit loop used checked multiply where the binary and hex
paths already wrap, so the octal spelling of ULLONG_MAX panicked a
debug build; wrap so debug and release agree and the type picker
types the literal from the bit pattern (C99 6.4.4.1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The recursive-descent parser had no depth limit, so deeply nested
parentheses, declarators, initializer braces, or blocks overflowed the
native stack and aborted the compiler (SIGABRT at ~250 paren levels in
a debug build). Every recursive entry point (statements, expressions,
constant expressions, declarators, initializer lists) now runs through
a shared counter bounded at 1024 levels -- well above the 63-level
minimum of C99 5.2.4.1 -- and reports "<construct> nesting too deep".
The CLI driver runs on a thread with a 256 MiB stack reservation so
the diagnostic fires before the native stack runs out in debug builds,
which spend tens of KiB of stack per nesting level.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The float arm of a conditional expression stored its result through
LocalAddr + Store, which marked the synthetic merge slot address-taken
and excluded it from mem2reg promotion. Both backends lower the fused
StoreLocal / LoadLocal { F32 } in a single instruction (narrowing an
f64 arm per C99 6.3.1.5), so the address-taken form was unnecessary.
Use the fused ops for float as for double; the merge slot is now
promoted and the single-precision value is preserved (C99 6.5.15).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Correct comments that describe behavior the code has moved past:

  * x86_64 param_reg_stack_split: register/stack placements need not
    be contiguous (System V AMD64 MEMORY-class or Win64 overflow
    aggregates interleave them); no assertion gates this.
  * x86_64 detect_tail_call: callee-saved use is allowed (the
    caller-saved arg window is disjoint from gpr_used), and any
    LocalAddr, not only a negative slot, disqualifies the tail call.
  * aarch64 move_call_result: an FP scalar result rides d0 (AAPCS64
    6.4.2), an integer result x0 (6.4.1); emit_return leaves it in d0.
  * reg_alloc last_use: document its live consumers (the call-spanning
    interval and coalescing tests), and add the GotoIndirect arm to
    the two terminator walks that lacked it -- redundant with the
    exit_acc bump but uniform with the liveness and use-count walks.
  * elf_reloc: .rela.text is emitted, so cross-TU calls link.
  * mach_o: __thread_data carries TLS init bytes once tls_init_size is
    non-zero; __DWARF is emitted for executables and dylibs at its own
    vmaddr slot.
  * pe: .reloc follows any absolute pointer (TLS directory and
    address-of-static relocations); the adrp+add pair is PC-relative
    and needs no base relocation.
  * pe / program / compiler: a _Thread_local initializer raises
    tls_init_size and lands in .tdata.
  * link / synth_build: multi-object TLS is resolved via per-unit
    TPOFF / descriptor rebasing; the Windows/aarch64 TEB path records
    a fixup for every access.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ive/freestanding input handling

setenv now honors the POSIX overwrite flag via a runtime shim (msvcrt
_putenv_s always overwrites); the Windows JIT resolves symbols through
the same module-handle set bind_imports uses; the --freestanding
entry check runs after inputs are parsed; and an archive-only
invocation links its members instead of reporting no files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e shim

The runtime-shim setenv definition resolved only on the compiled path;
under the interpreter and JIT the unbound symbol errored (a fixture's
setenv roundtrip failed on Windows). Define setenv inline in <stdlib.h>
so all three paths share one definition: probe getenv, then _putenv_s.
Overwrite=0 leaves an existing binding untouched (verified on the
Windows box).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erpreter

The inline setenv wrapper calls _putenv_s, which the interpreter's
libc bridge did not handle; a setenv roundtrip fixture failed under
--interp on a Windows host. Dispatch _putenv_s to the host with force
overwrite (its msvcrt semantics), matching the setenv arm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n surfaces

The macOS CI lanes need three more real system headers the fatal
missing-include diagnostic surfaced: sys/cdefs.h (the BSD declaration-
decoration macros, tcl's unix port), plus the minimal CoreFoundation
and SystemConfiguration surfaces curl's macos.c reaches for
(SCDynamicStoreCopyProxies, CFRelease); framework symbols bind under
their underscore-prefixed Mach-O names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
emit_load hard-coded offset 0 for the F32/F64 arms while emit_store
passed the folded displacement to the immediate-offset encoders. index
fold declined the floating kinds, so disp was always 0 there and the
drop stayed masked; extend the constant-offset fold to F32/F64 (the
scaled-index fold stays integer-only) and route disp through the FP load
encoders. The four FP ldr/str encoders now share enc_ldst_scaled, so an
out-of-range or misaligned immediate is caught uniformly.

Call-site SP adjustments used the raw 12-bit enc_add_imm/enc_sub_imm on
plan.scratch_bytes and the 16-byte-stride push total; an outgoing area
past 4095 bytes shifted the immediate into the LSL#12 bit and adjusted
SP by the wrong multiple. Route every call-site adjustment through
emit_sub_sp_imm/emit_add_sp_imm, the 24-bit split the prologue already
uses.

Two consequences the wide-argument fixtures surfaced: the argument
planner's fp_arg_mask shift overflowed for arguments past 31 (the walker
only sets the mask for the first 32), and emit_call_indirect blindly
fell back to x9 for the target pointer when every scratch held a live
arg source. Guard the shift and stage the pointer in a reserved stack
cell (host-ABI branch) or capture it after the pushes consume the
sources (c5-stack branch).

Fixtures fp_load_folded_disp.c, call_sp_adjust_imm12_overflow.c, and
indirect_call_target_scratch_exhausted.c registered in the jit, native,
and both native_elf lanes; two byte-scan codegen tests lock the folded
FP encoding and the shifted-12 SP split.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FP loads now fold the displacement in place (disp=N replaces a
separate add) and call sites use the 24-bit SP-adjust split, changing
the emitted shapes the affected fixtures pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Windows CRT emits \r\n for \n on stdout, so the exact-bytes
comparison failed on the Windows lanes; strip CR before comparing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…port symbol, harden silent fallbacks

#340: --export-all/--export-data now reach PE executables. `.edata` is
emitted whenever the image exports anything (pragma set or an
executable's dynamic exports), and the export directory resolves each
name to its runtime RVA (text or data). Previously the flags no-op'd for
PE executables so GetProcAddress returned NULL for every host symbol.

#341: a Mach-O data import (bound through the GOT, no PLT trampoline) no
longer gets a local text symbol at code offset 0 (the first function's
address), which mislabeled backtraces and breakpoints. `Build`'s
per-import trampoline offsets become `Vec<Option<usize>>` so the writers
key each local symbol by import index, not by an ordering contract the
synth path does not uphold; a `None` slot emits no text symbol.

#333: convert five silent fallbacks to diagnostics.
  * pe.rs pad_to overshoot -> hard error (layout-drift guard, replacing a
    release-compiled-out debug_assert).
  * elf.rs export whose ent_pc misses pc_to_native -> error, matching the
    Mach-O/PE writers instead of dropping it from .dynsym.
  * mach_o.rs __tlv_bootstrap ordinal via one shared helper for the bind
    stream and the nlist entry; error when no linked dylib is libSystem
    rather than guessing ordinal 1.
  * synth_build.rs import->dylib routing miss defaults to 0 only for a
    flat-lookup import or a single-dylib image; a miss with several
    dylibs is an error, not a silent misroute.
  * link.rs merged import->dylib map rejects a cross-unit routing
    conflict and a per-unit dylib index out of range instead of
    first-writer-wins + unwrap_or(0).

Regression tests: macho_data_import_gets_no_bogus_local_text_symbol
(tests/linker.rs), plus writer/unit tests for each guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tcl's macOS notifier includes <os/lock.h> (os_unfair_lock) and
quickjs-libc includes <crt_externs.h> (_NSGetEnviron); both bind to
libSystem under the underscore-prefixed Mach-O names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add C99 6.7.6.2 variable-length arrays (block-scope, single dimension),
closing the conformance gap where any non-constant array dimension was
rejected at parse time.

Frame mechanism: reuse the existing per-frame alloca arena. A VLA
declaration evaluates its runtime element count, multiplies by the
element size, and allocates from the arena via Intrinsic::Alloca; the
base pointer and byte count are stored in two hidden frame slots. A
function that declares a VLA sets the alloca-top slot, which already
disables mem2reg promotion for the whole function (the runtime-sized,
address-taken array is never promoted) and blocks inlining.

Reclamation: two new intrinsics, AllocaSave and AllocaRestore, read and
write the arena top. parse_block_stmt brackets a VLA-declaring block
with VlaScopeEnter / VlaScopeExit so the storage is reclaimed on block
exit and, for a loop body, on every iteration -- the fixed arena does
not grow unboundedly.

Type representation: Symbol gains is_vla + the two hidden-slot indices.
A VLA identifier decays through Expr::VlaBase (a load of the base
pointer); sizeof of a VLA lowers to Expr::VlaSizeof (a load of the byte
count) per 6.5.3.4p2. Indexing, deref, pointer arithmetic, and
address-of then reuse the existing pointer paths.

IN scope: single-dimension block-scope VLA; runtime sizeof; a dimension
from a function argument; per-iteration scope reclamation; a VLA
parameter (adjusted to a pointer, 6.7.6.3p7). __STDC_NO_VLA__ is no
longer predefined.

Rejected cleanly (no miscompile): multidimensional VLAs (runtime
element stride); file-scope / variably-modified file objects; a VLA
with an initializer (6.7.8p3).

Regressions: vla_basic_sum, vla_runtime_sizeof, vla_size_from_arg,
vla_scope_reclaim_loop, vla_param_decay (native + native_elf +
native_elf_x64 + PE fixture tables and a JIT lane in tests/vla.rs);
vla_multidim_rejected, vla_file_scope_rejected, vla_initializer_rejected
assert the diagnostics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… build

The Mach-O TLS path now requires libSystem in the dylib set (a wrong
ordinal guess would bind __tlv_bootstrap to the wrong dylib). Emitting
a host-compiled program for macOS from a non-macOS host lacks libSystem;
compile for each target so its own dylib bindings are present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The vla_*_rejected.c fixtures assert a clean diagnostic for
unsupported VLA forms, so the every-fixture-compiles smoke must not
expect them to build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kromych kromych merged commit 3583ee2 into master Jul 2, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment