Fil-C on Nix

Filnix wiki

mbrock/filnix/nanofil

main.pl

The nanofil command line: the impure driver that reads a C or IR file, runs it through the compiler's stages and prints the result.

Commands

CommandDoes
check FILEread and validate; prints ok
print FILEread, validate, print the IR (ir_text.pl)
compile FILEread, validate, print x86-64 assembly
cc FILEcompile C (.c, or preprocessed .i) to Fil-C assembly
cc-ir FILEcompile C, print the IR (unchecked; the Fil-C backend's input is pizlonated)
tokens FILEprint C token terms, without locations
run FILE FN ARG...interpret FN on integer arguments
difftest FILE OUTDIRwrite OUTDIR/expected.txt, OUTDIR/harness.c and OUTDIR/mode for the file's runFn, Args facts
outcomes FILEprint the checked module of a Fil-C IR file and the interpreter's outcome of each run line, as terms (rocq/)
asm-terms FILE TEXTas compile for a Fil-C module, but write the assembly text to the file TEXT and print each asm.pl line as a term (rocq/backend)
abi-terms FILEprint the checked module of a Fil-C file, then the reference ABI plan of each of its functions and its weak thunks' plans, as terms (rocq/backend)
encode FILE OUTread, validate and write the module as NFB, the bytecode of nfb.pl, to OUT (docs/nfb.md)
decode FILEread an NFB file, validate, print the IR
nfb-check FILEwrite the module, and a Fil-C module's checked module, as NFB and read each back: the same term with NAMES, the same words without it (tests/nfb.sh)

A FILE ending in .c or .i goes through the C frontend, one ending in .nfb is NFB, and any other FILE is IR text. A .c file is preprocessed with the predefined macros and include paths of $FILCC (default filcc).

→ → →
opt (-O)
→ →

cc runs the whole pipeline; c_pp lexes with c_lex. IR text enters at the middle end, and a module in plain mode goes to amd64.pl instead of pizlonate and filc_amd64.

-O

-O before the command runs the middle end (opt.pl) after loading. Without it the IR goes to the backend as loaded, which is the reference the optimized path is checked against. With -O, difftest also runs every run line on the unoptimized module and fails unless each optimized outcome refines the original one (translation validation), and prints how many allocs promote removed.

--registers=stack|temps|pointers independently selects reference emission or validated whole-function register homes for scalars with validated register operands and frame cells, on a frame with a home per scalar temporary (docs/perf.md items 3, 5a-loads and 3d), in the Fil-C backend. Pointers is temps with pointer address words as homes too, their lowers staying in the frame record (docs/perf.md 5b-2). The default is pointers with -O and stack without it; NANOFIL_REGISTERS supplies the same choice to build harnesses, -O --registers=temps keeps scalar homes only, and -O --registers=stack keeps the reference emission. The modes local, function, operands, cells and flow were retired in October 2026 (docs/perf.md, register modes) and are refused. The plain backend is unchanged. Options precede the command.

--operands=rewrite|regions selects, under temps or pointers, how each straight-line region's operands are chosen once the homes are in registers: rewrite, step 3b's forwarding of copies, or regions, which selects each region's registers again from the values the validated stream computes, operating on homes directly, with lea and three-address forms (docs/perf.md item 19). Both are checked by the same operand checker. The default is regions; NANOFIL_OPERANDS supplies the same choice.

--registers-limit=N (or NANOFIL_REGISTERS_LIMIT), opt-in, emits each function of more than N checked IR instructions by the reference (stack) emission when registers are temps or pointers, to save compile time on very large functions (docs/perf.md, compile time).

--branches=reference|selected independently selects the reference branch on a stored boolean, or a branch on the flags of a compare that only the branch reads, with jumps to the next block left to fall through (docs/perf.md). The default is selected with -O and reference without it; NANOFIL_BRANCHES supplies the same choice, and -O --branches=reference keeps the reference branches.

--memcpy=call|inline independently selects the reference call for every memcpy and memmove, or, for a constant 1 to 16 bytes to a function the module does not define, an inline copy in front of that call, taken only when the runtime's checks would pass and neither object has aux (docs/perf.md item 7a). The default is inline with -O and call without it; NANOFIL_MEMCPY supplies the same choice, and nanofil-cc passes call for -fno-builtin, -fno-builtin-memcpy, -fno-builtin-memmove and -ffreestanding.

--bounds=split|merged|sign|or selects the bounds check's shape: split, the default, tests ptr < lower and ptr > upper - Size with a branch each; merged tests both with one unsigned comparison of ptr - lower; sign and or test (ptr - lower) | (upper - Size - ptr) with one branch (docs/bounds.md). NANOFIL_BOUNDS supplies the same choice.

--share=none|block|null|upper shares parts of heap checks of one object under split bounds and local selection: block leaves out the null test of a check whose lower an earlier check in its block tested, null also when that check is in a dominating block, and upper also reuses the upper a check of the same object just loaded (src/check_share.pl, docs/perf.md item 17). The default is upper with -O, except with --checks=reference, and none otherwise; NANOFIL_SHARE supplies the same choice.

--lowers=own|shared selects the frame's GC roots: own, the default, gives every pointer temporary its own lowers[] entry; shared lets a flight use its lower root's entry, kept while the flight is live, and validates every frame (src/lower_roots.pl, src/lowers_check.pl, docs/lowers.md). NANOFIL_LOWERS supplies the same choice.

--inline=none|small|wide independently selects no inlining, or, with -O, inlining of small static functions before promotion (src/inline.pl, docs/perf.md item 4), or that and more: always_inline, single-call-site and non-leaf callees, callees first (item 20). The default is wide with -O; without -O nothing inlines. NANOFIL_INLINE supplies the same choice, and nanofil-cc passes none for -fno-inline and -O0. A trap inside an inlined function reports its caller's name, as filcc -O2 does without -g.

--simplify=none|full independently selects, with -O, the cheap middle end after copy propagation (src/simplify.pl, docs/perf.md item 8). The default is full; NANOFIL_SIMPLIFY supplies the same choice.

--checks=reference|reuse|renew|guard|hoist|schedule|half|forward|combined|hull|getter|extended independently selects the reference checked IR, a check before every access, check reuse after pizlonation, reuse followed by renewal of dominated checks, both followed by range guards for checks in loops, both followed by hoisted bounds, a limit taken before a loop for its full checks of a root defined before it, or both followed by scheduled checks, which check early the bytes every path ahead needs and renew what they cover; a scheduled check may trap before stores that precede the failing access, or both followed by half checks, which test only the bound an earlier check of the object does not already establish, or reuse that also serves pointer loads and covers a check by the hull of its object's live facts, then renewal, which moves no trap, or that followed by half checks (combined), combined with renewal by the hull of two checks (hull), with facts kept across repeated getters of local globals (getter), or both (extended) (src/check_reuse.pl, src/schedule.pl, src/half_check.pl, docs/m7.md, docs/bounds.md, docs/schedule.md, docs/half.md, docs/forward.md). The default is combined with -O and reference without; NANOFIL_CHECKS supplies the same choice. With any but reference, cc, compile and difftest's pizlonation comparison use that module.

GNU vector types and the SSE through AVX2 intrinsics (docs/vector-access.md) are on; --no-vectors (or --vectors=off) turns them off, refusing every vector type, and --vectors (or --vectors=on) turns them back on; the last one given wins. NANOFIL_VECTORS=0 or 1 supplies the same choice.

difftest

expected.txt holds the interpreter's result for every run line whose outcome a native run can reproduce: a known return value, or for a Fil-C module a trap, written just trap. harness.c prints the same lines from native code; for a Fil-C module it makes one call per process, chosen by argv[1], since a trap ends the process. mode holds plain or filc. Other outcomes are reported on stdout and left out of the comparison.

For a Fil-C module, difftest also runs every run line on the pizlonated module and fails unless each outcome is the same, traps and their reasons included.

usesir
usesnfb

Compile-only commands do not need the interpreter or its CLP(Z) solver.

180:- ifcurrent_prolog_flagdialect, swi
181:- autoloadinterp, run/5, module_prepared/2, run_prepared/5, outcome_text/2
182:- else
usesinterp
184:- endif
usesamd64
usesasm
usesc_lex
usesc_pp
usesopt
useslists
usesformat
usespio
usesdcgs
usespairs
204:- initializationmain
maindet

The command line entry point (bin/nanofil): reads the options from argv and the NANOFIL_* environment variables (options/8 and the *_default predicates), runs command/4, and halts with 0; or prints the error, or "failed" when the command fails, and halts with 1.

212main ←
Trealla gives argv as character lists, SWI as atoms.
current_prolog_flagargv, RawArgv
maplistarg_atom, RawArgv, Argv0
halt/1 outside catch/3: SWI implements halt as an exception.
catch
vectors_optionsArgv0, ArgvV
bounds_optionsArgvV, ArgvB
share_optionsArgvB, ArgvS
lowers_optionsArgvS, ArgvL
operands_optionsArgvL, Argv00
inline_optionsArgv00, Inline0, Inline, Argv1
simplify_optionsArgv1, Simplify0, Simplify, Argv2
limit_optionsArgv2, Limit0, Limit, Argv3
optionsArgv3, o0, Default, Checks0, Level, Registers0, Checks, Argv
limited_registersLimit, Registers0, Registers
opt_selectionLevel, Inline, Simplify, Opt
commandOpt, Registers, Checks, Argv
→
Status = 0
else
formatuser_error, "nanofil: failed~n", []
Status = 1
,
Error,
reportError
Status = 1
haltStatus

The emission choices: emit(Registers, Branches, Copies).

245register_defaultemitRegisters, Branches, Copies ←
Unbound until options/8: the defaults depend on -O.
getenv'NANOFIL_REGISTERS', R
→
Registers = R
else
true
getenv'NANOFIL_BRANCHES', B
→
Branches = B
else
true
getenv'NANOFIL_MEMCPY', M
→
Copies = M
else
true
250register_modeR ←
memberchkR, stack, temps, pointers
→
true
memberchkR, local, function, operands, cells, flow
→
formatatomMsg, 'registers=~w was retired (docs/perf.md); registers must be stack, temps or pointers', R
thrownanofil_errorMsg
else
thrownanofil_error'registers must be stack, temps or pointers'
256branches_modeB ←
memberchkB, reference, selected
→
true
else
thrownanofil_error'branches must be reference or selected'
259memcpy_modeM ←
memberchkM, call, inline
→
true
else
thrownanofil_error'memcpy must be call or inline'
checks_default?Checks

NANOFIL_CHECKS, or unbound until -O decides.

263checks_defaultChecks ←
getenv'NANOFIL_CHECKS', C
→
Checks = C
else
true
265checks_modeC ←
memberchkC, reference, reuse, renew, guard, hoist, schedule, half, forward, combined, hull, getter, extended
→
true
else
thrownanofil_error'checks must be reference, reuse, renew, guard, hoist, schedule, half, forward, combined, hull, getter or extended'
vectors_options+Argv0, -Argv

Argv is Argv0 less its vector options, the last of which sets NANOFIL_VECTORS (c_types vector_type/4), for drivers that pass only options: --vectors and --vectors=on to 1, --no-vectors and --vectors=off to 0.

273vectors_optionsArgv0, Argv ←
excludevectors_option, Argv0, Argv
includevectors_option, Argv0, Opts
lastOpts, Opt
→
setenv'NANOFIL_VECTORS', V
else
true
280vectors_optionOpt ←
vectors_option
281'--vectors''1'
282'--vectors=on''1'
283'--no-vectors''0'
284'--vectors=off''0'
bounds_options+Argv0, -Argv

Argv is Argv0 less its --bounds= options, the last of which sets NANOFIL_BOUNDS, the bounds check's shape (filc_amd64:bounds_check//4): split, the default, merged, sign or or.

289bounds_optionsArgv0, Argv ←
excludebounds_option, Argv0, Argv
includebounds_option, Argv0, Opts
lastOpts, Opt
→
atom_concat'--bounds=', B, Opt
memberchkB, split, merged, sign, or
→
setenv'NANOFIL_BOUNDS', B
else
thrownanofil_error'bounds must be split, merged, sign or or'
else
true
298bounds_optionA ←
atomA
atom_concat'--bounds=', _, A
share_options+Argv0, -Argv

Argv is Argv0 less its --share= options, the last of which sets NANOFIL_SHARE (check_share:share_mode/1).

301share_optionsArgv0, Argv ←
excludeshare_option, Argv0, Argv
includeshare_option, Argv0, Opts
lastOpts, Opt
→
atom_concat'--share=', S, Opt
memberchkS, none, block, null, upper
→
setenv'NANOFIL_SHARE', S
else
thrownanofil_error'share must be none, block, null or upper'
else
true
310share_optionA ←
atomA
atom_concat'--share=', _, A
lowers_options+Argv0, -Argv

Argv is Argv0 less its --lowers= options, the last of which sets NANOFIL_LOWERS (lower_roots:lowers_mode/1).

313lowers_optionsArgv0, Argv ←
excludelowers_option, Argv0, Argv
includelowers_option, Argv0, Opts
lastOpts, Opt
→
atom_concat'--lowers=', S, Opt
memberchkS, own, shared
→
setenv'NANOFIL_LOWERS', S
else
thrownanofil_error'lowers must be own or shared'
else
true
322lowers_optionA ←
atomA
atom_concat'--lowers=', _, A
operands_options+Argv0, -Argv

Argv is Argv0 less its --operands= options, the last of which sets NANOFIL_OPERANDS (amd64_regs:operand_selection/1).

326operands_optionsArgv0, Argv ←
excludeoperands_option, Argv0, Argv
includeoperands_option, Argv0, Opts
lastOpts, Opt
→
atom_concat'--operands=', S, Opt
memberchkS, rewrite, regions
→
setenv'NANOFIL_OPERANDS', S
else
thrownanofil_error'operands must be rewrite or regions'
else
true
335operands_optionA ←
atomA
atom_concat'--operands=', _, A
inline_options+Argv0, +Inline0, -Inline, -Argv

Argv is Argv0 less its --inline= options, the last of which gives Inline.

338inline_options[], I, I, []
339inline_optionsA|As, I0, I, Args ←
atomA
atom_concat'--inline=', I1, A
→
inline_optionsAs, I1, I, Args
else
Args = A|Args1
inline_optionsAs, I0, I, Args1
inline_default?Inline

NANOFIL_INLINE, or unbound until -O decides.

345inline_defaultInline ←
getenv'NANOFIL_INLINE', I
→
Inline = I
else
true
347inline_modeI ←
memberchkI, none, small, wide
→
true
else
thrownanofil_error'inline must be none, small or wide'
limit_options+Argv0, +Limit0, -Limit, -Argv

as inline_options/4, for --registers-limit=N; Limit is none or a nonnegative integer.

352limit_options[], L, L, []
353limit_optionsA|As, L0, L, Args ←
atomA
atom_concat'--registers-limit=', N, A
→
limit_optionsAs, L1, L, Args
else
Args = A|Args1
limit_optionsAs, L0, L, Args1
358limit_defaultL ←
getenv'NANOFIL_REGISTERS_LIMIT', N
→
else
L = none
360limit_modeA, N ←
catchatom_numberA, N, _, fail
integerN
N ≥ 0
→
true
else
thrownanofil_error'registers-limit must be a nonnegative integer'
limited_registers+Limit, +Emission0, -Emission

limited(R, N) for temps or pointers registers under a limit (filc_amd64.pl's item_placed_lines/6).

367limited_registersnone, E, E ←
!
368limited_registersN, emitR, B, M, emitlimitedR, N, B, M ←
memberchkR, temps, pointers
!
369limited_registers_, E, E
simplify_options+Argv0, +Simplify0, -Simplify, -Argv

as inline_options/4, for --simplify=.

372simplify_options[], S, S, []
373simplify_optionsA|As, S0, S, Args ←
atomA
atom_concat'--simplify=', S1, A
→
else
Args = A|Args1
simplify_optionsAs, S0, S, Args1
simplify_default?Simplify

NANOFIL_SIMPLIFY, or unbound until -O decides.

379simplify_defaultSimplify ←
getenv'NANOFIL_SIMPLIFY', S
→
Simplify = S
else
true
381simplify_modeS ←
memberchkS, none, full
→
true
else
thrownanofil_error'simplify must be none or full'
opt_selection+Level, ?Inline, ?Simplify, -Opt

the middle end's selection (opt.pl). Inlining and simplification need -O, where inlining is wide and simplification full unless chosen otherwise.

387opt_selectiono0, _, _, o0
388opt_selectiono1, Inline, Simplify, Opt ←
varInline
→
Inline = wide
else
true
varSimplify
→
Simplify = full
else
true
middle_selectionSimplify, Inline, Opt
middle_selection
392nonenoneo1
393nonesmallo1small
394nonewideo1wide, none
395fullInlineo1Inline, full
opt_level+Opt, -Level

o0 or o1, for difftest's validation.

opt_level
397o0o0
398o1o1
399o1_o1
400o1_, _o1
402options'-O'|As, _, R, C, O, Rs, Cs, Args ←
!
optionsAs, o1, R, C, O, Rs, Cs, Args
403optionsA|As, O, emit_, B, M, C, Opt, Rs, Cs, Args ←
atom_concat'--registers=', R, A
!
optionsAs, O, emitR, B, M, C, Opt, Rs, Cs, Args
406optionsA|As, O, emitR, _, M, C, Opt, Rs, Cs, Args ←
atom_concat'--branches=', B, A
!
optionsAs, O, emitR, B, M, C, Opt, Rs, Cs, Args
409optionsA|As, O, emitR, B, _, C, Opt, Rs, Cs, Args ←
atom_concat'--memcpy=', M, A
!
optionsAs, O, emitR, B, M, C, Opt, Rs, Cs, Args
412optionsA|As, O, R, _, Opt, Rs, Cs, Args ←
atom_concat'--checks=', C, A
!
optionsAs, O, R, C, Opt, Rs, Cs, Args
415optionsArgs, O, emitR, B, M, C, O, emitR, B, M, C, Args ←
registers_default+Opt, ?Registersdet

Registers is the register mode for optimization level Opt (o0 or o1) when no --registers option or NANOFIL_REGISTERS chose one: stack, the reference emission, without -O, and pointers, a home per scalar temporary and per pointer address word (amd64_regs.pl), with it. branches_default/2 does the same for branch selection: reference without -O, selected with it.

427registers_default_, R ←
nonvarR
!
428registers_defaulto0, stack
429registers_defaulto1, pointers
430branches_default_, B ←
nonvarB
!
431branches_defaulto0, reference
432branches_defaulto1, selected
memcpy_default+Opt, ?Copies

-O inlines small constant copies.

434memcpy_default_, M ←
nonvarM
!
435memcpy_defaulto0, call
436memcpy_defaulto1, inline
checks_default+Opt, ?Checks

-O combines forward facts and half checks (docs/forward.md, docs/half.md); without it every access keeps its full check.

440checks_default_, C ←
nonvarC
!
441checks_defaulto0, reference
442checks_defaulto1, combined
share_default+Opt, +Checks

-O shares null tests and uppers (check_share.pl) unless --share= or NANOFIL_SHARE chose or the checks are the reference's, which stay whole; without -O nothing is shared.

446share_defaulto0, _
447share_defaulto1, C ←
getenv'NANOFIL_SHARE', _
or
C ≡ reference
→
true
else
setenv'NANOFIL_SHARE', upper
arg_atom+Arg, -Atom

Atom is the command-line argument Arg, which is an atom (SWI) or a character list (Trealla).

454arg_atomAtom, Atom ←
atomAtom
!
457arg_atomCs, Atom ←
atom_charsAtom, Cs
report+Error

print Error on stderr, a nanofil_error by its message and anything else as a quoted term. Error is any ball, so the last clause is a default and the cut stays.

463reportnanofil_errorMsg ←
!
formatuser_error, "nanofil: ~w~n", Msg
466reportError ←
formatuser_error, "nanofil: ~q~n", Error

Loading

file_module+File, -Module

Module is the valid IR module in File. A .c or .i file goes through the C frontend, a .nfb file is NFB, and anything else is IR text.

474file_moduleFile, Module ←
atom_concat_, '.c', File
or
atom_concat_, '.i', File
→
c_file_moduleFile, Module
atom_concat_, '.nfb', File
→
read_nfb_fileFile, Words
module_nfbModule, Words
→
true
else
formatatomMsg, '~w: not the NFB of a module', File
thrownanofil_errorMsg
else
read_ir_fileFile, Module, _
c_file_module+File, -Module

Module is the valid IR module of the C file. c_file_module/4 also gives the assembly lines of its top-level asm (c_lower c_toplevel_asm/2), which cc prints after the module, and its functions' inlining attributes (c_lower c_module/3).

494c_file_moduleFile, Module ←
c_file_moduleFile, Module, _, _
495c_file_moduleFile, Module, AsmLines, Attributes ←
c_file_tokensFile, Tokens
c_parseTokens, Items
→
true
else
thrownanofil_error'C parse failed'
Deterministic stages: a leftover choicepoint would keep the tokens
and items of a large translation unit (SQLite) alive to the end.
oncec_moduleItems, Module, Attributes
oncec_toplevel_asmItems, AsmLines
The frontend's garbage is gone once its items are: SQLite with
FTS3, R-Tree and sessions (nixpkgs' flags) has 220 MB live here,
yet without this the validator ran out of the 1 GB stack.
oncevalid_moduleModule
c_file_tokens+File, -Tokens

Tokens are the C tokens of File. A .i file is already preprocessed; anything else goes through c_pp with the predefined macros and include paths of $FILCC (default filcc).

515c_file_tokensFile, Tokens ←
atom_concat_, '.i', File
→
oncei_file_tokensFile, Tokens0
else
getenv'FILCC', Clang
→
true
else
Clang = filcc
pp_profileClang, Profile
pp_fileFile, Profile, Tokens0
getenv'NANOFIL_STD', Std
Std ≢ c17
Std ≢ gnu89
→
Std ≡ c23
→
c23_tokensTokens0, Tokens
else
thrownanofil_error'NANOFIL_STD must be c17, c23 or gnu89'
else
Tokens = Tokens0

Commands

release_stacks: collect garbage and return unused stack memory to the system between the stages of a large compile. SWI grows its stacks between collections and keeps them grown, and its stack limit counts what is allocated, not what is live: gnulib's test-intprops.c, whose main has 87,000 blocks, never has more than about 210 MB live, yet the frontend leaves the trail at 250 MB and the middle end the global stack at 1 GB, and the next collection, which must shift them first, exceeds the default 1 GB limit. A host without these predicates skips them.

540release_stacks ←
catchgarbage_collect, errorexistence_error_, _, _, true
catchtrim_stacks, errorexistence_error_, _, _, true
command+Opt, +Registers, +Checks, +Argsdet

Runs the command Args, [Command|Operands], such as [cc, File]; the header comment's table lists the commands. Opt is the middle end's selection: o0 without -O, else o1, o1(Inline) or o1(Inline, full) (opt.pl). Registers is the emission choice emit(Registers, Branches, Copies), and Checks the check passes (checks_module/3). Only native emission needs Registers and only the Fil-C checked stage needs Checks (difftest runs it too). Throws nanofil_error(Msg) for a refused program.

554commandOpt, Registers, Checks, compile, File ←
!
file_moduleFile, Module0
Promote can remove every p from a module; the ABI is the original's.
module_modeModule0, Mode
optimizeOpt, Module0, Module
mode_print_asmMode, Registers, Checks, Module
560commandOpt0, Registers, Checks, cc, File ←
!
c_file_moduleFile, Module0, AsmLines, Attributes
inline_attributesOpt0, Attributes, Opt
onceoptimizeOpt, Module0, Module
mode_print_asmfilc, Registers, Checks, Module
forallmemberLine, AsmLines, format"~w~n", Line
568commandOpt, Registers, Checks, 'asm-terms', File, Text ←
!
file_moduleFile, Module0
module_modeModule0, filc
→
true
else
thrownanofil_error'asm-terms needs a Fil-C module'
optimizeOpt, Module0, Module
checked_moduleChecks, Module, Checked
openText, write, Stream
filc_module_emitChecked, Registers, print_asm_termsStream
closeStream
578commandOpt, _, Checks, 'abi-terms', File ←
!
file_moduleFile, Module0
module_modeModule0, filc
→
true
else
thrownanofil_error'abi-terms needs a Fil-C module'
optimizeOpt, Module0, Module
checked_moduleChecks, Module, Checked
format"~k.~n", Checked
filc_module_plansChecked, print_term_line
587commandOpt, _, Checks, 'nfb-check', File ←
!
file_moduleFile, Module0
optimizeOpt, Module0, Module
nfb_round_tripFile, unchecked, Module
module_modeModule, filc
→
checked_moduleChecks, Module, Checked
nfb_round_tripFile, checked, Checked
else
true
596commandOpt, _, Checks, difftest, File, Dir ←
!
difftestOpt, Checks, File, Dir
598commandOpt, _, Checks, outcomes, File ←
!
outcomesOpt, Checks, File
600commandOpt, _, _, Args ←
commandOpt, Args
inline_attributes+Opt0, +Attributes, -Opt

wide inlining takes the C file's always_inline and noinline functions (c_module/3), which the IR does not carry, in its selection. Other selections are unchanged.

605inline_attributeso1wide, S, Attributes, o1wideAttributes, S ←
!
606inline_attributesOpt, _, Opt
command+Opt, +Args

carry out the command line Args (without options), running the middle end when Opt is o1 (-O given) and not when o0. Fails or throws when the command does.

611command_, tokens, File ←
c_file_tokensFile, Tokens
forallmembertokToken, _, Tokens,
write_canonicalToken
write'.'
nl
615command_, check, File ←
format"ok~n", []
618commandOpt, encode, File, Out ←
file_moduleFile, Module0
optimizeOpt, Module0, Module
module_nfbModule, Words
write_nfb_fileOut, Words
623command_, decode, File ←
atom_concat_, '.nfb', File
→
true
else
thrownanofil_error'decode needs a .nfb file'
file_moduleFile, Module
629commandOpt, print, File ←
file_moduleFile, Module0
optimizeOpt, Module0, Module
633commandOpt0, 'cc-ir', File ←
c_file_moduleFile, Module0, _, Attributes
inline_attributesOpt0, Attributes, Opt
optimizeOpt, Module0, Module
638commandOpt, run, File, Fn|ArgAtoms ←
file_moduleFile, Module0
optimizeOpt, Module0, Module
maplistatom_number, ArgAtoms, Args0
func_namedModule, Fn, func_, _, _, Params, _
maplistcanonical_arg, Params, Args0, Args
oncerunModule, Fn, Args, Fuel, Outcome
outcome_textOutcome, Text
format"~w~n", Text
648difftestSel, Checks, File, Dir ←
file_moduleFile, Module0
module_modeModule0, Mode
optimizeSel, Module0, Module
opt_levelSel, Opt
file_runsFile, Runs
opt_valid_moduleOpt, Module
module_preparedModule0, Prepared0
module_preparedModule, Prepared
One findall over all runs, so each run's heap is reclaimed on
backtracking before the next starts.
findallResult−Verdict,
memberRun, Runs
validated_runSel, Module0−Prepared0, Module−Prepared, Run, Result, Verdict
,
ResultVerdicts
pairs_keys_valuesResultVerdicts, Results, Verdicts
opt_report_validationOpt, File, Module0, Module, Verdicts
mode_pizlonation_agreesMode, Checks, File, Module−Prepared, Runs
includecomparableMode, Results, Compared
excludecomparableMode, Results, Skipped
atom_concatDir, '/expected.txt', ExpectedFile
atom_concatDir, '/harness.c', HarnessFile
atom_concatDir, '/mode', ModeFile
write_fileExpectedFile, expected_linesCompared
write_fileHarnessFile, harnessMode, Module, Compared
write_fileModeFile, fmt"~w~n", Mode
lengthCompared, NCompared
lengthSkipped, NSkipped
format"~w: ~w runs compared, ~w not comparable~n", File, NCompared, NSkipped
forallmemberrFn, Args, Outcome, Skipped,
outcome_textOutcome, Text
call_textFn, Args, CallText
format" ~w = ~w~n", CallText, Text
nfb_round_trip+File, +Stage, +Module

Module, written as NFB, reads back as itself, and its core sections alone read back as a module whose core encoding is those very words (nfb.pl). Prints the sizes; throws nanofil_error otherwise.

690nfb_round_tripFile, Stage, Module ←
module_nfbModule, Words
nfb_coreWords, Core
module_nfbModule1, Words
Module1 ≡ Module
module_nfbModule2, Core
module_nfbModule2, [], Core2
Core2 ≡ Core
→
lengthWords, N
lengthCore, NC
format"~w: ~w ~w words, ~w core~n", File, Stage, N, NC
else
formatatomMsg, '~w: the ~w module does not read back from NFB', File, Stage
thrownanofil_errorMsg
outcomes+Opt, +Checks, +Filedet

Prints, as Prolog terms, the checked module of the Fil-C IR File (after the middle end when Opt is o1, and the check passes Checks) and then, for each run line, outcome(Fn, Args, Fuel, Outcome), the interpreter's outcome on that module, or error(Fn, Args, Fuel, E) when the run throws E or finds no outcome. An independent model of the checked IR (rocq/, docs/formal.md) reads them.

709outcomesSel, Checks, File ←
file_moduleFile, Module0
module_modeModule0, Mode
Mode ≡ filc
→
true
else
thrownanofil_error'outcomes needs a Fil-C module'
optimizeSel, Module0, Module
checked_moduleChecks, Module, Checked
format"~k.~n", Checked
file_runsFile, Runs
module_preparedChecked, Prepared
forallmemberrunFn, Args0, Runs,
catch
module_run_resultChecked−Prepared, runFn, Args0, Fuel, r_, Args, Outcome
→
Line = outcomeFn, Args, Fuel, Outcome
else
Line = errorFn, Args0, Fuel, failed
,
E, Line = errorFn, Args0, Fuel, E
format"~k.~n", Line
mode_print_asm+Mode, +Registers, +Checks, +Moduledet

Prints the assembly of Module by the backend for Mode, filc or plain. Fil-C prints one item at a time (filc_module_emit/3), since a large module's lines (SQLite's) need not all be held at once.

735mode_print_asmplain, _, _, Module ←
module_asmModule, Asm
737mode_print_asmfilc, Registers, Checks, Module ←
checked_moduleChecks, Module, Checked
filc_module_emitChecked, Registers, print_asm
checked_module+Checks, +Module, -Checkeddet

Checked is Module pizlonated (pizlonate_module/2), validated as checked IR (valid_module/1), and then rewritten by the check passes Checks selects (checks_module/3). Throws if the validator refuses either.

762checked_moduleChecks, Module, Checked ←
oncepizlonate_moduleModule, Checked0
oncevalid_moduleChecked0
checks_moduleChecks, Checked0, Checked
checks_module+Checks, +Checked0, -Checkeddet

Checked is the checked module Checked0 after the check passes of Checks (--checks; docs/m7.md): reference keeps pizlonate's naive checks; reuse deletes a check an earlier live one covers; renew also replaces a dominated check by a test of only the free and read-only flags; guard also puts a span test before a loop and makes its checks renew_if; combined, the -O default, is forward facts followed by half checks; hull, getter and extended add renew_hull and facts across repeated getters (docs/forward.md). Each pass's result must pass its *_refines identity check against its input, and the validator (valid_module/1) then checks the result's evidence again on its own.

781checks_modulereference, Checked, Checked
782checks_modulereuse, Checked0, Checked ←
reused_moduleChecked0, Checked
oncevalid_moduleChecked

Renew (docs/m7.md, round 4) runs after reuse: what reuse keeps, a dominating check may renew. Only the result is validated: a renew keeps its check's proof and index, so reuse's output is valid if it is.

789checks_modulerenew, Checked0, Checked ←
renewed_moduleChecked0, Checked
oncevalid_moduleChecked

Guards (docs/m7.md, round 5) run after renewal: a check renewal left in a loop may become a guarded renewal. Only the result is validated.

795checks_moduleguard, Checked0, Checked ←
renewed_moduleChecked0, Checked1
onceguard_checksChecked1, Checked
guard_refinesChecked1, Checked
→
true
else
thrownanofil_error'range guards changed more than checks'
oncevalid_moduleChecked

Hoisted bounds (docs/bounds.md) run after renewal: a full check renewal left in a loop may test its bounds against a limit taken before the loop. Only the result is validated.

806checks_modulehoist, Checked0, Checked ←
renewed_moduleChecked0, Checked1
oncehoist_checksChecked1, Checked
hoist_refinesChecked1, Checked
→
true
else
thrownanofil_error'hoisted bounds changed more than checks'
oncevalid_moduleChecked

Scheduled checks (docs/schedule.md) run after renewal: a check widens over the bytes every path ahead needs from its base, and the checks it then covers are renewed at a distance. Only the result is validated.

817checks_moduleschedule, Checked0, Checked ←
renewed_moduleChecked0, Checked1
onceschedule_checksChecked1, Checked
schedule_refinesChecked1, Checked
→
true
else
thrownanofil_error'scheduled checks changed more than checks'
oncevalid_moduleChecked

Half checks (docs/half.md) run after renewal: a full check renewal left whose object an earlier check covers at one end tests only the other. Only the result is validated.

828checks_modulehalf, Checked0, Checked ←
renewed_moduleChecked0, Checked1
oncehalf_checksChecked1, Checked
half_refinesChecked1, Checked
→
true
else
thrownanofil_error'half checks changed more than checks'
oncevalid_moduleChecked

Forward facts (docs/forward.md): reuse that also serves heap pointer loads and covers a check by the hull of its object's live facts, then renewal at any distance (docs/schedule.md's renew from a related pointer's check). No trap moves. Only the result is validated.

840checks_moduleforward, Checked0, Checked ←
forwarded_moduleforward, any, Checked0, Checked
oncevalid_moduleChecked

Combined: forward facts, then half checks for what renewal left. Each stage keeps its own refines check. Only the result is validated.

846checks_modulecombined, Checked0, Checked ←
combined_moduleforward, any, Checked0, Checked

Combined with renewal by the hull of two checks (hull), with facts kept across repeated getter calls (getter), or both (extended), opt-in (docs/forward.md). Each stage keeps its own refines check.

851checks_modulehull, Checked0, Checked ←
combined_moduleforward, hull, Checked0, Checked
853checks_modulegetter, Checked0, Checked ←
combined_modulegetter, any, Checked0, Checked
855checks_moduleextended, Checked0, Checked ←
combined_modulegetter, hull, Checked0, Checked
858combined_moduleReuse, Renew, Checked0, Checked ←
forwarded_moduleReuse, Renew, Checked0, Checked1
oncehalf_checksChecked1, Checked
half_refinesChecked1, Checked
→
true
else
thrownanofil_error'half checks changed more than checks'
oncevalid_moduleChecked
866forwarded_moduleReuse, Renew, Checked0, Checked ←
oncereuse_checksReuse, Checked0, Checked1
reuse_refinesReuse, Checked0, Checked1
→
true
else
thrownanofil_error'forward reuse changed more than proofs'
oncerenew_checksRenew, Checked1, Checked
renew_refinesRenew, Checked1, Checked
→
true
else
thrownanofil_error'check renewal changed more than checks'
876renewed_moduleChecked0, Checked ←
reused_moduleChecked0, Checked1
oncerenew_checksChecked1, Checked
renew_refinesChecked1, Checked
→
true
else
thrownanofil_error'check renewal changed more than checks'
883reused_moduleChecked0, Checked ←
oncereuse_checksChecked0, Checked
reuse_refinesChecked0, Checked
→
true
else
thrownanofil_error'check reuse changed more than proofs'
mode_pizlonation_agrees+Mode, +Checks, +File, +Module−Prepared, +Runsdet

For a filc module, the checked Module (checked_module/3 with Checks) has the same outcome as Module on every run of Runs, traps and their reasons included, or the command throws: the explicit checks are exactly the implicit ones. A missing or too weak check shows as the outcome unchecked(Why). Both run with the same fuel, since pizlonate keeps every block. A plain module has nothing to compare.

897mode_pizlonation_agreesplain, _, _, _, _
898mode_pizlonation_agreesfilc, Checks, File, Module−Prepared, Runs ←
checked_moduleChecks, Module, Checked
module_preparedChecked, CheckedPrepared
findallBad,
memberRun, Runs
pizlonation_verdictModule−Prepared, Checked−CheckedPrepared, Run, Bad
Bad ≢ ok
,
Bads
forallmemberbadCallText, Text0, Text, Bads, format"PIZLONATION FAILED ~w ~w: ~w unchecked, ~w checked~n", File, CallText, Text0, Text
Bads ≡ []
→
true
else
thrownanofil_error'pizlonation changed an outcome'
915pizlonation_verdictOriginal, Checked, Run, Verdict ←
module_run_resultOriginal, Run, Fuel, rFn, Args, Outcome0
max_fuelMaxFuel
Outcome0 ≢ out_of_fuel
or
Fuel ≡ MaxFuel
!
module_run_resultChecked, Run, Fuel, r_, _, Outcome
groundOutcome0
→
Outcome0 ≡ Outcome
else
¬¬Outcome0 = Outcome
→
Verdict = ok
A scheduled check's earlier trap (docs/schedule.md) is accepted
exactly when the reference run traps too, whatever its reason.
Outcome = trapearly_
nonvarOutcome0
Outcome0 = trap_
→
Verdict = ok
else
call_textFn, Args, CallText
outcome_textOutcome0, Text0
outcome_textOutcome, Text
Verdict = badCallText, Text0, Text
opt_valid_module+Opt, +Module

with -O (o1), the optimized Module is valid. valid_module's clauses leave choice points that throw on retry.

940opt_valid_moduleo0, _
941opt_valid_moduleo1, Module ←
oncevalid_moduleModule
opt_report_validation+Opt, +File, +Module0, +Module, +Verdicts

with -O (o1), report_validation/4.

945opt_report_validationo0, _, _, _, _
946opt_report_validationo1, File, Module0, Module, Verdicts ←
report_validationFile, Module0, Module, Verdicts
report_validation+File, +Module0, +Module, +Verdictsdet

Prints the -O difftest report: promote's effect, then every validation failure among Verdicts, which together make the command throw.

954report_validationFile, Module0, Module, Verdicts ←
alloc_countModule0, Allocs0
alloc_countModule, Allocs
format"~w: promote: ~w allocs -> ~w~n", File, Allocs0, Allocs
forallmemberbadCallText, Text0, Text, Verdicts, format"VALIDATION FAILED ~w ~w: ~w before the middle end, ~w after~n", File, CallText, Text0, Text
memberchkbad_, _, _, Verdicts
→
thrownanofil_error'translation validation failed'
else
true

Running and validating

Fuel (blocks run) for a command-line run and the last validation step.

969max_fuel1000000

validated_run(+Opt, +Original, +Optimized, +Run, -Result, -Verdict): Result is Run's r/3 on the module that difftest compares, and Verdict ok or bad(CallText, OriginalText, OptimizedText).

Translation validation: the unoptimized module's outcome on the same run must allow the optimized one's. Both runs happen in the same findall step, so an unknown result keeps its constraints. Promote keeps every block and branch, so a correct result needs exactly the blocks the original needs: both run with the same fuel, starting small and growing until the original finishes. A broken pass that loops then ends as out_of_fuel, cheaply (the interpreter's memory grows quickly with the number of blocks run, so a looping run at full fuel would exhaust it).

984validated_runo0, _, Optimized, Run, Result, ok ←
module_run_resultOptimized, Run, Fuel, Result
987validated_runSel, Original, Optimized, Run, Result, Verdict ←
opt_levelSel, o1
module_run_resultOriginal, Run, Fuel, rFn, Args, Outcome0
max_fuelMaxFuel
Outcome0 ≢ out_of_fuel
or
Fuel ≡ MaxFuel
Commit to the first fuel step at which the original finishes, or
to the last one.
!
optimized_fuelSel, Fuel, Fuel1
module_run_resultOptimized, Run, Fuel1, Result
Result = r_, _, Outcome
refinesOutcome0, Outcome
→
Verdict = ok
else
call_textFn, Args, CallText
outcome_textOutcome0, Text0
outcome_textOutcome, Text
Verdict = badCallText, Text0, Text
optimized_fuel+Sel, +Fuel, -Fuel1

the optimized module's fuel. Inlining adds a join block per inlined call, at most one block run for each block the original runs.

1011optimized_fuelo1, Fuel, Fuel
1012optimized_fuelo1_, Fuel, Fuel1 ←
Fuel1 is 2 × Fuel
1013optimized_fuelo1_, _, Fuel, Fuel1 ←
Fuel1 is 2 × Fuel
fuel_step-Fuelmulti

Fuel is one of the growing fuel limits, smallest first, ending at max_fuel.

1017fuel_stepFuel ←
memberFuel, 1000, 8000, 64000, 512000, 1000000
canonical_arg+Param, +Arg0, -Arg

Arg is Arg0 reduced modulo the class of Param.

1022canonical_argparamClass, _, Arg0, Arg ←
modulusClass, Modulus
Arg is Arg0 mod Modulus
module_run_result+Module−Prepared, +Run, +Fuel, -Result

Result is the r(Fn, Args, Outcome) of the "# run:" line Run on a module and the interpreter's preparation of it, with Fuel. Throws if the line does not fit the module or the interpreter finds no outcome.

1030module_run_resultModule−Prepared, runFn, Args0, Fuel, rFn, Args, Outcome ←
func_namedModule, Fn, func_, export, _, Params, _
→
true
else
thrownanofil_errorFn−'run line names a function that is not exported'
lengthParams, N
lengthArgs0, N
→
true
else
thrownanofil_errorFn−'run line has the wrong number of arguments'
maplistcanonical_arg, Params, Args0, Args
oncerun_preparedPrepared, Fn, Args, Fuel, Outcome
→
true
else
call_textFn, Args, CallText
thrownanofil_errorCallText−'interpreter found no outcome'
comparable+Mode, +Resultsemidet

a native run in Mode (plain or filc) reproduces Result's outcome, a known return value or, for filc, a trap or a reached unreachable, both a Fil-C safety panic natively. Nothing else has a native counterpart.

1050comparable_, r_, _, retV ←
integerV
1052comparable_, r_, _, ret
1053comparablefilc, r_, _, trap_
1054comparablefilc, r_, _, unreachable
call_text+Fn, +Args, -Text

Text is the atom 'Fn(A1, A2, ...)'.

1057call_textFn, Args, Text ←
atomic_list_concatArgs, ', ', ArgText
formatatomText, '~w(~w)', Fn, ArgText

difftest output files

write_file+File, :Body2

write the characters Body2 (a DCG body) describes to File.

1065write_fileFile, Body2 ←
phraseBody2, Cs
openFile, write, Stream
formatStream, "~s", Cs
closeStream
expected_lines+Results//

one "CALL = OUTCOME" line per result, a trap's outcome shown as just "trap" (the native run cannot tell why). Only Fil-C mode compares unreachable, which panics there.

1074expected_lines[] ⟶
[]
1075expected_linesrFn, Args, Outcome|Results ⟶
call_textFn, Args, CallText
Outcome = trap_
→
Text = trap
Outcome ≡ unreachable
→
Text = trap
else
outcome_textOutcome, Text
fmt"~w = ~w~n", CallText, Text
harness+Mode, +Module, +Results//

a C program that calls Module's functions as Results do and prints what expected_lines//1 prints.

1086harnessplain, Module, Results ⟶
"#include <stdio.h>\n#include <stdint.h>\n\n"
results_fnsResults, Fns
prototypesFns, Module
"\nint main(void) {\n"
harness_callsResults, Module, labelled
" return 0;\n}\n"

Fil-C: one run per process, selected by argv[1], since a trap ends the process. The call text is printed first, so a trap leaves it unfinished.

1095harnessfilc, Module, Results ⟶
"#include <stdio.h>\n#include <stdint.h>\n#include <stdlib.h>\n\n"
results_fnsResults, Fns
prototypesFns, Module
"\nint main(int argc, char **argv) {\n switch (atoi(argv[1])) {\n"
filc_casesResults, 0, Module
" }\n return 0;\n}\n"
results_fns+Results, -Fns

Fns is the ordset of the functions Results call.

1105results_fnsResults, Fns ←
findallFn, memberrFn, _, _, Results, Fns0
sortFns0, Fns
filc_cases+Results, +I, +Module//

the switch cases I, I+1, ... of the Fil-C harness, one per result.

1111filc_cases[], _, _ ⟶
[]
1112filc_casesResult|Results, I, Module ⟶
Result = rFn, Args, _
call_textFn, Args, CallText
fmt" case ~w: fputs(\"~w = \", stdout); fflush(stdout);~n", I, CallText
harness_callsResult, Module, bare
" break;\n"
I1 is I + 1
filc_casesResults, I1, Module
prototypes+Fns, +Module//

an extern C declaration of each of Fns.

1123prototypes[], _ ⟶
[]
1124prototypesFn|Fns, Module ⟶
func_namedModule, Fn, func_, _, Ret, Params, _
c_typeRet, RetType
findallType,
memberparamClass, _, Params
c_typeClass, Type
,
ParamTypes
types_textParamTypes, ParamText
fmt"extern ~w ~w(~w);~n", RetType, Fn, ParamText
prototypesFns, Module
types_text+Types, -Text

Text is the C parameter list of Types.

1134types_text[], void
1135types_textType|Types, Text ←
atomic_list_concatType|Types, ', ', Text
c_type?Class, ?Type

Type is the C type of the IR class.

c_type
1139wuint32_t
1140luint64_t
1141voidvoid
harness_calls+Results, +Module, +Style//

Style labelled prints "CALL = ret N" lines from one main; bare prints "ret N" inside a case.

1145harness_calls[], _, _ ⟶
[]
1146harness_callsrFn, Args, _|Results, Module, Style ⟶
func_namedModule, Fn, func_, _, Ret, _, _
call_textFn, Args, CallText
findallCArg,
memberX, Args
formatatomCArg, '~wull', X
,
CArgs
atomic_list_concatCArgs, ', ', CArgText
harness_callStyle, Ret, Fn, CArgText, CallText
harness_callsResults, Module, Style
harness_call+Style, +Ret, +Fn, +CArgText, +CallText//

the line of harness_calls//3 for one call of Fn, which returns Ret.

1157harness_callStyle, Ret, Fn, CArgText, CallText ⟶
ret_resultRet, Result
style_callStyle, Result, Fn, CArgText, CallText
ret_result?Ret, ?Result

a function that returns Ret has a Result of none or value.

ret_result
1163voidnone
1164wvalue
1165lvalue
1166pvalue
1167tuple_value
style_call+Style, +Result, +Fn, +CArgText, +CallText//

as harness_call//5, for a call with Result.

1171style_callbare, Result, Fn, CArgText, _ ⟶
bare_callResult, Fn, CArgText
1173style_calllabelled, Result, Fn, CArgText, CallText ⟶
labelled_callResult, Fn, CArgText, CallText
bare_call+Result, +Fn, +CArgText//

a case's line for a call of Fn.

1177bare_callnone, Fn, CArgText ⟶
fmt" ~w(~w); puts(\"ret\");~n", Fn, CArgText
1179bare_callvalue, Fn, CArgText ⟶
fmt" printf(\"ret %llu\\n\", (unsigned long long)~w(~w));~n", Fn, CArgText
labelled_call+Result, +Fn, +CArgText, +CallText//

main's line for a call of Fn.

1185labelled_callnone, Fn, CArgText, CallText ⟶
fmt" ~w(~w); printf(\"%s = ret\\n\", \"~w\");~n", Fn, CArgText, CallText
1188labelled_callvalue, Fn, CArgText, CallText ⟶
fmt" printf(\"%s = ret %llu\\n\", \"~w\", (unsigned long long)~w(~w));~n", CallText, Fn, CArgText

Runs

file_runs+File, -Runs

Runs are the run(Fn, Args) facts of IR File, in order.

1196file_runsFile, Runs ←
read_ir_fileFile, _, Runs

library(format)'s format_//2 raises type_error(list, '') for an argument of '' (Trealla 12f4cbd), which a call with no arguments produces. fmt(+Format, +Args)//: the characters format/3 makes of Format and Args.

1201fmtFormat, Args ⟶
formatcharsCs, Format, Args
seqCs