Standard conformance

badc targets C99. Anything a C99 program relies on that is not listed here follows C99. This document records three things: the implementation-defined choices C99 requires a compiler to make (6.2.5, 6.7.2), the divergences – from C99, and from gcc / clang practice where the standard leaves the choice open – and the non-C99 extensions badc implements (C11, C23, POSIX, GCC, MSVC, and badc’s own).

Implementation-defined choices (C99 6.2.5, 6.7.2)

type macOS / Linux Windows
char 1 1
short 2 2
int 4 4
long 8 4
long long 8 8
pointer / T * 8 8
float 4 4
double 8 8

LP64 on macOS / Linux, LLP64 on Windows – both match the host platform ABI. wchar_t is 4-byte int on macOS / Linux and 2-byte UTF-16 on Windows, matching each host. The widths are also readable from the __SIZEOF_*__ predefines, which agree with the table by construction.

Plain char signedness is implementation-defined (C99 6.2.5p15). badc follows the host C ABI: signed on x86_64 (all OSes), Apple AArch64, and Windows AArch64; unsigned on AArch64 ELF. The chosen signedness agrees with the __CHAR_UNSIGNED__ predefine and drives the extension when an 8-bit char l-value widens to a larger integer.

long double’s storage follows the target ABI. C99 6.2.5p10 permits any FP type at least as wide as double. sizeof, _Alignof, struct offsets, array stride, static initializers, <float.h>, and the __LDBL_* / __SIZEOF_LONG_DOUBLE__ predefines all report one layout per target:

target platform long double badc stores size / align
linux-x64 x87 80-bit x87 80-bit 16 / 16
linux-aarch64 IEEE binary128 IEEE binary128 16 / 16
macos-aarch64 IEEE binary64 IEEE binary64 8 / 8
windows-x64 IEEE binary64 IEEE binary64 8 / 8
windows-arm64 IEEE binary64 IEEE binary64 8 / 8

An object therefore has the platform’s layout and encoding on every target, so a struct, an array, or a .data object shared with code built by the platform toolchain agrees byte for byte. A load converts the stored value to binary64 and a store converts back exactly: on linux-x64 through fld/fstp, matching the hardware conversions bit for bit including the noncanonical encodings; on linux-aarch64, which has no quad-precision unit, through open-coded integer sequences that match gcc’s __extenddftf2 / __trunctfdf2 bit for bit.

Two consequences remain on both Linux targets:

The remaining work is the argument / return conventions (a MEMORY-class 16-byte stack slot and an st(0) return on System V, a Q register on AAPCS64) and extended-precision arithmetic. TODO: extended-precision long double.

Byte order is little-endian on every target: __BYTE_ORDER__ expands to __ORDER_LITTLE_ENDIAN__ and __LITTLE_ENDIAN__ is defined.

Divergences

Severity (for compiling existing C): 1 = blocks almost everything, 2 = blocks much real code, 3 = blocks specific idioms, 4 = workaround exists, 5 = rare in modern source.

const accepted but not enforced, severity 4

volatile is enforced (6.7.3p6): an access through a volatile-qualified lvalue is marked through the IR, performed exactly once in program order at every optimization level, kept memory-resident (no promotion, coalescing, forwarding, or dead-access elision), and not moved across an inline-asm statement. One gap remains: a whole-aggregate copy of a volatile-qualified struct is lowered as an unmarked block copy. const is accepted but not enforced: badc does not diagnose assignment to a const-qualified object (a 6.5.16.1 constraint violation) or the discarding of const in a conversion, so a program that modifies a const object compiles without the required diagnostic. restrict is accepted and ignored: it is an aliasing hint with no observable semantics.

Function-pointer return lineage carries one call level, severity 5

A pointer to a function returning a function pointer (int (*(*p)(int))(int) = f) is called correctly in every spelling – (*p)(0)(3), p(0)(3), (*(*p)(0))(3) – for local, global, typedef, struct-member, and parameter carriers. The flat tag holds only the total pointer depth (shared with int (**)(int)); the symbol carries the split as two scalars, the derefs down to the function pointer and the return value’s own lineage, and the call sites seed the decay tracking from them (C99 6.3.2.1p4). One scalar per side covers one function-pointer level per call: a return chain that is itself a pointer to a function-pointer-returning function (int (*(*(*p)(int))(int))(int)) calls correctly without * between the later calls, while a star-decorated later call ((*(*(*p)(0))(0))(3)) is rejected with a diagnostic rather than compiled. TODO: carry the full per-level lineage.

An inline definition is materialized unit-locally, severity 5

C99 6.7.4p6-p7 decides whether a definition is an inline definition: it is when every file-scope declaration of the function includes inline and none includes extern. An inline definition provides no external definition, and badc emits none. Where gcc leaves the un-inlined reference undefined, for the program’s external definition to satisfy, badc gives the definition internal linkage and binds the reference to the unit-local body – the alternative 6.7.4p6 grants the translator (“an alternative to an external definition, which a translator may use to implement any call to the function in the same translation unit”). That covers a call. A use of the function designator as a value – &f, or passing f – still references the external symbol, so a unit that takes the address and no unit supplies the external definition fails at the link, as it does under gcc and clang.

__STDC_HOSTED__ is always 1, severity 5

C99 6.10.8p3 defines __STDC_HOSTED__ as 1 only for a hosted implementation. badc defines it as 1 unconditionally, including under --freestanding and -ffreestanding, so source that selects a freestanding subset from this macro alone takes the hosted branch. --freestanding changes what is linked and -ffreestanding which calls fold, not what is predefined.

Not implemented, severity 4-5

C99 features rejected (all rare in current source): _Complex / _Imaginary (6.2.5), and digraphs and trigraphs (6.4.6 / 5.2.1.1). A universal character name is implemented in a string or character literal, under 6.4.3’s constraints, and in an identifier, where 6.4.2.1 limits it to the Annex D characters; the UTF-8 spelling of such a character is the same identifier. The absence of complex types is announced in the C11-conforming way: __STDC_NO_COMPLEX__ is defined as 1. #pragma STDC FP_CONTRACT / FENV_ACCESS / CX_LIMITED_RANGE (7.1.2p6) are accepted and ignored: -O contracts a*b+c into an FMA whatever the pragma says.

Implemented, and listed here because they sit next to the above in C99: K&R identifier-list function declarators with separate parameter declarations (obsolescent, 6.11.7) are accepted and lowered. _Noreturn is recorded on the function symbol and propagated – a call to a _Noreturn function does not reach its continuation in the fall-through reachability analysis, which also reports a non-void, non-main function that can fall off its end without returning a value. That report is return-type (B3003), off by default and enabled by -Wall; clang’s counterpart is on by default.

__STDC__, __STDC_HOSTED__, __DATE__, and __TIME__ are predefined. __DATE__ and __TIME__ are the time of translation in UTC; the SOURCE_DATE_EPOCH environment variable fixes it, as under gcc and clang. __STDC_VERSION__ is defined as 201112L (C11): the implemented surface is C99 plus the C11 features real code gates on this macro (_Static_assert, _Noreturn, _Atomic, _Thread_local, _Generic, anonymous members, <stdatomic.h>).

Unrecognized command-line options are fatal, severity 4

badc’s driver has no accept-and-ignore bucket: any dash-prefixed argument no option arm matches is an error, not a warning. Common gcc spellings badc does not implement – -x, -isystem, -static – therefore fail the invocation rather than being dropped. A build system that passes a compiler’s whole flag set through has to filter it; the kernel harness under demos/linux/ does exactly that.

An option badc parses but cannot fully honour is the exception: it is accepted, and the request it names selects the one behaviour badc has. -O1 / -O2 / -O3 / -Os / -Oz / -Ofast / -Og all select the single optimization level, -g<level> the single amount of debug information, and -mcpu=<name>, whose name must be an AArch64 part badc knows, a scheduling model badc does not differentiate; the instructions stay the baseline whatever the part. Only the -g family reports the gap, since a DWARF version and format are written into the output.

-g, -g0 .. -g3, -ggdb[0-3], -gdwarf, -gdwarf-<n>, -gdwarf32, -gdwarf64, -gstrict-dwarf and -gno-strict-dwarf are accepted with gcc’s meanings. badc emits DWARF version 4 in the 32-bit DWARF format, so a request it cannot produce – a version other than 4, or the 64-bit format – is reported as dwarf-output (B7011) naming what is emitted instead, and the compile proceeds. A spelling that names no request is rejected with gcc’s wording: a version outside 2 .. 5, a level above 3, or a non-integer where -gdwarf- takes one. -gsplit-dwarf, -gz and -gline-tables-only change the file set or the section contents and stay unimplemented, so they are refused by name.

The -W family follows the same rule against the diagnostic catalogue. -w, -Werror, -Wno-error, -Werror=<sel>, -Wno-error=<sel>, -W<sel>, -Wno-<sel>, -Wall, -Wextra and -Wpedantic are implemented; a selector is a diagnostic’s name, one of its aliases, its B code or a group name, and one no catalogue row answers to is refused by name. --list-diagnostics prints the catalogue.

The diagnostic pragmas – #pragma GCC diagnostic, #pragma clang diagnostic and MSVC’s #pragma warning(...) – take the same selectors and decide a row’s level at the source position they precede, for the parser’s diagnostics as well as the preprocessor’s. A pragma covering the position wins over the command line; push and pop bound the region it covers. A link diagnostic has no position in a translation unit, so the command line alone governs one.

-Wa,<opt> and -Xassembler <opt> are checked rather than passed on, since the assembler is built in: an option outside the accepted set is refused by name (unsupported assembler option) instead of reaching a program that is not there. The accepted set is -L / --keep-locals, which keeps the local-label temporaries in the symbol table as GNU as does, plus the options whose effect badc’s assembler already has – --fatal-warnings, -mrelax-relocations=, --noexecstack, --no-warn-rwx-segments and -march=. -march= is the one that is not exact: badc implements a fixed instruction set and admits every member of it, so a ceiling below that set selects nothing rather than rejecting the instructions above it.

-std=<dialect> is accepted. badc compiles C99 with the GNU extensions always available, so the name selects only whether __STRICT_ANSI__ is defined under --gnu: gnu* clears it and c<digits> / iso9899:* set it, as in gcc and clang. Without the flag --gnu reports strict conformance, so a header takes its standard-C path for the GNU features badc lacks. A name outside those three shapes is refused rather than read as strict ISO, so gcc’s -std=c2x fails the invocation while -std=c23 is taken.

ELF imports take their versions from a pinned manifest, severity 5

A GNU symbol version on an import is normally read out of the shared object the link host carries, which makes the image a function of the build machine: the same source and command line produced different bytes on hosts whose own libc differed, each diverging on the target it is native to, and a link on a current distribution stamped versions an older one cannot resolve. badc reads them from libc/versions/elf-<arch>.txt instead – committed data keyed on (soname, symbol), each entry the newest version at or below a pinned glibc 2.17 floor and the oldest the symbol has where the floor predates it. A library named on the -l path still decides its own version data, so -L <sysroot>/lib -lc pins the versions to that sysroot on any host, and a reference to a C library the bundled headers do not describe stays unversioned. A version at the floor can name a compatibility implementation whose semantics differ from the header badc ships for that name. TODO: hold the bound version and the declared interface in step.

Extensions implemented

C11 / C23

POSIX

GCC

MSVC-compatible

badc-specific

Roadmap

  1. Volatile-marked whole-aggregate copies (scalar volatile accesses are enforced; a volatile struct assignment’s block copy is not marked).
  2. x86_64 Windows UNWIND_INFO describes only the frame-pointer prologue (RIP/RSP/RBP recover exactly); callee-saved GPR spills are not yet described, so a debugger / profiler / SEH unwind crossing such a frame does not recover those registers. Program execution is unaffected – badc emits no exception-using code. A faithful description needs a push-before-setframe prologue restructure.

^ To the top