Fil-C on Nix

Filnix wiki

nanofil/docs/m4.md

M4: C frontend

The plan and decisions are in PLAN.md. This file records what exists. M4a (lexer, parser, types), M4b (lowering of functions), M4c (globals, string literals, function pointers, libc calls, main), M4d (structs and unions as values), and M4e (preprocessor) are done. M4f's pinned corpus and complete failure triage are in corpus.md.

cd nanofil/src
export FILCC=/path/to/filcc/bin/clang     # target profile and linking only
../bin/nanofil cc-ir prog.c            # the IR, as ir_text.pl clauses
../bin/nanofil cc prog.c > prog.s      # Fil-C assembly; .i still works

Pipeline

 C ──▶ c_pp ──▶ c_lex ──▶ c_parse ──▶ c_lower ──▶ IR (Fil-C module)
                     tokens     syntax      │  ▲
                                trees       ▼  │
                                          c_types
                                 specifiers, declarators, layouts,
                                 conversions, constant expressions

The real glibc headers from filcc's sysroot parse: stdio, stdlib, string, stdint, ctype, limits, errno, unistd, signal, math, pthread, time, sys/stat, dirent, wchar, inttypes, fcntl, sys/mman, getopt and setjmp. stdatomic.h is rejected. Struct layouts were compared with clang's sizeof, _Alignof and offsetof for stat, tm, timespec, dirent, pthread_mutex_t, pthread_attr_t, sigset_t, fpos_t, div_t and ldiv_t, and match.

GNU hello frontend additions

_Generic parses an unevaluated controlling expression and an association list. Selection applies lvalue/array/function conversion, but not integer promotion; typedefs and compatible pointer/array/function types match, and default is used only when no type matches. A scoped qtype(Name) entry preserves qualification for reflection without changing ABI types. Only the selected expression is lowered and it retains its value category. Duplicate compatible associations, duplicate defaults, incomplete object associations and missing matches are errors.

sizeof of an expression is available to integer constant evaluation as well as function lowering. Array names already decay to relocation-bearing address constants in static initializers. Block-scope static initializers are now resolved while their local names (including enums and typedefs) are still in scope, rather than after restoring the enclosing scope.

__builtin_unreachable() ends a block with unreachable. __builtin_alloca(n) emits a bounded, 16-byte-aligned alloc at the call site; see the ABI findings. GNU hello's same header expansions also need __builtin_constant_p, which queries integer constant evaluation without evaluating its argument and returns 0 when it cannot establish a constant.

Preprocessor (M4e)

src/c_pp.pl preprocesses the source and headers itself; none of their contents is sent to clang. The DCG scanner retains token spellings, preceding whitespace, original file/line and a macro hide set. Only after replacement and rescanning does c_lex classify each spelling into the parser's existing tok(T, File:Line) terms. .i input still goes straight to c_lex, including its line-marker support.

Target profile decision: discover predefined macros with $FILCC -dM -E -x c /dev/null and include directories with $FILCC -E -v -x c /dev/null once per invocation (FILCC defaults to filcc). Embedding either would duplicate the toolchain pin and bake Nix store paths into Prolog. Clang's feature operators are absent from -dM: the adapter batches glibc's __has_attribute, __has_builtin, __has_extension, __has_feature and __building_module queries, and queries other single feature names on demand. Only target-property probes go through clang; nanofil performs all source preprocessing. Keeping this profile discovery requires the selected Fil-C executable, which is also needed for linking. No replacement headers or per-header output patches are used. Header-driven additions were wide character constants in #if (bits/wchar.h), the feature operators (sys/cdefs.h), and the next-include operators in Clang's resource headers.

Acceptance: tests/pptest.sh, part of the Nix check, compares location-free c_lex terms against filcc -E for each of the 20 headers listed above and every .c recursively under tests/c (including rejections). Generated probes cover the macro rules, the C standard's recursive rescanning example, once-only includes, integer conversions, short-circuiting, #error, original-file/line assertions, spliced #line, multiline comments in directives, and nested macro save/restore. #line and macro save/restore also preserve M4f's existing passing cases c-testsuite/00152.c and c-testsuite/00206.c. The existing differential, whole-program and rejection scripts now compile .c directly through nanofil. To prove the comparator detects a regression, object macros were temporarily changed to expand to nothing: a #define VALUE 42 probe failed with PREPROCESSOR MISMATCH and a missing int(42,-(none,dec)) token (exit 1). Restoring expansion made the same command pass (exit 0).

Lowering

Globals, literals, function pointers and libc (M4c)

The ABI facts are in abi-findings.md, read from the runtime and pass at the pin.

IR. A Fil-C module may now have data. item(p, sym(Sym, Off)) is a pointer data item, and linkage ro(Lk) marks read-only data (in .rodata). A symbol appears in code only as let(tmp(T), p, copy, [global(Sym)]), which is a call to its getter, and call(Res, tmp(T), Args) calls through a p value. A module that calls a function it does not define is a Fil-C module.

Interpreter. Globals are 16-aligned objects with sizes rounded up to 8, as Fil-C lays them out; read-only data has capability ro(Base) and traps on writes. Each function has an address with capability fn(F), a zero-size object, so loads through it trap. An indirect call traps unless the pointer is exactly a function's; a call whose classes differ from the callee's goes through the generic convention, so too few arguments or using a void result traps, as the generic entry's size checks do.

Backend.

Frontend. Globals with constant initializers (integers, address constants such as &a[2], "str", function names, &s.f, with designators and brace elision), tentative definitions, static locals (renamed fn.name.N), string literals as read-only str.N data, function designators and pointers, and __func__. static inline functions are lowered only when something refers to them, as clang does; headers are full of ones nanofil cannot lower.

Non-static inline definitions follow C99: any file-scope declaration with extern or without inline, even after the body, makes the body an external definition. Otherwise it emits no definition. GNU gnu_inline/__gnu_inline__ attributes retain the GNU89 rule instead: extern inline emits no definition, plain inline does. The attributes are already retained by the parser in specifiers and declarator suffixes. tests/c/prog/extern-inline.c tests both rules across translation units against clang, including declarations before and after the body and the unchanged static inline behavior.

Structs and unions as values (M4d)

The ABI facts are in abi-findings.md.

IR. One addition, a two-word result, for structs of 9 to 16 bytes: function {l, p} $f(...), ret {%a, %b} and {%a, %b} ={l, p} call ... (terms tuple([K1, K2]), rets([V1, V2]) and call(tuple([to(D1, K1), to(D2, K2)]), ...)). Each word is l or p. A tuple makes the module a Fil-C module. Everything else a struct needs is lowered by the frontend into existing IR: struct arguments are their words, one-word structs return as l or p, and larger returns take an sret pointer as the first argument.

Interpreter. rets gives the outcome rets([X1, X2]) and a tuple call binds both temporaries. The generic convention of indirect calls now works word by word for results too: the call site may read fewer words than the callee returns, not more.

Backend. Results with at most two slots (a p word is two) come back in RDX:RCX; {l, p}, {p, l} and {p, p} use the caller's buffer, with all registers shifted by one. Functions, direct and indirect calls, generic entries and thunks all handle both forms. A buffered call keeps its buffer on the native stack for the moment between the return and copying the slots into the frame; there is no safepoint in between.

Frontend. A struct rvalue is the address of storage holding it (a variable, or a temporary local for a call result), so a = b, return s, f(s), c ? s1 : s2 and f().x need no struct temporaries in the IR. A copy of more than 40 bytes, or of a packed type that holds a pointer, calls the runtime's zmemmove (filcc's cutoff, FilPizlonator's InlineMemmoveDstSizeLimit), which checks both ranges first, keeps each 8-aligned capability and handles overlap. Smaller copies and conversions to words go an eightbyte at a time: load(p) and store(p, ...) where a pointer starts, so capabilities copy exactly (decision 6), else load(l)/store(l, ...), with a short last eightbyte moved in 4-, 2- and 1-byte pieces (struct {char a, b, c} is a halfword and a byte). A struct expression initializes a struct member or array element whole rather than by brace elision. Struct arguments over 16 bytes (and misaligned packed ones) are byval: passed as their eightbytes, and a function taking one is generic-only, promoting its argument packet to the heap as a variadic function does (decision 7, abi-findings.md Byval arguments). Such a struct's float members do not matter, since it never travels in SSE registers. Smaller structs with float members travel in SSE eightbytes as SysV classifies them (abi-findings.md, Structs with float members), and va_arg of a struct or union reads it a cell per eightbyte. tests/c/prog/fstructs.c checks them and clang's signatures across the boundary. tests/c/prog/byval.c passes them across the clang boundary both ways, directly, indirectly, past a prototype's ... and through wrong prototypes, and traps through a pointer field that made the round trip.

Initializers. An array of unknown size counts the entries each element takes with brace elision (struct {int a, b;} s[] = {1, 2, 3, 4} has 2 elements, not 4), and an elided aggregate stops at the next designator, which belongs to the enclosing list. Both were found by the M4f corpus (c-testsuite 00205).

Bit-fields

A bit-field is a member m(some(F), Type, Off, bits(Pos, W)): W bits from bit Pos of the Type-sized storage unit at byte Off. Layout follows the SysV psABI (and clang): a bit-field takes the next bits unless it would cross a boundary of its type's size, where it starts instead; a zero-width one moves to that boundary; an unnamed one takes space but does not align the struct, and a named one aligns it as its type. The unit is aligned to its own size inside a struct aligned at least as much, so every unit access stays inside the object, and a unit never shares an eightbyte with a pointer, so the Fil-C capabilities are untouched.

The elaborator marks a bit-field lvalue obj(T, bitfield(member(XS, F), W)). Its value has the promoted type of bitfield_promotion/3: int when an int holds every value, else unsigned int when that does, else its own type (C11 6.3.1.1p2, and clang's rule for long bit-fields). & and sizeof of a bit-field are refused. Lowering reads the unit and shifts the field to the bottom, extended by the type's sign; a store loads the unit, masks the field in and stores it back, and the value of an assignment, ++ or -- is the field read back from what was stored. Static initializers merge each bit-field into the bytes it covers. tests/c/bitfields.c compares the interpreter, nanofil and clang on layouts, signs, neighbours kept by stores, promotion, assignment values, initializers, copies and struct arguments. Packed bit-fields, which GCC places differently, are rejected.

Packed structs and unions

GNU's packed attribute (__attribute__((packed)) before the tag or after the body, or [[gnu::packed]]) gives every member alignment 1, and the struct too (su_layout/5). Non-pointer accesses already check no alignment, so misaligned members load and store like any other; copies and static initializers are bytewise as before. A packed pointer is laid out like any member. Fil-C keeps a capability only at an 8-aligned address, and each pointer load and store already checks at run time that its address is 8-aligned, with filcc's trap ("alignment requirement of 8 bytes not met"), so libmbim's fragment_info and elfutils' Dwarf_Abbrev (a pointer at a multiple of 8) work where they are placed 8-aligned, and parted's GPT header (Reserved2 at 92) traps where parted stores it, as with filcc. A struct aligned below 8 that holds a pointer is copied by the runtime's zmemmove (not a memcpy the program may define), as filcc copies it, so a copy never traps and keeps a capability only where both ends are 8-aligned; passing one by value loads its eightbytes, which traps at a misaligned address as filcc's does. A static initializer that puts a non-null pointer at an offset that is not a multiple of 8 is refused, as filcc refuses it ("a pointer in its initializer is not word-aligned"). A packed enum has the smallest integer type that holds its constants, unsigned unless one is negative, as GCC and clang give it (svt-av1, libepoxy, elfutils). A packed bit-field follows the bits before it across any boundary, as in GCC and clang (parted's GPT attributes); one that no access of its type within the struct covers is accessed, as clang does, in the narrowest integer of its sign that fits (parted's one-byte FatClusterInfo of unsigned int:6 and an enum :2) when its type is no wider than int, which keeps its values and their promotion, and is refused otherwise. #pragma pack(N), with push and pop, caps member alignment at N for the definitions after it (tpm2-tss's command headers): c_lex marks each struct or union keyword under it with an internal attribute, so .c and preprocessed .i input agree; a pointer it underaligns is checked at run time as a packed one, and a bit-field or explicitly aligned member it would underalign is refused, as is a named push or pop. Until this, nanofil ignored the pragma. A struct with a misaligned member, perhaps nested, has SysV class MEMORY (unaligned_type/2): it is returned through a hidden pointer as clang does, and passed by value byval. aligned stays unsupported. tests/c/packed.c compares layouts and accesses three ways; tests/c/prog/packed.c returns a misaligned packed struct across the clang boundary in both directions.

Small C99 and GNU additions

GNU statement expressions ({ ... }) open a block scope and yield the last expression statement's value (including a labelled expression), or void otherwise. Nested expressions, shadowing and header macros such as glibc's assert work. Labels remain function-scoped: gotos may stay within or leave an expression, but entering one (also from a sibling expression) is rejected, as is switch dispatch from outside to a case/default inside. Computed gotos are described under "Linux userland additions" below. Break/continue retain the enclosing statement context; in loop conditions/init/increment they target an outer loop, as GCC documents, not the loop being constructed. The pinned clang differs for condition-expression break/continue, so those cases must be checked against GCC instead. Evaluation is not interleaved with sibling operands. tests/c/stmtexpr.c compares values, void, nesting, scope, assert, labels and outward jumps against clang.

Compound literals (a block-scope one is an unnamed local initialized where it appears; a file-scope one is unnamed static data), wide character and string literals (L is int, u is unsigned short, U is unsigned int; elements are code points), u8 strings, GNU c ?: b with c evaluated once, and __builtin_expect. In initializers: GNU range designators [a ... b] = v, each element initialized as if designated alone (so v may not have side effects, which GCC would evaluate once); a designator naming a member of an anonymous member (C11 6.7.2.1p13); and GNU constant compound literals of the slot's own struct type in static initializers, read as their initializer lists. tests/c/c99.c covers them three ways.

SQLite additions

SQLite 3.51.2's amalgamation and shell (SQLITE_THREADSAFE=0, SQLITE_OMIT_LOAD_EXTENSION, FTS5 and the math functions) build with nanofil alone; nix/sqlite.nix compares them with a filcc build. They needed:

tests/c/prog/sqlite-idioms.c and tests/c/prog/gnu-math.c compare these with clang.

Atomic builtins

__atomic_load_n(P, Order) and __atomic_store_n(P, V, Order) take a pointer to an integer or a pointer and a constant Order: relaxed, consume, acquire or seq_cst for a load, and relaxed, release or seq_cst for a store. Other orders and the other __atomic builtins are refused with a message.

tests/c/prog/atomic-ptr.c runs a writer and a reader thread on one slot, nanofil against clang in each direction, with two objects of different sizes: a torn pair traps. It also runs a store-buffer litmus of nanofil's seq_cst store against clang's.

Linux userland additions

GNU sed 4.10, jq 1.8.2 (with its bundled oniguruma and decNumber) and dash 0.5.13.3 build with nanofil alone, each with filnix's port patch; nix/sed.nix, nix/jq.nix and nix/dash.nix compare them with filcc builds on workloads in nix/workloads and run their test suites. They needed:

Refused, with a message: inline asm other than the empty barrier (gnulib's x87 control-word tests); a stronger alignment request; a pointer converted to bool in a static initializer (bool b = &s;, a GNU extension); classifying a long double other than by its sign. dash's vfork is fork, as filnix's port has it, since a vforked child would share the collector's heap. gnulib's test-intprops.c, whose main has 87,000 blocks (constant conditions are still lowered as branches), compiles with -O in about 150 s within the default 1 GB stack: CFG liveness found unreachable blocks quadratically until 3d3d251, and main.pl's release_stacks/0 now returns SWI's grown stacks between stages, since its stack limit counts allocated, not live, memory.

tests/c/prog/userland-idioms.c, c23.c, fortify.c, sockets.c, computed-goto.c, alias.c and source-bytes.c compare them with clang.

xz additions

xz-utils 5.8.4 builds with nanofil alone, with the C changes of filnix's port patch; nix/xz.nix compares it with a filcc build on nix/workloads/xz.sh, which runs liblzma's threaded encoder and decoder at 1 to 8 threads, and runs xz's test suite. It needed:

Refused, and configured out of both builds: vector types and the SIMD intrinsics, which xz uses for its CLMUL CRC (--disable-clmul-crc) and its LZ decoder's SSE2 copy (LZMA_LZ_DECODER_CONFIG=1, the memcpy variant, which lz_decoder.h lets the build choose). tests/c/prog/aligned.c, unused-intrinsics.c, bit-cast.c, constructor.c and vararg-thunk-width.c compare them with clang.

coreutils additions

GNU coreutils 9.11 (107 programs, gnulib at scale) needed three changes:

Refused, and configured out of both builds: vector types (the AVX2, AVX-512 and PCLMUL paths of wc and cksum), _Float16 and __bf16 (od), and factor's longlong.h asm (USE_LONGLONG_H=0). Some commands stop with a Fil-C safety error in both builds alike: seq and printf on floating formats (the fpucw asm above), and yes into a pipe, which reaches glibc's vmsplice syscall asm.

CPython additions

CPython 3.12.5 with filnix's port (the version the port pins; nixpkgs' 3.13 is marked broken there), its interpreter, test modules and 62 extension modules, needed:

__uint128_t is configured out of both builds (libmpdec takes its portable path): nanofil refuses __int128. Frames are larger than clang's: _PyEval_EvalFrameDefault's is 24,880 bytes plain and 9,872 with -O, against 3,640 for clang -O3, so Python's recursion tests overflow an 8 MB stack before Python's recursion guard stops them (Fil-C reports the overflow); the check runs both interpreters on a fiber with a 256 MB stack (nix/python-main-stack.patch; Fil-C sizes a thread's stack by RLIMIT_STACK, whatever its attributes ask), so it does not depend on the builder letting it raise RLIMIT_STACK (test_isinstance's test_infinitely_many_bases recurses about 2,400 levels of about 27 KB each in the plain build, past 64 MB).

SWI-Prolog additions

SWI-Prolog 9.2.9 with filnix's port (checks.nanofil-swipl, 22 packages) needed one frontend change:

The plain build needs a 32 MB C stack for SWI's tests: analyseVariables2 recurses 10,000 levels into a cyclic term before it checks for a cycle, and its frame is 864 bytes plain and 512 with -O, against clang's 216.

Thread traps on chapel (November 2026). One nanofil-swipl run had -O's swipl:GC trap on chapel (an 8-core i7-10700K, kernel 7.2), on the parallel run and the serial rerun; swa (a 32-thread 7950X3D) never showed it. Looping swipl:GC's ctest command on chapel, four at a time, made filcc's build trap too, more often than nanofil's: 5 times in 36 runs, against 2 in 61 for -O. All the traps were SWI's own races, which Fil-C turns into safety errors:

Under Fil-C a plain pointer load reads the capability and the address as two loads (filc_load_ptr_with_manual_tracking, filc_runtime.h: the lower from the location's atomic box if it has one, the address from primary memory), so it can tear against a racing store or CAS, even when the location is only ever written with CAS; only an atomic load (filc_load_ptr_atomic_with_manual_tracking) reads both from the box. volatile does not make a pointer access atomic. A torn pointer always traps (invisicap.txt, gimso_semantics.md), so these are crashes, not memory unsafety. The port now reads and writes those pointers with __atomic builtins, and acquire_ldata revalidates its hazard pointer. Then the same loop gave 0 traps in 200 runs for filcc's build and 0 in 272 for -O's. nanofil compiled both races as filcc did. Its -O code is slower, so it hit the windows less often.

swipl:thread, looped the same way at eight to twelve at a time, and full ctest runs showed more of the same: free_lingering's CAS'd list, a predicate's clause_indexes entries, thread_gc_loop's CAS'd list, sizeof_local_definitions (thread_property(T, size(_)) reading another thread's local definitions), markAtomsThreadMessageQueue (atom GC scanning another thread's message queue) and I_LCALL's DEF = proc->definition, a pointer autoloading replaces from another thread (swipl:signals' test_bomb, "ptr < lower"), all in filcc's build as in nanofil's.

So the port declares every pointer that other threads replace while one reads it without a lock ATOMIC_PTR(T) (pl-incl.h), which is _Atomic(T) when SWI is built with threads: every load and store of such a field is then a sequentially consistent atomic, and no reader can be missed. That covers the COMPARE_AND_SWAP_PTR targets (the atom, functor, indirect and lock-free hash tables' buckets, chains and maps, the dynamic arrays' blocks, the tries' children and indirect tables, the tabling and IDG roots, the linger lists, free lists and module wait areas, stream contexts), the clause lists, a procedure's definition, a predicate's supervisor code (def->codes and a wrapped predicate's supervisor), each thread's local definitions and message queue, the thread table and the hazard pointers in info->access. def->codes showed up only after the rest were atomic: one of 300 swipl:thread runs of nanofil's -O build on chapel trapped in PL_next_solution with an address from heap code and the capability of PL_code_data, the static supervisors that createSupervisor replaces while other threads run the predicate. The packages without pl-incl.h (semweb, sgml, ssl) write _Atomic directly. The first fixes' __atomic calls became plain accesses of such fields; clause_indexes's entries (CI_GET/CI_SET) and the pointer table keep their builtins. Measured on swa, the port with atomics against the one without, single-threaded: the SWI workload 4.96 s against 4.91 s and nanofil's unit suite 13.5 s against 13.3 s (1 to 2%, mean of 5 and 3 runs; an earlier pair of runs gave 5% and 3%). On chapel, the same loops as above, six at a time with both builds looping at once (load about 60 on 16 threads): swipl:GC, swipl:signals and swipl:thread 300 times each, in filcc's build and nanofil's -O, 1,800 runs with no failure and no trap. The port's checkPhase and the check no longer rerun failed tests; only http:proxy, whose "unused" port a test beside it can take, runs once more when it fails.

Left as upstream wrote them: arrays whose blocks are stored under a lock and read only after an index into them is published (functors, source files, indirect values), and pointers in structures that one thread fills before it publishes them.

Atomic read-modify-write

A compound assignment or ++/-- of an _Atomic object (6.5.16.2p3, 6.5.2.4p2), <stdatomic.h>'s exchange, compare-exchange (strong and weak) and fetch-and-op, clang's __c11_atomic_* forms, GNU's __atomic_exchange_n, __atomic_compare_exchange_n, __atomic_fetch_OP and __atomic_OP_fetch, the __sync_* builtins, __atomic_test_and_set and __atomic_clear are each one cas or a cas loop (cas(T), docs/pizlonation.md), on integers, pointers, and floats and doubles for compound assignment and the __c11 exchange and compare-exchange (which compare bits: 0.0 is not -0.0, as in clang). Refused: GNU __atomic_fetch_add on a pointer (filcc's clang breaks its module), __c11_atomic_fetch_add on a floating _Atomic, and a read-modify-write of a packed member that may be misaligned (clang calls libatomic for it; underaligned_member/4). atomic-rmw.c hammers a counter from 8 threads (filcc-built and nanofil-built writers mixed) and runs a lock-free stack of pointers; atomic-misaligned.c traps on a misaligned counter as filcc does.

Variable length arrays

C11 6.7.6.2 as c_types states it. An array size that is not an integer constant expression gives the type array(T, vla(E)), keeping the size expression E ([*] is vla(star)); vm_type/1 (6.7.6p3) and vla_type/1 (6.7.6.2p4) say which types are variably modified and which are VLAs. A variable length is compatible with any length (6.7.6.2p6). Parameter names are in scope for the later parameters' sizes. A comma operator folds only when its left operand does, so the length of int[(c++, 5)] is not a constant and c++ happens. A struct member may not be variably modified (6.7.2.1p9).

Lowering evaluates each vla(E) once, where C does (realized//3): at a declaration when it is reached (including typedefs and static pointers to VLAs), in a cast, va_arg or compound literal's type name, in sizeof of a VLA type, and for each variably modified parameter on entry, after all parameters are bound (6.9.1p10). The length, converted to long, becomes the type's dyn(V), V an IR temporary; outermost lengths are evaluated first. extent//3 computes a size from those lengths, so sizeof, pointer arithmetic and pointer differences scale at run time. sizeof evaluates its operand only when it has a VLA type (6.5.3.4p2): sizeof(c++, p) for a pointer to one does not.

A VLA object is an alloc of its run-time size at the declaration, not hoisted to the entry block, as __builtin_alloca is (abi-findings): a fresh Fil-C object each time, whose capability bounds are the actual size (rounded up to 16 bytes by the runtime, as clang's are), so an access past the length traps. No IR term changed. Lowering uses the evaluated type a tree node's lowered pointer carries (vm_typed/3) where the elaborator's type still holds an unevaluated type name's vla(E).

The temporaries holding lengths and VLA addresses are defined where the declaration is, so lowering enforces the constraints that keep them dominating every use: no goto into the scope of a variably modified identifier (6.8.6.1p1), no switch case inside one the switch is outside (6.8.4.2p2). Also refused: static VLAs, variably modified objects at file scope or with linkage (6.7.6.2p2), initialized VLAs (6.7.9p3), VLA compound literals (6.5.2.5p1), and [*] outside a prototype. tests/c/vla.c compares interpreter, nanofil and clang on storage, bounds traps, multidimensional indexing, evaluation counts, sizeof, casts, parameters, loops and backward jumps; tests/c/reject/vla-*.c hold the refusals.

Tests

Known gaps