Fil-C on Nix

Filnix wiki

mbrock/filnix

nanofil/nix/rocq.nix

The Rocq model of the checked IR (rocq/, docs/formal.md Campaign 1): default: model (every theory compiled, one derivation per file) + a small outcome/base-verdict smoke corpus. The grammar round trip of web/rocq/tests/parse.sh is its own check, passthru.parse (checks.nanofil-rocq-parse), also one derivation per file. full=true opts into the complete outcome and mutation campaign below: compile it with the pinned nixpkgs' Rocq, refuse any Admitted or Axiom (none is allowed outside the Campaign 2 runtime module type in Runtime.v), extract and link the model, then check that it agrees with interp:run/5 on every run line of rocq/tests, the Fil-C IR fixtures, the C tests and Fil-C modules from tests/gen.pl, the last three with and without -O and under every --checks mode (rocq/agree.sh), and Lua's own functions (rocq/lua.sh); and that its validator reaches ir:valid_module/1's verdict on mutants of every checked module in the same matrix, and of the tests/c/prog programs with aliases and weak, thread-local or constructor data (rocq/valid.sh). Both check that the modules the program proofs are about are what nanofil makes today (rocq/programs.sh). The program logic (Language.v, Logic.v) is built on Iris, so the model and the parse check take stdpp and Iris from the same nixpkgs; the extracted driver does not use them.

λpkgs, filcc, full?
pkgs
filcc
fulldefault false
let
27rocq=
pkgs.rocqPackages
28ocamlPackages=
rocq.rocq-core.ocamlPackages
uses rocq used by driver
30rocqInputs=
rocq.rocq-core rocq.stdlib rocq.stdpp rocq.iris
uses rocq used by model parseInputs vo

Each theory is its own derivation, whose inputs are its one .v file and the objects of the theories it imports, so an edit recompiles that file and its importers and nothing else; every other object comes from the store or a cache. Imports are read here from the sources' From Nanofil Require sentences; deps below checks them against rocq dep.

43names=
maplib.removeSuffix".v" lib.filterlib.hasSuffix".v" lib.attrNamesbuiltins.readDirtheories
46imports=
lib.genAttrsnames n: lib.uniquelib.concatMapm: lib.filterw: builtins.isStringw ∧ w ≢ "" builtins.split"[[:space:]]+" builtins.elemAtm 2 lib.filterbuiltins.isList builtins.split"From[[:space:]]+Nanofil[[:space:]]+Require[[:space:]]+((Import|Export)[[:space:]]+)?([^.]*)\\." builtins.readFiletheories + "/${n}.v"
65closure=
lib.genAttrsnames n: lib.uniquelib.concatMapd: closure.${d} ++ d imports.${n}
uses imports lib names used by link

link N: the objects of everything N imports, under theories/.

69link=
n: lib.concatMapStringsd: "ln -s ${vo.${d}}/${d}.vo theories/${d}.vo\n" closure.${n}
uses closure lib vo used by reprint vo
74vo=
lib.genAttrsnames
λn
pkgs.runCommand"nanofil-rocq-${n}" nativeBuildInputs = rocqInputs ++ pkgs.python3;
mkdir theories extraction
cp ${theories + "/${n}.v"} theories/${n}.v
python3 ${watch} --label "Rocq compile ${n}.v" --warn-after 120 --timeout 900 -- \
rocq c -q -R theories Nanofil theories/${n}.v
mkdir $out
cp theories/${n}.vo theories/${n}.glob $out/
# Extract.v writes the OCaml model.
if [ -n "$(ls extraction)" ]; then cp -r extraction $out/; fi

The grammar check (web/rocq/tests/parse.sh), one theory at a time: reprint it with every subterm in parentheses and compile the reprint against the original objects of its imports. It needs neither its own object nor the other reprints, so a grammar change reruns every file at once and a theory edit reruns that file.

103parseInputs=
rocqInputs ++ pkgs.swi-prolog pkgs.python3
107reprint=
lib.genAttrsnames
λn
pkgs.runCommand"nanofil-rocq-reprint-${n}" nativeBuildInputs = parseInputs;
mkdir -p project/theories verdicts
echo "-R theories Nanofil" >project/_RocqProject
(
cd project
)
cp ${theories + "/${n}.v"} project/theories/${n}.v
PARSE_TMP=$PWD/verdicts PARSE_IN_PLACE=1 python3 ${watch} --label "Rocq reprint ${n}.v" \
--warn-after 120 --timeout 900 -- bash ${parseSrc}/web/rocq/tests/parse.sh --check 0 "$PWD/project/theories/${n}.v"
grep -q '^ok' verdicts/verdict-0
mkdir $out
cp verdicts/verdict-0 $out/verdict

parse.sh's own Ltac, term and Iris fixtures.

127fixtures=
pkgs.runCommand"nanofil-rocq-parse-fixtures" nativeBuildInputs = parseInputs;
python3 ${watch} --label "Rocq grammar fixtures" --warn-after 120 --timeout 900 -- \
bash ${parseSrc}/web/rocq/tests/parse.sh --fixtures | tee log
mkdir $out
cp log $out/
136parse=
pkgs.runCommand"nanofil-rocq-parse" { }
mkdir $out
{
${lib.concatMapStringsn: "cat ${reprint.${n}}/verdict\n" names}
cat ${fixtures}/log
} | tee $out/verdicts
echo "parse: $(grep -c '^ok' $out/verdicts) files read and reprinted"
144theorySrc=
lib.fileset.toSource
151driver=
pkgs.stdenv.mkDerivation
152name=
"nanofil-rocq-driver"
153dontUnpack=
true
154nativeBuildInputs=
158buildInputs=
159buildPhase=
cp ${vo.Extract}/extraction/nanofil_model.ml ${vo.Extract}/extraction/nanofil_model.mli .
ocamlfind ocamlopt -w -a -package zarith -linkpkg \
nanofil_model.mli nanofil_model.ml main.ml -o nanofil-model
165installPhase=
mkdir -p $out/bin
cp nanofil-model $out/bin/

The compiled theories as one tree, for the logic spikes (spikes.nix), which compile against them. Nothing in CI's gate needs it.

172objects=
pkgs.runCommand"nanofil-rocq-objects" { }
mkdir -p $out/share/rocq/theories
cp ${theorySrc}/rocq/_RocqProject $out/share/rocq/
cp ${theorySrc}/rocq/theories/*.v $out/share/rocq/theories/
${lib.concatMapStringsn: "cp ${vo.${n}}/${n}.vo ${vo.${n}}/${n}.glob $out/share/rocq/theories/\n" names}

The model: every theory compiled, no Admitted or stray Axiom, imports as rocq dep reads them, and the extracted driver, with the sources the checks read (programs.sh compares the Code.v files). Its output is a few MB; the objects stay in their own derivations.

184model=
pkgs.runCommand"nanofil-rocq-model"
187nativeBuildInputs=
188passthru=
inherit vo reprint parse objects driver;
cd ${theorySrc}/rocq
# Axioms and parameters only inside Runtime.v's module type, which
# InterpRuntime discharges; no Admitted anywhere.
if grep -rnw Admitted theories ||
grep -rnwE 'Axiom|Axioms|Parameter|Conjecture' --exclude=Runtime.v theories ||
sed '/^Module Type Runtime\./,/^End Runtime\./d' theories/Runtime.v |
grep -nwE 'Axiom|Axioms|Parameter|Conjecture'; then
echo "rocq: no Admitted, and no Axiom outside the runtime module type" >&2
exit 1
fi
# The per-file derivations import what rocq dep says each file needs.
rocq dep -R theories Nanofil theories/*.v |
awk '{ split($1, t, /[\/.]/); d = ""
for (i = 2; i <= NF; i++) if ($i ~ /^theories\/.*\.vo$/) { split($i, s, /[\/.]/); if (s[2] != t[2]) d = d " " s[2] }
print t[2] ":" d }' |
while read -r line; do
f=''${line%%:*}; set -- ''${line#*:}
printf '%s:%s\n' "$f" "$(printf ' %s' $(printf '%s\n' "$@" | sort -u))"
done | sort >$TMPDIR/rocqdep
cat >$TMPDIR/nixdep <<'EOF'
${lib.concatMapStringsn: "${n}:${lib.concatMapStringsd: " ${d}" lib.sortlib.lessThan imports.${n}}\n" lib.sortlib.lessThan names}EOF
sed -i 's/^ *//; s/ *$//' $TMPDIR/nixdep
sed -i 's/ *$//' $TMPDIR/rocqdep
if ! diff -u $TMPDIR/rocqdep $TMPDIR/nixdep; then
echo "rocq.nix: a theory's imports differ from rocq dep's (- rocq dep, + rocq.nix); write them as From Nanofil Require" >&2
exit 1
fi
${lib.concatMapStringsn: "test -e ${vo.${n}}/${n}.vo\n" names}
echo "@@nanofil-rocq ${toStringbuiltins.lengthnames} theories compiled"
mkdir -p $out/bin $out/share/rocq/theories $out/share/rocq/extraction
cp ${driver}/bin/nanofil-model $out/bin/
cp theories/*.v $out/share/rocq/theories/
cp _RocqProject $out/share/rocq/
cp ${../rocq/extraction/main.ml} $out/share/rocq/extraction/main.ml

The check reads compiler sources, harnesses and corpora, not the wiki, benchmarks or theories (the latter enter through model above).

The corpus reads the extracted driver (Extract.v's imports, a few of the theories) and the sources, so it runs beside the proof compiles; the check below still requires the whole model.

262corpusCheck=
pkgs.runCommandCCif full then "nanofil-rocq-full-corpus" else "nanofil-rocq-corpus" nativeBuildInputs = pkgs.swi-prolog pkgs.python3 rocq.rocq-core rocq.stdlib rocq.stdpp rocq.iris;
set -o pipefail
start=$SECONDS
mark() {
echo "@@nanofil-rocq $((SECONDS - start))s $*"
start=$SECONDS
}
cp -r ${corpus} nanofil
chmod -R u+w nanofil
cp -r ${theorySrc}/rocq/. nanofil/rocq/
chmod -R u+w nanofil
cd nanofil
patchShebangs rocq bin tests
export FILCC=${filcc}/bin/clang MODEL=${driver}/bin/nanofil-model
bin/nanofil --save-state "$TMPDIR/nanofil.state"
export NANOFIL_STATE=$TMPDIR/nanofil.state
# The modules the program proofs are about are nanofil's current
# products (rocq/programs.sh, Panics.v).
python3 tests/watch.py --label "Rocq program fixtures" -- bash rocq/programs.sh
mark programs
+
→
# A deliberately small, stable smoke corpus: every hand-written model
# fixture plus ordinary checked IR, long doubles and data/aliases.
export NANOFIL_CHECKS=reference NANOFIL_JOBS=$NIX_BUILD_CORES NANOFIL_MUTANTS=0
tests/par.sh rocq/agree.sh rocq/tests/*.ir tests/filc/basic.ir tests/c/ldouble.c tests/c/prog/alias.c \
| tee "$TMPDIR/quick-outcomes.log"
tests/par.sh rocq/valid.sh rocq/tests/*.ir tests/filc/basic.ir tests/c/ldouble.c tests/c/prog/alias.c \
| tee "$TMPDIR/quick-valid.log"
mkdir -p $out
cp "$TMPDIR"/quick-*.log $out/
mark quick-base-verdicts
else
python3 tests/watch.py --label "mutant partition coverage" -- bash rocq/tests/partitions.sh
# Hand-written checked IR (rocq/tests) goes in as written: the check
# passes take pizlonate's output, never renew_hull or renew_if.
NANOFIL_CHECKS=reference NANOFIL_JOBS=$NIX_BUILD_CORES tests/par.sh rocq/agree.sh rocq/tests/*.ir
# Forty Fil-C modules of ten random functions (tests/gen.pl's filc
# mode), which trap often and reach every reuse form, join the
# matrix below.
mkdir gen
for seed in $(seq 1 40); do
(cd src && ../bin/nanofil --script ../tests/gen.pl $seed 10 filc </dev/null) > gen/gen-$seed.ir
done
# Lua 5.4.7's lctype.c, lobject.c and lstring.c, the source nix/lua.nix
# builds, with the run lines of rocq/tests/lua.c, join the outcome
# matrix; the validator judges their mutants once, below, since each
# is judged on the whole module.
mkdir luasrc
tar -xf ${pkgs.lua5_4.src} -C luasrc --strip-components=1
rocq/lua.sh luasrc/src >lua.ir
mark prepare
# Modes are independent, just as in the validator matrix below. Keep
# each mode's complete log and propagate any failing xargs child.
for opt in plain -O; do
for checks in reference reuse renew guard hoist schedule half forward combined hull getter extended; do
echo "$opt $checks"
done
done | xargs -P "$NIX_BUILD_CORES" -L 1 bash -c '
set -o pipefail
opt=$0; [ "$opt" = plain ] && opt=
t0=$(date +%s)
NANOFIL_OPT=$opt NANOFIL_CHECKS=$1 NANOFIL_JOBS=1 tests/par.sh rocq/agree.sh tests/filc/*.ir tests/c/*.c gen/*.ir lua.ir \
2>&1 | tee "$TMPDIR/agree-$0-$1.log" | grep --line-buffered "^@@nanofil-" \
|| { echo "### agree NANOFIL_OPT=$opt NANOFIL_CHECKS=$1"; cat "$TMPDIR/agree-$0-$1.log"; exit 1; }
echo "agree NANOFIL_OPT=$opt NANOFIL_CHECKS=$1: $(grep -c "^/" "$TMPDIR/agree-$0-$1.log") files agree ($(( $(date +%s) - t0 ))s)"'
mark outcomes
# The validator against ir:valid_module/1 on mutants of every checked
# module (rocq/valid.sh, rocq/mutants.pl), in every mode at once.
NANOFIL_CHECKS=reference NANOFIL_JOBS=$NIX_BUILD_CORES tests/par.sh rocq/valid.sh rocq/tests/*.ir
# (xargs drops empty words, so "plain" stands for no -O.)
for opt in plain -O; do
for checks in reference reuse renew guard hoist schedule half forward combined hull getter extended; do
echo "$opt $checks"
done
done | xargs -P "$NIX_BUILD_CORES" -L 1 bash -c '
set -o pipefail
opt=$0; [ "$opt" = plain ] && opt=
t0=$(date +%s)
NANOFIL_OPT=$opt NANOFIL_CHECKS=$1 NANOFIL_JOBS=1 tests/par.sh rocq/valid.sh tests/filc/*.ir tests/c/*.c gen/*.ir \
2>&1 | tee "$TMPDIR/valid-$0-$1.log" | grep --line-buffered "^@@nanofil-" \
|| { echo "### valid NANOFIL_OPT=$opt NANOFIL_CHECKS=$1"; cat "$TMPDIR/valid-$0-$1.log"; exit 1; }
echo "valid NANOFIL_OPT=$opt NANOFIL_CHECKS=$1: $(grep -c "^/" "$TMPDIR/valid-$0-$1.log") files agree ($(( $(date +%s) - t0 ))s)"'
mark validator-matrix
# The C programs of tests/c/prog that declare an alias, a weak
# symbol, thread-local data or a constructor, with the reference
# checks only: they bring the corpus's aliases and its kinds of data
# linkage. Not those with a "// cflags:" line, which compile as
# progtest.sh does and not as cc-ir does.
grep -lE 'alias *\(|weak|_Thread_local|__thread|thread_local|constructor|destructor' \
$(grep -L '^// cflags:' tests/c/prog/*.c) >prog.list
xargs -P "$NIX_BUILD_CORES" -n 1 bash -c '
set -o pipefail
NANOFIL_CHECKS=reference NANOFIL_JOBS=1 tests/par.sh rocq/valid.sh "$0" 2>&1 \
| tee "$TMPDIR/prog-$$.log" | grep --line-buffered "^@@nanofil-" \
|| { echo "### valid $0"; cat "$TMPDIR/prog-$$.log"; exit 1; }' <prog.list
# Lua's whole-module mutant generation alone takes minutes on one
# core. Once the other programs finish, share its independent item
# partitions across the available cores, without nested matrix fanout.
NANOFIL_CHECKS=reference NANOFIL_JOBS=1 NANOFIL_MUTANT_JOBS=$NIX_BUILD_CORES \
tests/par.sh rocq/valid.sh lua.ir | tee "$TMPDIR/prog-lua.log"
echo "valid tests/c/prog and lua.ir: $(($(wc -l <prog.list) + 1)) modules agree"
mark validator-programs
mkdir -p $out
cp "$TMPDIR"/agree-*.log "$TMPDIR"/valid-*.log "$TMPDIR"/prog-*.log $out/
in
pkgs.runCommandif full then "nanofil-rocq-full-check" else "nanofil-rocq-check"
392passthru=
393inherit
model
394inherit model
parse
# Every theory compiled and the model's guards, and the corpus.
test -e ${model}/bin/nanofil-model
cp -r ${corpusCheck} $out