Fil-C on Nix

Filnix wiki

nanofil/docs/archive/hosts.md

Prolog hosts

Archived: a record of finished work, kept as written. nanofil's archive lists the others.

nanofil runs on SWI-Prolog (the default, through src/swi/) and supports Trealla for the IR backend. Full Trealla frontend/interpreter testing remains paused. This file records the compatibility work and measurements.

Trealla backend restored (November 2026)

The pinned native Trealla (nix/trealla.nix) runs optimize/3, pizlonate_module/2 and filc_module_emit/3. This section was written and measured on revision 12f4cbd7; the pin has since moved, see Trealla 98a821db. Run the CLI from nanofil/src, with stdin closed:

NANOFIL_HOST=trealla TPL=/path/to/tpl ../bin/nanofil -O compile input.ir </dev/null

tests/backend-hosts.sh compares optimized IR, checked IR and assembly in stack, temps and pointers modes on both hosts, plus plain and -O CLI compilation. Its eight ABI fixtures cover floats, phi shadows, local storage, variadics, setjmp, globals and copies. A forty-parameter function exercises liveness masks beyond 64 bits: reverting the bitset fix makes the comparison fail even when compilation succeeds. The small parity step runs in the default Nix check; the full Trealla suite is still opt-in and currently stops parsing a multiline test literal in tests/unit.pl ("unterminated quoted atom").

What worked, and what broke

The parent of the October 24 pause, 8a5f5fa8, still passes all 57 of its unit tests on the pinned Trealla when rerun. The pause, d31875b3, was a decision to stop spending time on Trealla's memory and runtime cost, not a known backend correctness failure. Later SWI-only development introduced several independent incompatibilities:

Remaining cost

On the nine patterns (tag, email, word, alt, entity, shortcode, key, date, lit) generated by experiments/regex-nanofil/rx.pl, CLI -O compile produced byte-identical 8,233-line assembly on SWI and Trealla. The frontend-free parity driver also matched optimized IR, checked IR and all three emission modes (36,882 lines including the wide-mask fixture). A separate driver isolates the stages below. These are CPU seconds on an orb, SWI 9.2.9 versus pinned native Trealla, with optimize(o1(wide,full), ...), validation and filc_module_emit(..., emit(pointers,selected,inline), print_asm):

Stage SWI Trealla
read IR 0.004 0.045
validate input 0.037 0.310
optimize 0.195 1.692
pizlonate 0.004 0.020
validate checked IR 0.043 0.395
emit 1.124 7.089
whole process, wall seconds 1.95 10.78
peak RSS, MiB 37 2,035

Emission, especially allocating registers, dominates absolute time. With stack emission the emit stage takes 0.112/0.637 CPU seconds and the whole process 0.83/4.24 wall seconds (SWI/Trealla); outputs still match between hosts. This is an existing cheaper compilation mode, not a substitute for testing the allocating modes. There is no measured speedup from this compatibility repair, and no host/runtime tuning is claimed. An embedded Trealla build and full frontend/interpreter coverage are not established by these native-host measurements.

Trealla 98a821db: speed, memory and where they go (November 2026)

The pin moved from 12f4cbd7 (September 5) to upstream HEAD of November 10, 98a821db, 630 commits later, with one new patch for a wrong-answer defect in it (patches/trealla-heap-page-mark.patch, described in docs/trealla.md). tests/backend-hosts.sh passes and takes 9.2 s instead of 12.7 s. This revision fixes both host defects worked around above: atom_number/2 fails on a symbolic label and (1<<64) /\ \1 is 2^64. The workarounds are still in the source.

-O compile of the 39 single-function modules that experiments/regex-nanofil/hook.c compiles, in one process without the C front end. The outputs are SWI's, byte for byte, in both register modes:

Host Registers Load Sum Median Longest Peak RSS
SWI 9.2.9, saved state pointers 0.05 s 0.2 s 0.7 s 43 MB
Trealla 12f4cbd7 pointers 1.05 s 33.8 s 0.58 s 4.0 s 1.92 GB
Trealla 12f4cbd7 stack 1.05 s 12.1 s 0.21 s 1.24 s 547 MB
Trealla 98a821db pointers 0.50 s 23.8 s 0.40 s 2.8 s 634 MB
Trealla 98a821db stack 0.50 s 8.4 s 0.15 s 0.7 s 229 MB
Fil-C Trealla 12f4cbd7 pointers 5.5 s 4.0 s 26 s 7.5 GB
Fil-C Trealla 98a821db pointers 2.8 s 142 s 2.4 s 16.7 s 2.1 GB
Fil-C Trealla 98a821db stack 2.6 s 48.7 s 0.87 s 4.2 s 0.80 GB

So the new revision takes about 0.7 of the time and a third of the memory, and is still 2 to 4 times slower than SWI with 15 times the memory. The heap patch accounts for 210 MB of that: unpatched 98a821db peaks at 844 MB.

The rest is from the largest of the 39 (3242 lines of assembly), which takes 2.8 s of CPU and 600 MB on Trealla against 0.64 s and 38 MB on SWI.

Memory

Trealla has no garbage collector. A frame, its variable slots and the heap cells built under it are released by backtracking, by a tail call that reuses the frame, or when a deterministic call exits and nothing older refers into it. Binding an older variable to a newer frame's variable or structure pins that frame, and that is what deterministic code that returns terms through output arguments does all the time. nanofil's passes are such code, so nearly everything they touch stays allocated until the pass's caller fails or the process ends.

High-water marks for the largest pattern, and what is live after each stage:

Frames (72 B) Slots (24 B) Heap cells (24 B) Trail (16 B)
after read 65 433 7 k 86
after optimize, 0.3 s 143 k 1.02 M 132 k 170 k
after validation and checks, 0.5 s 175 k 1.29 M 297 k 229 k
in emit, after the first lowering, 1.2 s 544 k 4.15 M 560 k 804 k
in emit, after register allocation, 2.7 s 1.33 M 10.6 M 1.76 M 1.93 M
high-water mark 1.37 M 10.75 M 5.52 M 1.94 M

The high-water marks come to 520 MB of the 600 MB. Emission is 2.3 of the 2.8 s and four fifths of the memory, and within it the register allocator and its checker (placed_lines).

Running a stage inside findall/3, so that leaving it backtracks, gives the same assembly in the same time: the peak is 558 MB with the stages isolated on unpatched 98a821db, and 362 MB with the heap patch and the three steps of one function's emission isolated as well. What is left is inside one call of the register allocator. This was a scratch driver; nanofil's source is unchanged.

Time

Hardware counters are unavailable in an orb, so there are no cache or branch figures.

What would help

Scryer Prolog: not worth supporting yet (October 2026)

Scryer 0.10.0 (nixpkgs scryer-prolog) ran a scratch copy of src/ on Lua 5.4.7's files. The output was identical to SWI's: cc-ir and -O cc of lvm.c (76,757 lines of assembly) and lapi.c, and the preprocessor's tokens of lvm.c matched filcc -E's. It was slower and larger everywhere:

lvm.c SWI 9.2.9 Scryer 0.10.0
startup (empty file) 0.3 s 2.0–2.4 s
tokens of the .i 0.8 s 4.5 s
preprocess + tokens 3.0 s, 314 MB 12.5 s, 536 MB
cc-ir 1.7 s, 133 MB 7.6 s, 367 MB
-O cc 3.5 s, 216 MB 14.5 s, 737 MB

Scryer's packed strings did not make the character-list preprocessor cheaper: text it builds (spliced lines, spellings) is ordinary list cells again. Scryer also compiles all of src/ on every run.

Running there took, besides one real fix (an operator atom as an operand must be bracketed, Op == (=), not Op == '='; now in c_lower.pl):

So "Trealla's dialect" also leans on Trealla's SWI-like extras. A src/scryer/ layer would be about 60 lines plus a format wrapper, but at 4x SWI's time it buys only a strictness check. Revisit when Scryer is faster or caches compiled code.

SWI-Prolog: what the preprocessor's time was (October 2026)

Preprocessing Lua 5.4.7's lvm.c (65,628 tokens) took 3.2 s, against 0.06 s for filcc -E. Measured on SWI 9.2.9: