Fil-C on Nix

Filnix wiki

nanofil/docs/archive/frontend.md

The frontend's one typed tree: the record

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

Archived from campaign.md in November 2026. In October 2026 four walks of the C expression grammar each had their own idea of types; steps 3 to 5 below made them one elaborated tree that lowering, integer constants and address constants all read. The work is done and the code is the reference (c_types.pl, c_lower.pl); this page keeps the review that started it, the design, and the record of each step.

Where we stand (October 2026)

Rounds 1 and 2 are logged in factorings.md. An outside review in October 2026 found:

When this review was written, four walks traversed the same C expression grammar, each with its own idea of types and categories (steps 3 to 5 below made them one tree, read three ways):

Walk Where Lines
Lowering c_lower.pl: obj//3, rvalue//3, decay//3, convert//3 ~380
Judgment c_types.pl: expression_type/3, expression_category/3 ~120
Integer constants c_types.pl: const_eval/4, kind_binary, const_convert/4 ~150
Address constants c_lower.pl: addr_const/5, lvalue_const/5, expr_chunk/7 ~130

Smaller ones: scalar-type facts spread over eight tables (int_info/3, rank/2, unsigned_of/2, type_class/2, load_op/3, store_width/2, narrow_ext/2, conversion_int_type/2); C operators tabled about six times (c_parse, c_types, c_lower), with punctuators and identifier characters defined in both c_lex.pl and c_pp.pl. (The Fil-C checks, stated separately by filc_amd64.pl and interp.pl, now have an IR form: pizlonation.md.)

Completed frontend design: one typed tree

Steps 3–5 below implemented this design. Its rationale is retained here: elaborate each full expression once into a typed tree, and let every consumer fold over it: IR emission, integer constants and address constants. One description of what each C expression is, read three ways.

% X     ::= val(Type, Node) | obj(Type, Place) | fun(Type, Designator)
% Place ::= local(Name) | global(Sym) | deref(X) | at(Place, Off)
%         | literal(Lit) | complit(Type, Init)
% Node  ::= int(N) | float(Bits) | conv(Conversion, X) | bin(Op, X, X) | ...
% Conversion ::= load | decay_array | decay_function | int(From, To)
%              | int_float | float_int | ptr_int | int_ptr | bool | void
%              | null_ptr

Frontend roadmap and implementation record

Update the status column when a step lands, with its commit.

# Step Verify Status
1 Commit the snapshot gate, tests/snapshot.sh. Plant a one-token change in arith_op/3; the gate fails. Done: 1006 outputs; the planted change made 6 differ.
2 One scalar table (scalar/5 or similar) replacing the eight scalar-fact tables; one C operator table replacing binop/3, assign_op/1, binary_kind/2, cmp_op/3, arith_op/3, compound_op/2; one punctuator and identifier-character definition shared by c_lex.pl and c_pp.pl. Snapshot identical; pptest.sh; unit tests and Nix checks on both hosts. Expect about −100 lines. Done, −53 lines: integer_type/4 and ir's narrow_type/3; c_operator/4 with compound_assignment/2; c_lex's punctuator/1, digraph/1 and character classes, shared by c_pp. Not done: const_arith/4, cmp_op/3 and arith_op/3, which map C ops to IR ops and belong to step 3.
3 elaborate/4 as the judgment: expression_type/3 and expression_category/3 become projections, and the elaborator raises diagnostics. Assert old and new agree on every subexpression the snapshot reaches; snapshot identical; reject tests reject. First part done: elaborate/3 builds the tree, the two old predicates are its projections, and 15,413 queries agreed with the old judgment. Second part done: every operand is elaborated (call arguments, assignment sources, comma left operands, ?: conditions, sizeof operands, va_arg's list), and src(E) is gone. No child failed to elaborate on the snapshot or on zlib and Lua. Builtin arguments, statement-expression blocks and compound-literal initializers stay as source. Third part begun: c_lower judges each full expression with elaborate/4 first, and fault/4 diagnoses one that cannot be typed with lowering's messages, in its evaluation order. The elaborator now checks the constraints lowering did (modifiable targets, assignment and argument conversions, call arity, scalar conditions and logical operands, & of a value), and on the corpora raises 20 diagnoses itself with lowering's texts. It declines only where lowering refuses first something C allows (unsupported types, structs by value, va_arg, statement expressions). Remaining for step 4: delete lowering's duplicate checks as each form lowers from the tree.
4 Lower from the tree, form by form, through src(E): literals and conversions (deleting decay//3, assign_convert//5), then places, then ?: and assignment, then calls. Delete obj//3 and rvalue//3. Snapshot identical after every form. Done. rval//3 elaborates, then lower//3 and place//3 translate the tree; nothing lowers from source any more, and the source argument is gone. Deleted: obj//3, rvalue//3, decay//3, expr_type//3, lower_call and the duplicate checks for ?:, assignment, ++/--, casts and calls. Each node has one clause, with its C11 section: places (identifiers, __func__, string literals, compound literals, *, .) and values (constants, conversions, operators, &&/`
5 Integer and address constants as folds over the tree: const_eval/4 loses its own typing, addr_const/lvalue_const fold places to sym(S, Off). The #if cases against clang -E, pptest.sh, snapshot, nanofil-zlib, nanofil-lua. The four walks should be one. Done. const_eval/4 elaborates, then folded/3 does arithmetic on the tree with the elaborator's conversions (binary_operands/6 gives a comparison's common type); kind_binary lost its typing. #if elaborates with Env = pp: env_int_type/3 at constants and int-yielding operators states 6.10.1p4 once, and refuses floating constants there. Null pointer constants are judged on the elaborated tree, not by elaborating the source again. c_lower's address_constant/5 and place_constant/5 replace addr_const/5 and lvalue_const/5, one clause per tree node, which closed two gaps against clang, both now in tests/c/static-array-address.c: &x[1][2] (an array lvalue that decays) and 0 + 0 as a null pointer. Snapshot identical apart from cproc's initializer-nested-array-address.c, which now compiles. −16 lines in src/.