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 worksPipeline
C ──▶ c_pp ──▶ c_lex ──▶ c_parse ──▶ c_lower ──▶ IR (Fil-C module)
tokens syntax │ ▲
trees ▼ │
c_types
specifiers, declarators, layouts,
conversions, constant expressions
- src/c_lex.pl: tokens
tok(T, File:Line), following# N "file"markers, so errors point into the original source. GNU spellings fold into keywords (__restrict__isrestrict). Integer constants keep their radix, because C types4294967295aslongbut0xFFFFFFFFasunsigned int. - src/c_parse.pl: a C11 parser whose
only state is the set of typedef names in scope, enough to tell
declarations from expressions (a local
int node;hides a typedefnode). Declarator types are built outermost first around the atombase, soint (*c)[5]isptr(array(base, 5)). Attributes after declarators are kept asattr(Tokens)specifiers; asm labels are kept as renames. - src/c_types.pl: type terms, SysV sizes
and struct layout, integer promotions, the usual arithmetic conversions,
and integer constant expressions (
sizeof,offsetof, casts, enum constants). A type nanofil cannot handle yet isbad(Reason), an error only when a declaration uses it, so whole glibc headers still parse. - src/c_lower.pl: cproc-style lowering.
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.
- Object and function macros, empty arguments, variadic
__VA_ARGS__(also GNU named variadics), argument prescan,#,##, placemarkers and rescanning. Hide sets prevent direct and indirect recursion, including when a replacement consumes a call from the remaining input. Arguments used only by#or##are not prescanned. - Quoted includes search the including file's directory first; angle
includes use the target search list. Literal header names are not macro
expanded (
linuxis a predefined macro!), but macro operands are.#include_nextand__has_include_nextsearch after the current system directory.__has_includetests the same search path. - Nested conditionals,
defined,#undef,#errorand ignored pragmas;#pragma onceremembers a canonical file name, and nestedpush_macro/pop_macrosave and restore definitions (including undefined macros). Integer conditions use 64-bit intmax/uintmax conversions and exact integer division.&&,||and?:do not evaluate unselected arms. __FILE__and__LINE__refer to original sources. A parallel location vector survives line splicing, including splices inside tokens. Macro replacement tokens point to their invocation, not to the definition in a system header. Macro-expanded#linechanges logical locations without changing the physical directory used for includes and__has_include.
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
- Locals. Every local and parameter is an
allocin the entry block, and each use is a load or store, as in cproc before QBE'spromote. An initializer zeroes the whole object first, because a hoisted alloc is reused across loop iterations. Initializer lists support designators and brace elision. - Narrow integers are always held sign- or
zero-extended to 32 bits in a
wvalue. A conversion emits anextonly if some value of the source type does not fit the target (inttounsigned chardoes,unsigned chartoshortdoes not).inttolongisextsw/extuw, andlongtointneeds no instruction, since awuse of anlvalue reads its low half. Narrow call results are re-extended, because nothing guarantees the callee did it. - Pointers are
p:p + iscalesito alongand adds it withaddatp, which keeps the capability; differences and comparisons go throughptoi. A constant integer used as a pointer is a capability-freepconstant, so*(int *)0traps. &&,||and?:produce phis.?:lowers each arm once against a throwaway copy of the lowering state to find the result type, then again with the conversion;sizeof expruses the same trick.switchjumps to a dispatch block after the body, a chain of compares filled in once allcaselabels are known.
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.
DO_g,GP_g,DA_g(only if the payload holds pointers), a relocation table, and a getter whose slow path isfilc_global_initialization_start,filc_execute_constant_relocations,filc_global_initialization_end, with the flags clang uses (0x1, 0x11, 0x3, 0x13). Pointer slots are 0 in the static data and filled in by the relocations; a pointer with an offset uses a constexpr node.copy $ssets the frame's call-site origin and calls the getter, so a getter is a safepoint like any call.- Every function now has a getter, generic entry and function object, so local functions can have their address taken.
- Indirect calls check for a function object (lower ≠ 0, ptr = its aux
pointer, special type function, else
filc_check_function_call_fail), then use the fast entry if the signature word matches and the generic entry otherwise, checking the returned size. - Each external callee gets a hidden weak thunk
pizlonatedFI<sig>_f, as clang emits: getter, function-object check, fast or generic entry. A clang definition in the same link wins. This is how libc is reached. mainneeds nothing special: the runtime callspizlonated_main.
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:
Integer address constants. An integer constant cast to a pointer is an address constant in a static initializer (C11 6.6p9), as in SQLite's
SQLITE_INT_TO_PTR(1). It becomes an address with no capability, as clang emits it, and 0 the null pointer. Dereferencing one traps in both builds.__builtin_nan(""),__builtin_nanf("")(glibc'sNAN) are constants: the quiet NaN with a zero payload. A non-empty payload string is refused.__builtin_ceil,__builtin_floorcall libm'sceilandfloor, which compute the same exact result as clang's inlineroundsd.Large translation units. sqlite3.c is 260,000 lines. The pipeline commits to each statement, function, item and block, so finished work leaves no choicepoints that keep it alive. Frame-slot liveness shares the per-block live sets instead of copying a point per temporary, and the assembly prints one item at a time from atomic parts, not character lists. Whole-module lookups (a function by name, a function's provenance context, whether a callee is variadic) go through indexes built once per pass, never a scan of the items per use, and each full expression is elaborated once. The output is unchanged. With FTS5 the amalgamation compiles in about 65 s within SWI's default 1 GB stack, and with
-Oin about 3 minutes, also within it, since main.pl'srelease_stacks/0collects and trims SWI's stacks between stages (its limit counts allocated, not live, memory).NANOFIL_STACK_LIMIT=BYTESraises the limit, from source or a saved state, as a fallback. nanofil-cc (the package builds and the survey) compiles every file within the default 1 GB first, and only a compile that overflows it (resource_error(stack)) is compiled once more withNANOFIL_STACK_LIMIT=4000000000, saying so on stderr. So parallel builds meet the larger limit only where a file needs it. gettext's 31 MB tree-sitter OCaml parser (about 11M tokens) needed it while a.ifile was read into one character list before lexing; since c_lex lexes a.ia line at a time it compiles within 1 GB, in about 177 s at 2.3 GB RSS.With the port's flags (nixpkgs' NIX_CFLAGS_COMPILE: FTS3 and FTS4, R-Tree, Geopoly, sessions, RBU) more of sqlite3.c is compiled. Without
-O, as the survey builds it, it fits since release_stacks also runs between the frontend and the validator (nanofil-sqlite'sportconfiguration, 90 s). With-Oit is at the limit, a known limit: one run passed in 259 s, another overflowed, and-O --checks=reuseoverflowed in copy and provenance. Live global stack after a collection, by stage: tokens 71 MB, parse 100 MB, c_module 320 MB, valid_module 220 MB, optimize 364 MB, pizlonation 484 MB, and emission holds the whole pizlonated module. Fitting it would need per-function pizlonation and emission (M7's area); the plain, O and reuse configurations keep the check's smaller flag set.SWI collects the global stack only once it holds three times what the last collection left (
set_prolog_stack/2's factor), and grows it otherwise; at the limit it raises an overflow without collecting. sqlite-interactive's sqlite3 (the port's flags and the shell's, a 4.2 MB.i) then needed between 800 and 950 MB, and overflowed in the survey's busier builds, which collect at other moments. swi/compat.pl sets the factor to 2: it fits between 650 and 800 MB, in the same 84 s. gtk3's and gtk4's 11 MB resource blobs, 256 MB live while lexing, needed more than 850 MB and now fit in 750 MB.
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.
- Integers. A load is
*Pand a relaxed or release store is(void)(*P = V): on x86-64 an aligned integer access is already atomic, and none of those orders needs a fence. A seq_cst store is the store, then IRfence(mfence). Clang emitsxchg; both are trailing-fence seq_cst stores, so they combine with each other and with plain-movseq_cst loads. - Pointers, at every order, use IR
atomic_load(p)andatomic_store(p, V, A). Pizlonation gives them the ordinary inline check and the effect lists[atomic_read]and[atomic_write], which call Fil-C's helpers as clang does:filc_load_ptr_atomic_with_manual_tracking_outlineandfilc_store_ptr_atomic_outline(abi-findings.md). They keep the address and capability one unit (an atomic box in the slot's aux) and are sequentially consistent. Escape analysis makes an atomically accessed object escape, so it stays a heap object; stack aux has no boxes. The interpreter, single-threaded, treats them as plain pointer accesses andfenceas nothing.
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:
- C23 keywords. configure gives sed
-std=gnu23, and gnulib's headers then spellbool,true,false,nullptr,static_assert,alignas,alignofandthread_localas keywords. nanofil-cc setsNANOFIL_STD=c23for-std=c23,gnu23,c2xorgnu2x, and c_lex'sc23_tokens/2then reads those spellings as keywords; C17 stays the default.trueandfalseare(bool)1and(bool)0;nullptris(void *)0, sonullptr_tisvoid *(only_Generictells them apart). progtest's// cflags:line builds a test with such flags. _FORTIFY_SOURCE. gnulib's config.h turns it on when optimizing, and glibc's headers then call__builtin_object_sizeand the__builtin___*_chkbuiltins. nanofil answers that an object's size is unknown ((size_t)-1for types 0 and 1, 0 for 2 and 3), which GCC documents as correct for any pointer. A__builtin___X_chkcall is a call of the library's__X_chkwhen it is declared (glibc's printf family), else ofXwhen the size is(size_t)-1. Clang computes real sizes, so__builtin_object_sizecan answer differently; Fil-C checks every access either way.- clang's
overloadable. glibc's fortify headers overloadopen,open64andopenatby arity. A declaration or definition marked overloadable whose type is not compatible with the visible one is skipped, and calls reach the original function; glibc's overloads only check the flags before calling it. - Alignment requests that change nothing. An
_Alignasoraligned(N)no stronger than the member's own alignment is dropped (C11 6.7.5p4), as in glibc's and clang'smax_align_t. A stronger one is still refused, except on a struct or union definition (xz additions below). transparent_union. glibc's socket functions take__SOCKADDR_ARGunder_GNU_SOURCE. A parameter of a transparent union whose members are all pointers has its first member's type, so any member's pointer passes, as GCC passes it.cleanup. GLib'sg_autoptr,g_autofreeandg_auto, and systemd's_cleanup_, attachcleanup(F)to an automatic variable v:F(&v)runs on every way out of v's scope (the end of its block,returnafter its value is taken,break,continue,goto), in reverse order of declaration. A jump into such a scope and F of the wrong type are refused as clang refuses them; so is agoto *inside one, which clang compiles. On a variable that is not automatic it is ignored, as clang ignores it with a warning. One difference remains: a function that returns a struct copies it before the cleanups run, where clang, returning a named local in place, lets the cleanup change the returned value (docs/abi-findings.md, Cleanup and a returned struct). Before this was supported, nanofil ignored the attribute, and dconf's leak tests leaked a GDBusConnection reference.- Labels as values.
&&Lis(void *)N, N the label's number in its function, an address constant with no capability (so static tables of labels work). Everygoto *Eof a function stores E and jumps to one dispatch block, a search among the function's address-taken labels; any other value reachesunreachable, a Fil-C panic. oniguruma's bytecode interpreter dispatches so. A label value is meaningful only in its own function. - The
aliasattribute (dash'sbgcmd; fontconfig's, gettext's and gtk2's object aliases) binds the name to the target's symbol. An external or weak alias is also the IR itemalias(Name, Linkage, Target), which filc_amd64 emits as filcc does: a getterpizlonated_<Name>that calls the target's getter, so another translation unit reaches the target's function or data object, at the target's address. The validator requires the target to be a function or object the module defines, and a data alias never to be called. Functions of any type (variadic and unprototyped included) and objects alike; a function alias of an object, an alias of thread-local data and several declarators are refused. - Source bytes. Files are read as bytes. A narrow
string keeps the bytes as written, valid UTF-8 or not (oniguruma's tests
are EUC-JP), and a wide string decodes UTF-8, as clang does. c_pp keeps
u8"..."one preprocessing token. - Smaller pieces.
typeof(C23 6.7.2.5) of an expression or a type, not of a variably modified type; the empty compiler barrierasm volatile("" ::: "memory"), which emits nothing; GNUvoid *arithmetic (steps of 1);sizeofand_Alignofof a type name that defines a tag;sizeof (T[]){...}; static assertions among struct members (C11 6.7.10, gnulib'sverify_expr);__builtin_isnan,__builtin_flt_rounds, the classification builtins of glibc's math.h (isinf_sign,isfinite,isnormal,signbit,signbitf,signbitl,fpclassify) and long doublehuge_vall,inflandnanl. - Thread-local variables. jq's libjq keeps three
static __threadobjects._Thread_local, C23thread_localand__threadwithstatic,externor at file scope give the object thread storage duration (C11 6.2.4p4): its data has linkagetls(L)(ir.pl), and filc_amd64 emits clang's per-thread getter and slow path (abi-findings.md). Refused, as clang refuses them: a block-scope thread-local that is neitherstaticnorexternand a thread-local function (6.7.1p3-4), and&xof one in a static initializer (6.6p9). tests/c/prog/thread-local.c runs eight threads against clang's side; before this, the storage class was dropped and every thread shared one object (a block-scope one even became automatic). - Nested
?:cost. A conditional checks whether an arm is a null pointer constant only when the other arm is a pointer; folding every arm refolded each subexpression once per enclosing?:(gnulib's intprops tests went from minutes to seconds in the frontend).
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:
aligned(N)on a struct or union definition (xz'sio_buf): the alignment is the larger of N and the members', the size rounds up to it, packed or not; barealignedis 16, clang's__BIGGEST_ALIGNMENT__(c_typesdefinition_alignment/3). nanofil's globals and allocations are 16-aligned, so N above 16 is refused, as arealignedon a typedef, an object or a member, and_Alignasstronger than natural.- Eightbytes with no data. A struct of at most 16 bytes passes only the eightbytes that hold a member (SysV NO_CLASS, abi-findings.md).
- Header intrinsics that are never called. xz
includes
<immintrin.h>, which declares vector types,_Float16and__bf16vectors, and inline functions with asm bodies. Inline asm other than the empty barrier parses asunsupported_asmand is refused where a function containing it is lowered, so an unusedstatic inlineone is never refused, as clang never emits it.vector_size(and clang'sext_vector_type) makes a bad type, refused where it is used; it was silently ignored before, which gave a vector the size of its element._Float16and__bf16are refused types. Astaticfunction that nanofil cannot lower is deferred as astatic inlineone is, and its error is raised only if something refers to it (amxcomplexintrin.h's__tile_cmmimfp16pstakes a struct with a vector member by value); clang emits an unreferenced static function not at all, and the output of every function that lowers is unchanged. __builtin_bit_cast(T, E), of two complete object types of one size, T not an array: E's value is stored and loaded as T, so a pointer's bytes keep its address and lose its capability, as through memcpy (c_typesbit_castable/3).__builtin_assume_aligned(p, a[, o])is p as avoid *.- Constructors and destructors.
__attribute__((constructor))anddestructor, with an optional priority from 0 to 65535, were ignored, so the functions never ran (xz sets its CRC function pointers in one when its CLMUL CRC is built, which the check configures out; no xz object has one). Each is now data of linkageinit(ctor|dtor, Priority)holding the function (ir.pl), which filc_amd64 emits as clang's Fil-C does (abi-findings.md). The entry refers to the function, so a deferred static one is lowered; a constructor not defined in the translation unit is refused. - Weak thunks are per signature, not per call site (abi-findings.md).
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:
- Asm with a memory operand. gnulib's fpucw.h reads
and sets the x87 control word with
fnstcw %0/fldcw %0on an"m"operand, under vasnprintf's long double formats (seq, printf). clang's Fil-C compiles such an asm to a run-time safety error, which a program reaches only if it runs it; nanofil now does the same (a call ofzsafety_errorwith clang's message), instead of refusing the function at compile time (abi-findings.md). Any other asm is still refused. asm-memory.c. - Attributes on a label (gnulib's
_GL_UNUSED_LABEL,fail: __attribute__((__unused__));) are ignored, as GCC and clang do. userland-idioms.c. - No
__SIZEOF_INT128__. nanofil refuses__int128, so its preprocessor profile, and nanofil-cc's clang preprocessing, no longer define the macro clang advertises it with: factor.c'sumul_ppmmtakes its portable path. int128-portable.c.
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:
_Atomic(pycore_atomic.h'svoid *_Atomicandatomic_intmembers, through<stdatomic.h>)._Atomic(T)and the_Atomicqualifier apply to an integer, a pointer,floatordouble, whose atomic version has T's size and alignment on x86-64; a struct, union orlong doubleis refused (atomic_object_type/1). Reading an_Atomicobject is a seq_cst atomic load and simple assignment a seq_cst atomic store, mapped as the builtins below are: a pointer through Fil-C's atomic helpers, anything else a plain load, or a store and thenmfence. Initialization is not atomic (C11 7.17.2.1). A compound assignment or++/--of one is an atomic read-modify-write (see Atomic read-modify-write below).<stdatomic.h>'satomic_load_explicit,atomic_store_explicit,atomic_initand the fences are clang's__c11_atomic_*builtins: load and store map to the__atomicbuiltins, init is a plain store, and a thread fence ismfencewhen seq_cst and nothing otherwise (as on x86-64 clang);__atomic_thread_fenceand the signal fences too. An_Atomicbit-field is refused (clang refuses one too). An_Atomicparameter loses its qualifier and is assigned plainly, which only matters if its address reaches another thread. Before, the_Atomicqualifier was silently ignored: astruct obj *_Atomicslot stored by one thread and read by another tore and trapped. c11-atomics.c.- Alignment of objects.
_Alignasand GNUaligned(N)on an object declaration (socketmodule'schar buf[16384] Py_ALIGNED(8)) align the object, local, VLA, static or global, up to 16 bytes, the most anallocor data item aligns to; the type, and sosizeofand_Alignofof it, is unchanged (object_alignment/5). A stricter request, or one on a function or typedef, is still refused. object-alignment.c. - Pointer-as-int laundering. Fil-C gives an
inttoptrthe capability of the oneptrtointits integer came from (gimso_semantics.md, Inttoptr); _elementtree tags pointers with(PyObject *)((uintptr_t)p & ~1), the string search aligns them with_Py_ALIGN_DOWN, and bind's qp_test keeps a key inuintptr_t pval = (uintptr_t)str. Since November 2026 one IR pass does it, launder.pl, on the frontend's output before any optimization: Fil-C's lattice (bot,def(P),top) evaluated on the SSA form promote.pl gives the function, so an integer keeps its source through promotable local variables, phis and loops at every level, and two loads of one address in a block are one source. A local integer that carries one gets a shadow pointer slot, which-Opromotes into Fil-C's capability phi. No source, or two different ones, gives no capability, as Fil-C's TOP does; so do loads from memory (a volatile local counts as memory), calls, comparisons and a conversion to_Bool. Before, c_lower followed sources syntactically, in the cast's own expression and in local variables defined once, and gave none where clang -O1 might make a select anllvm.umin; those rules are gone. Where clang does make one, filcc traps and nanofil keeps the capability of the value selected (launder-ssa.c). launder.c, launder-local.c. - Large tables. unicodedata's 26,000-entry tables and
deepfreeze.c (6.6 MB preprocessed, string literals of up to a few
hundred kilobytes) overflowed SWI's 1 GB stack. A choicepoint per
declaration (
reflect_type/4) and per static-initializer leaf kept every declaration's and leaf's terms alive; the lexer's narrow string literals recursed per character without last-call optimization; and the stacks grown by lexing and parsing stayed allocated into lowering (release_stacks/0now also runs after each). Both fit in about 600 MB and 800 MB, and the output is byte-identical to the old compiler's with a raised limit.
__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:
- Pointer initializers from integer-constant
addresses. A static pointer initialized with
&((T *)0)->m(pl-event.c's&((PL_local_data_t *)NULL)->event.hook.name) was refused; it is now an address with no capability, as clang gives it (address_constant/5throughconstant_address/3). null-offsetof.c.
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:
acquire_ldata(pl-inline.h) reads another thread'sinfo->thread_data, publishes it in its hazard pointeraccess.ldataand dereferences it without checking thatthread_datastill holds it, sofreePrologThreadcan clear it, scan the hazards and free it in between (filcc: "cannot read pointer to free object" atld->magic, fromcgc_thread_statsandfreePrologThread's read of its creator).alloc_threadpopsGD->thread.freewith a plain load of the head CAS'd by other threads ("cannot read pointer with ptr >= upper", in both builds).
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
tests/c/basic.c and tests/c/more.c: 52 runs, each run three ways (interpreter on nanofil's IR, nanofil's Fil-C code, and clang compiling the same C file), all linked with the same harness by tests/difftest.sh, which now accepts
.cfiles and takes// run: $fn argscomments as run lines. They cover conversions between all integer widths and signs, signed and unsigned division and shifts, enums andswitch, structs, unions, nested arrays with brace elision and designators, string initializers, pointer walking and comparison, mutual recursion through prototypes, short-circuit side effects and out-of-bounds traps. A planted bug (unsigned>>assar) shows up only as a mismatch against clang, because the interpreter and nanofil's code share the frontend. That is why the clang variant exists.tests/reject.sh: 53 programs in tests/c/reject that must be refused with a given message: C constraint violations (assignment,
_Generic,static_assert,switch, VLA scope and storage, thread-local storage), the address or size of a bit-field,va_startoutside a variadic function, inline asm,_Complex,_Atomic, jumps into statement expressions, K&R parameters, packed pointers and bit-fields, alignment attributes, undeclared identifiers, and case ranges.tests/progtest.sh: whole programs in tests/c/prog, built by nanofil (including
main) and byfilcc -O1, must agree on stdout, stderr and exit status, a safety panic counting astrap. They useprintf,snprintf,puts,fputs(stderr),malloc,strcpy/strcat/strlen/memcpy,qsortwith nanofil callbacks, function pointers in structs and static tables, and an overflow instrcpyinto a nanofilmallocthat must trap.gcglobals.ckeeps 20,000-node lists reachable only from globals and a static local across forced collections, and the check also runs it withFUGC_SCRIBBLE=1 FUGC_VERIFY=1. A getter that skips registration (it returned the object withoutfilc_global_initialization_start) loses the list in both modes. A thunk that drops pointer lowers in the generic path is caught byfuncs.c.tests/c/structs.c: 18 runs of struct values inside one module, three ways as above: every return form, odd sizes, copies,
?:, unions, members of rvalues, and a pointer returned in a struct that still traps past its object. A planted bug in reassembling short eightbytes failsodd.tests/c/prog/structs.c is built by nanofil and linked with lib/structs.c built by clang (a
// link:line in progtest). Struct arguments and every return form cross the boundary both ways, directly, through function pointers, through pointers of the wrong type (generic entries on both sides) and through mismatched prototypes (the thunks' generic path). Planted bugs in the generic entry's buffer copy, the thunk's register and buffer results, the indirect call's generic result and the generic entry's registers each fail it.tests/filc/tuples.ir: tuple returns in IR.
tests/filc/globals.ir: IR-level globals, read-only writes, pointer data with offsets, indirect calls with matching and mismatching classes, calls through non-functions and offset function pointers, and reads through a function pointer.
Known gaps
- Bounds of locals are rounded up to 16 bytes. M3's
allocisfilc_allocate, which rounds every size up toFILC_MINALIGN(16) and setsupperto match (prepare_allocate_objectinfilc_runtime.c). This is Fil-C's own heap granularity, not a hole. clang-builtmalloc(8)and escaping locals at-O0behave the same, and the overrun only reaches the object's own zeroed padding, never another object or a capability. clang at-O1traps exactly onoobp(2)because it keeps that non-escaping local on the stack and checks its exact size statically. nanofil gets that with M5's escape analysis, which keeps non-escaping locals on the stack. - Byval struct arguments always take the generic convention, as clang's do.
- A struct argument is still one IR argument per eightbyte, however
large (a 64 KB one is 8193 words, loaded and stored one at a time). tests/bigarg.py
65536 overflows the 1 GB stack at
-Oin amd64_regs, where filcc calls memmove. Fixing it needs a by-reference argument form in ir.pl, the interpreter and the emitter; it waits until a package needs it (no survey error has shown one).