A C99 compiler for six targets -- {Linux, macOS, Windows} x {x86-64,
arm64} -- that writes the executables itself (ELF, Mach-O with an ad-hoc
signature, PE) directly from its own encoders, compiles itself, and ships as one
file, unisacc.com. Toolchain status (0.0.19): -c -b os/arch writes a
relocatable object in the target's own format -- ELF, Mach-O or COFF -- that the
system linkers (GNU ld, lld, ld64, lld-link) accept, and that may read data
symbols a C compiler defined; -c -b os/arch -funit writes a unit object for
separate compilation, and unisacc a.o b.o lib.a [-o prog] links unit objects
and archives (unisacc ar rcs|t|x) for any of the six targets. The
reference compiler writes objects for all six targets; the shipped unisacc.com links
unit objects, writes archives, and writes -c -b lnx/ARCH objects, but refuses -c -b
for osx/win by name (exec/c/compiler.c:510).
Since 0.0.18, -S -b lnx/x86_64|lnx/arm64 -o FILE.s writes that object as GNU assembly
(AT&T syntax on x86-64) which the system assembler accepts, and unisacc as
assembles it -- or hand-written code in the same subset -- back into the
identical object; unisacc nm and unisacc objdump -d read the same objects. Assembly text for Mach-O and COFF, Intel syntax and inline asm are not scheduled (moved to the 0.1.x appendix). The design and the back-end facts behind it are in
docs/toolchain.md; the plans in archive/plans/v0.0.19.md (published) and archive/plans/v0.0.20.md (published) and archive/plans/v0.0.21.md (published). "C99" is ISO/IEC 9899:1999 as amended by TC1-TC3 (the text is WG14
N1256), and how much of it is covered is a number from a clause-by-clause
ledger, not a claim -- see C99 coverage. The front end takes
C99 as written in real projects: jsmn, cJSON, miniz, stb, tinyexpr and the
c-testsuite corpus compile and run (realprog, tools, corpus gates), and
since 0.0.18 so does the kilo editor; processes (fork, execvp, waitpid, pipe, dup2, getcwd, sys/wait.h) arrived in 0.0.19 on Linux and macOS. Lua is in the realprog passing baseline; SQLite still needs pwd.h (see tests/realprog.sh). The language-side gaps are complex
types and trigraphs, and the bundled C library is a documented subset;
all are itemised in the limitations table below. unisacc is a
neural-network-based compiler: in the default product, compilation stream-stages
run on constructed integer threshold networks and a generic executor. Their
weights come deterministically from a TSV table DSL for finite state
transitions, not from training. The classic reference is a separate route; its
18 fact networks are constructed and checked, while its runtime queries DENSE
answer tables enumerated from them.
TSV tables are the construction and verification method used by this product, not a defining requirement of a neural-network-based compiler. Other network construction routes remain research possibilities, not shipped capabilities.
The model compiler is built from declarations, dynamic facts and
shared templates. Offline construction tools build and check integer networks;
the packaged compiler runs without Python. The TSV table DSL declares a
state, a finite observation or * default, a next state, and an ordered action
sequence. Parameterised template rows expand using build-time facts into a
complete finite transition table; the constructor builds a threshold network
from that table, and --check-net enumerates its declared domain to check
network = table. The expander, fact binding and packer are part of the
build-time trusted base. This equality check does not prove that the table
implements C99. The classic C and Python implementations are archived
(archive/reference-c/, archive/reference-py/) and kept only for
comparison; there is no classic fallback build any more, and a model refusal
never silently retries through another route.
There are exactly two product names, and they mean two different things:
| name | what it is | shipped? |
|---|---|---|
unisacc-seed.com |
the first generation: built by the host, with the host's compiler as the --via |
no -- a seed is a build input |
unisacc.com |
the final product: the compiler rebuilt by the seed, and then by itself | yes |
The N22 bootstrap contract starts with a host-built unisacc-seed.com, rebuilds
the compiler with that seed, then rebuilds it with the second generation; all
three SHA-256 values must match. The original N22 receipts
retain the exact historical candidates, hashes and build lineage. They do not
qualify the current candidate.
The installed unisacc.com is still a product of the old C chain. The CX entry
(src/unisacc.cx) is built by tools/entry-build.cx as an lnx/x86_64 ELF candidate
with the installed P3 package. It matches the installed product on all 148
entry cells, but it is not yet the product. The steps from that candidate to
a five-slice APE product are listed, not yet executed, in
candidate to product. As of
2026-10-30, lnx/arm64 objects compile, but osx/* and win/x86_64 do not:
the model driver's -c -b does not yet reach non-Linux targets.
tools/product-build.cx, tools/hostseed-step*.cx and tools/selfhost-mixed.cx
still compile or link the archived C reference, so they cannot run. They
retire with the switch; see the retirement table in that plan. The Python-running
exec/c/buildcompiler.sh (with the Makefile's com, model-com and comboot-*
targets) and release/tools/seal_candidate.sh are already archived under
archive/reference-py/.
tools/kernel-build.cx (kernels) does not reference archived
sources. tools/package-refresh.cx (packages) still compares
exec/c/core.c, which is archived, so it too must be reworked before the
switch's package refresh. unisacc-next.com is only a build-directory output
name. The live Python left on any path is listed in
python residue.
Published versions, their sizes, SHA-256 and acceptance receipts: GitHub releases, with the per-version rows and the current model byte ledger in prd §0. Development state, candidate identities, evidence boundaries and measurement history are specification material and live there too -- this file is the user entry point, not the status page.
unisacc.com is a single file that runs on all six targets (Windows reads it
as a PE; a Unix shell runs it as a script that picks the slice for the
machine). The C headers it needs travel inside it.
./unisacc.com hello.c [args] # compile and RUN in memory (the default; not cc's a.out)
./unisacc.com hello.c -- --flag -x # arguments that start with '-' go after --
./unisacc.com -run hello.c [args] # compile and run in memory, nothing on disk
./unisacc.com hello.c -o hello # write an executable for this machine
./unisacc.com hello.c -b osx/arm64 -o hello # write an executable for a target
./unisacc.com -O2 a.c b.c -b lnx/x86_64 -o prog
./unisacc.com -E file.c # preprocess only
./unisacc.com hello.c -S -b lnx/x86_64 -o hello.s # GNU assembly of the object -c -b would write
./unisacc.com as hello.s -o hello.o # assemble it back (also hand-written code in the subset)
./unisacc.com hello.c -S --tapebin -o hello.tapebin # write the portable tape container
./unisacc.com -run hello.tapebin # run its recorded program
./unisacc.com hello.tapebin -b osx/arm64 -o hello- Default mode is run:
unisacc file.c [args]with neither-runnor an output flag compiles and runs the program in memory (owner decision 2026-10-01; 0.0.17); this is deliberately unlike cc's silenta.out. Writing a file always takes-o(or-b target,-S,-c,-E). Releases up to 0.0.16 wrotea.outlike cc. - Flags:
-O0/-O/-O1/-O2/-O3/-Os(-O3uses the-O2pass set;-Oand-Osuse-O1),-o,-b os/arch,-run,-I,-D,-include,-E,-M/-MM/-MD/-MMD/-MF/-MT/-MQ/-MP(gcc's.dbytes;-Mlists what-MMlists, the bundled headers are not files),-nostdinc,-ftrim-libc/-fno-trim-libc,--version;-Wall,-Wextra,-g,-std=c99are accepted. A#!first line is skipped. - Headers: the standard/compatibility headers (
assert ctype dirent errno float inttypes iso646 limits locale math memory signal stdarg stdbool stddef stdint stdio stdlib string time wchar) plus, on Linux and macOS, POSIX ones implemented on kernel calls (unistd fcntl poll termios sys/stat sys/ioctl sys/wait sys/time sys/socket sys/select netinet/in netinet/tcp arpa/inet netdb), andunisacc_ffi.h. Every carried header compiles when included on its own (checked byhosthdr). Ordinary library calls use the bundled C implementations, compiled on demand;-ftrim-libc(on by default since 0.0.11;-fno-trim-libckeeps every library body;-libneedis a compatible alias) conservatively selects library bodies using identifier and dependency closures. On macOS, the explicit dl/libffi bridge can call system APIs. Since 0.0.19,-runon macOS also forwards an external function that has a prototype, no definition and no bundled body to the host libc (dlsym + libffi); variadic and struct-by-value calls are refused by name. Direct C-runand-oroutes can emit host-forwarding stubs for Linux, macOS and Windows targets; symbols are resolved on the system executing the program, not on the build host. Object/archive linking and-S/-cdo not use this route. A missing forwarded function is diagnosed at runtime with exit status 127. See host interop for calling limits. System FILE/va_list and allocator families must not be mixed with the bundled implementations. - Several files, one program:
unisacc a.c b.c ...compiles all the units of a program in one invocation. Each unit is preprocessed on its own, and a later unit's file-scopestaticnames are renamed, so two units may each keep their ownhelper. - Not a
ccdrop-in yet: bare-cand bare-S(no-b) write the compiler's intermediate tape (so does-S -b Twithout a.soutput);-S -b lnx/ARCH -o FILE.swrites GNU assembly.unisacc astakes the subset the back end emits (plus.text/.data/.bss,.globl,.byte/.ascii/.zero); other instructions, Intel syntax and inlineasmare refused by name (0.0.19).-c -b os/archwrites an object (reference compiler): a whole-program object links with the system linker on its own and may read data symbols from cc-compiled objects, but cannot call cc-compiled functions or be called by them yet (the calling convention differs; 0.0.18). Unit objects (-funit) are linked byunisaccitself, not by the system linker, because only unisacc chains every unit's initialisers.-l/-Lare still accepted and ignored; pass archives by path. C11 features and GCC extensions (statement expressions,_Generic, empty structs, inline asm) are outside the subset. - Tape origin:
.tapebinrecords the target selected when C was compiled to tape. A different-btarget is rejected unless--force-originis given; forcing it does not undo target-specific preprocessing. See the v1 format.--tapebinwith-S/-cwrites the container; the same file can be read by-runor-b <target>.
sh ./unisacc.com tools/entry-build.cx -- ROOT P3 OUT # candidate from src/unisacc.cx: OUT/candidate, OUT/receipt.txtThe compiler entry is src/unisacc.cx. tools/entry-build.cx compiles its 33
objects and links them, each step under tools/bound, then appends the P3
package taken from the file P3 (usually the installed unisacc.com). It does
not replace ROOT/unisacc.com. The full usage, including --target, --only
and --link-only for splitting a build into bounded batches, is in the comment
at the top of the tool. Turning that candidate into the six-target product
(five slices, APE wrap, install) is a list of steps in
archive/plans/2026-11-01/candidate-to-product-2026-10-30.md;
as of 2026-10-30 the candidate covers lnx/x86_64 only and the installed
unisacc.com has not been replaced.
The classic route is archived: unisacc.c, src/*.c and the C sources of the
old driver are in archive/reference-c/; tests/build_ref.sh,
tests/export_ref.sh and tests/release.sh are in
archive/testing-c43-legacy/tests/. make com and make classic-com no
longer exist; exec/c/buildcompiler.sh is archived under archive/reference-py/. The generated
kernel/weight.<stage>.inc and kernel/dense.<stage>.inc keep the 18 classic
nets' data; the default product's shared networks remain in its separate P3
package. CI only re-runs tests.
Shell = inferencer + executor + model data.
The default model product compiles through a seven-stream byte pipeline
(below). The classic seed/reference (decision-network instance) is a
separate route: the walker, symbol table, relocation arithmetic and image
writers are ordinary code. Its fact nets use cube networks at ask points,
with one kernel for all of them (embed -> gemv -> ReLU -> gemv -> argmax,
integers only).
The default model product runs compilation as seven stream-stages. Each stream-stage is a constructed integer threshold network driven by one generic executor. Stream-stage boundaries are the I/O contract below (aligned with Paper A §4.2 in research/unisacc-paper.en.md). Tests and model keys should stop at these edges; this is not the same table as the eighteen classic decision nets later in this section.
| stream | input → output | role |
|---|---|---|
| preprocess | source bytes → preprocessed text | line continuation, comments, directives, macros, conditionals, includes |
| lex | preprocessed text → typed token stream | keywords, identifiers, operators, literals, longest match |
| parse | token stream → tape | declarations, scopes, types, precedence, statements, init, calls |
| opt | tape → tape | peephole / stack-to-reg style rewrites on tape |
| prune | tape → tape | drop unreachable whole functions on image/run routes (public tape out skips prune) |
| lowering | tape → target ops + data | ABI, regmap, syscalls, layout, reloc |
| encode + image | target ops → ELF / Mach-O / PE (or in-memory image) | encoding, branches, reloc, headers, Mach-O ad-hoc signature |
tape is the target-independent IR (fixed register set + op vocabulary). The
eighteen-net table below is the decision-network / classic-reference
instance (finite key → class at ask points). It does not replace the
seven-stream byte-pipeline contract above.
| stage | key | out | keys | units |
|---|---|---|---|---|
pp |
dir(9) x defined(2) | 4 classes | 18 | 6 |
lex |
c(12) x peek(12) | 10 classes | 144 | 11 |
parse |
nt(5) x tok(68) | 36 classes | 340 | 34 |
type |
t1(15) x op(19) x t2(15) | 16 classes | 4,275 | 62 |
scope |
ctx(6) x kind(5) | 7 classes | 30 | 9 |
irsel |
family(6) x flavor(70) | 70 classes | 420 | 75 |
enc |
op(87) x os(3) x arch(2) | 5 classes | 522 | 5 |
reloc |
kind(3) x arch(2) | 3 classes | 6 | 3 |
regmap |
treg(8) x arch(2) | 16 classes | 16 | 10 |
tyinfo |
t(16) | 3 heads | 16 | 6 |
pfconv |
conv(9) | 7 classes | 9 | 7 |
peep |
a(8) x b(17) x rel(12) | 10 classes | 1,632 | 18 |
opinfo |
op(73) | 3 heads | 73 | 16 |
prec |
op(19) | 11 classes | 19 | 11 |
binsel |
op(16) x sign(2) | 23 classes | 32 | 18 |
isel |
op(87) x arch(2) | 2 heads | 174 | 88 |
abi |
op(87) x os(3) x arch(2) | 13 heads | 522 | 69 |
combo |
op(87) x os(3) x arch(2) | 15 heads | 522 | 121 |
| total | 8,770 | 569 |
Frozen text. It was generated by python3 -m unisa docs and checked by tests/docs.sh; both are archived (archive/reference-py/unisa/docgen.py, archive/testing-c43-legacy/tests/docs.sh). Nothing regenerates or checks this table today.
The last two are a control arm: the lowering asks neither. Units are the constructed weights' hidden units -- what ships -- not trained parameters.
Exactness is constructed, not searched. The weights are derived from the
gold decision tables (weights/gold/*.tsv is the tables as data): W1 in
{0,1}, b1 recoverable from W1, W2 small powers of two. The hidden
activation is always 0 or 1, so inference has no multiply and no float.
Verification is exhaustive, not statistical. Each net is a total
function on a finite domain, so "the net equals the table" is decided by
enumerating every key (the total is in the table above). That proves the net
equals the table; whether the table is right about C is checked separately,
against the system compiler (tests/hostreferee.cx type, difftest).
Development runs on the installed compiler itself: every tool is a .cx
file run directly with sh ./unisacc.com FILE.cx -- ARGS (the default mode is
run). The Python seed unisa/ is archived at archive/reference-py/unisa/
(manifest: archive/reference-py/README.md),
and the classic C reference at archive/reference-c/
(archive/reference-c/README.md); both are kept
for comparison only. Each tool's usage is in the comment at the top of its file.
sh ./unisacc.com tools/facts-gen.cx -- --check . # the 30 generated exec/facts tables
sh ./unisacc.com tools/gold-table.cx -- --check . # weights/gold/*.tsv: domain, classes, rule forms
sh ./unisacc.com tools/kernel-emit.cx -- --check . kernel # kernel/ (41 files; generated, never hand-edit)
sh ./unisacc.com tools/cx-flatten.cx -- --check . # include/cx-flat.h from include/cx-nested.h
sh ./unisacc.com tests/run.cx -- --smoke # local smoke; --all fails while coverage is incomplete--write in place of --check regenerates (for gold-table.cx it takes a
STAGE). On 2026-10-30 the facts-gen, gold-table and cx-flatten checks and the
smoke passed on this tree. kernel-emit --check reports DIFF for all 41
committed kernel/ files: their provenance comments still name the archived
Python emitter, and the committed kernel/unisa_headers.inc came from the
header exporter below. kernel/unisa_headers.inc is written by
tools/carried-header-export.cx (prepare, --verify, install). The
compiler entry is src/unisacc.cx, in six layers (host, packmodel, executor,
stages, drive, bind); tools/entry-build.cx builds a candidate from it.
ARCHITECTURE.md maps the files, and
plans/cx-layers.md the layering: only cx/text,
cx/mem and cx/sys (the bottom layer) call libc/POSIX directly, and only
include/, tools/cxcc-hostinc/ and cx/sys/ may use #include
(tests/cxinclude-boundary.tsv). The ratchets tests/cxraw.cx,
tests/cxinclude.cx, tests/cxapis.cx and tests/cxtrunc.cx hold those
counts so they only go down (sh ./unisacc.com -run tests/cxinclude.cx -- --check;
on 2026-10-30 it still fails, with 29 files above their baseline).
Python still left on live paths is listed in
plans/python-residue-2026-10-30.md.
iterate/ holds development tools written in C (a weight constructor and a
kernel-data generator); they are not part of the product.
The current entry is sh ./unisacc.com tests/run.cx -- --smoke|--all|--list
(registry tests/batches.tsv, coverage list tests/coverage.tsv); --smoke
is local only, and --all fails while coverage is incomplete. The rest of
this section describes the old suites, now archived under
archive/testing-c43-legacy/tests/ (all.sh was their entry), and is kept
as history: in the old tests/all.sh, each suite's verdict was its exit status, and
each got a watchdog so no suite could hang the run. Testing happens on this
desk, not on GitHub: macOS natively, Linux in a local Lima VM
(tests/linux.sh), Windows 11 in a local UTM machine (tests/crossnative.sh).
CI is a second opinion, not the test loop: the local suites run first, and
.github/workflows/ci.yml re-checks them on a machine nobody has been
editing. Training is not in it:
the weights we ship are constructed, and unisa acc verifies those by enumeration
in a twentieth of a second.
| suite | what it checks |
|---|---|
vm |
the tape interpreter, on hand-written fixtures |
difftest |
unisa vs the system compiler — the only instrument that can see a defect in the reference tables |
native |
emitted images actually executed on this host |
crossnative |
the other targets executed on real machines — local Linux VMs and the Windows 11 machine in UTM; the blind spot where three codegen bugs lived |
linux |
the whole suite again, inside the local Lima VM, because glibc and BSD libc disagree about things C only calls unspecified |
fat |
one file, both macOS architectures, both slices actually executed |
ccrun |
unisacc compiles C and the reference VM runs it |
stages |
every decision net the Python front end asks, the C front end asks too — the only suite that can see a hand-written rule that happens to agree with the table |
selfgap |
how much C unisacc's own front end still refuses that the Python one accepts — a ratchet, because that gap was growing unmeasured |
multi |
two translation units compiled into one program, against cc a.c b.c. Both ORDERS are run (m1 m2 and m2 m1), because a file-scope static used to be registered when its definition was reached, so a header whose first function calls one defined below it spelled the call without its unit suffix and only the order that reached the definition first worked. tests/multi/fwd1.c and fwd2.c are that case on its own, and static1.c/static2.c are two units with same-named statics that must NOT merge |
bootstrap |
B = C = U, the self-hosting fixed point |
fb12-multi |
the fixture DIRECTORY suite, tests/fb12/*/ through tests/fb12multi.sh, one case per real-world defect (23 miniz round trip, 27 signature pool, 28 multi-unit static declarator, 30 label naming, 34 arm64 fallback, 35/36 __has_feature, 22 multiple -I, 24 nested quoted include). It runs in the gate's --com block only: each fixture needs an executable compiler, and the fixtures' expected values come from the host cc |
closure |
the image unisacc -b writes is byte-identical to the Python back end's, for every probe on all six targets (this is a check of the C back end against the seed, not a proof of C semantics) |
nativeboot |
unisacc builds itself and the result rebuilds itself to the same bytes — no Python anywhere |
ablate |
each net's answer is rotated to a wrong one: an image must change, or the compile must be refused — asking a net is not the same as obeying it |
run |
unisacc -run compiles a file and runs it in memory, against the system cc |
cli |
the compiler as a tool, from a scratch directory: built-in headers, -I, -D, shebang, exit status |
ccparity |
unisacc next to cc -std=c99 as a user sees it: a.out, -o, exit status, stderr, files left after a failed compile, -E/-D/-U/-I, -Werror, and the documented -c difference |
layout |
the data layout enumerated, not sampled: every tape of up to three data definitions, each also with one symbol defined twice — 762 tapes, 4,572 image comparisons across all six targets, in twelve seconds |
datashape |
generated programs whose globals are declared in one order and allocated in another; the suite asserts the shape is present before it asserts the bytes match |
bigclosure |
the closure on the compiler itself — the largest input there is, and the only one ever big enough to break the back end while every probe stayed green |
corpus |
c-testsuite — 220 programs written by other people, for other compilers |
realprog |
real PROGRAMS by other people, pinned (kilo, jsmn, cJSON, Lua 5.4.7, SQLite 3.49.1), built by unisacc and by cc from the same sources and run the same way; bytes and exit status must agree; tests/realprog.baseline.list is the ratchet (2 of 6 passed in 0.0.16, 3 of 6 in 0.0.17 with unistd.h, and the current baseline names five: kilo, jsmn-simple, jsmn-dump, cjson and lua; SQLite still needs pwd.h) |
elfobj |
-c -b lnx/ARCH writes a relocatable ELF: sections, allowed relocation types, .text/.data equal to the image outside relocated fields, ld.lld links both architectures, GNU ld links and the arm64 programs run in the Lima VM and print what the image prints |
subtract-safety |
nothing outside archive/ imports a module or names a path whose only copy is archived; AGENTS.md/CLAUDE.md resolve to a non-empty file without a link cycle (both 0.0.15 incidents are rebuilt by --selftest and must be caught) |
tools |
real library code by other people — crypto-algorithms, tiny-AES-c, tiny-regex-c; eleven entries, several files each, with their own known-answer tests |
src/unisacc.cx compiler entry; six layers beside it (host, packmodel, executor, stages, drive, bind)
cx/ CX modules: L0 text/mem/sys, then core, runtime, driver, compiler
exec/ production model declarations, fact tables (exec/facts) and constructors
kernel/ generated classic fact data + integer inference kernel, as C (tools/kernel-emit.cx)
include/ the bundled C99 headers and the .cx prelude (cx-nested.h, generated cx-flat.h)
weights/ the constructed weights (built.uns2) and the tables as data (gold/*.tsv)
tools/ CX build and generator tools (entry-build, facts-gen, kernel-emit, gold-table, ...)
iterate/ development tools in C (weight constructor, kernel-data generator); not the product
tests/ the new CX contract suite: tests/run.cx, tests/batches.tsv, tests/coverage.tsv
archive/ archived: reference-py/ (Python seed), reference-c/ (classic C), testing-c43-legacy/ (old suites)
prd.md current product state, architecture, roadmap and history index
Current design and status are in prd §0.3; older measured findings and failed predictions remain in the archive.
The results below describe earlier classic/reference measurements, retained
for the defects they exposed. They are not the current candidate's acceptance
snapshot; tests/corpus.baseline now requires 216 passes. Current candidate
receipts are linked above.
tests/corpus.sh runs c-testsuite, 220 single-file C programs this project had
no hand in writing:
corpus 220 pass 214 wrong 0 unsupported 0 knownfail 6 slow 0
The programs are compiled to a real image for this host and executed, not
interpreted: the reference VM is a Python loop, and an eight-queens search a
real CPU finishes instantly takes a quarter of an hour there. wrong is the
only failure — a program that compiled and then disagreed.
unsupported is the honest coverage gap — the front end refuses the
program — and was zero in this snapshot, so the next one to appear was an alarm
rather than another entry in an old backlog. knownfail is the list we are
deliberately not chasing: floating point, GCC extensions, _Generic, and one
place where this subset evaluates int arithmetic at 64 bits on purpose.
tests/corpus.baseline is a ratchet, so pass may only go up.
But a corpus of single files written for compilers is not the same as
library code written to be used. tests/tools.sh runs the other kind —
crypto-algorithms, tiny-AES-c and tiny-regex-c: eleven entries, every one of
them several files, with their own known-answer tests, none of it floating
point:
tools 11 pass 11 wrong 0 unsupported 0 skip 0
It read pass 2 the first time. The ten defects behind that are the most
useful thing this project has measured in a while — seven of them produced a
wrong answer rather than a crash, and two were invisible to all six --fold
targets because the interpreter does not model addressing modes:
- comments were being removed by the lexer, not before the directives, so
#define N 32 // notecommented out the rest of every line that used N; - macro parameters were substituted one at a time instead of simultaneously,
so MD5's
FF(d,a,b,c,…)rounds wrote the wrong variable; - arm64's
[fp, #-off]immediate is a signed 9-bit field and we were masking it, so every function with more than 256 bytes of frame read the wrong local; uint8_twas a typedef for signedchar, so AES printed a screenful offs;- parentheses destroyed an lvalue, so
(*p)++was refused outright; - and a struct passed by value was copied through the registers the later arguments were still sitting in.
prd.md §6 E-45 and E-46 retain the full list. The lesson is not that real code
is harder — it is that real code leans on the type system, the ABI, the
encoder and the library at the same time, and a probe we write ourselves
leans on one of them at a time.
research/prior-art.md is a survey of the neighbouring literature, and it is
deliberately unflattering. The short version: constructing weights rather than
training them is 1996 (Omlin & Giles) and 2023 (Tracr); replacing a lookup
table with a small network is ACAS Xu, 2016 — which is also where the
counter-example lives (Bak & Tran 2022 showed the compression unsafe, and
Boniol et al. 2026 then compressed the same tables exactly with BDDs). Not
new here: the existence theorem, integer/power-of-two weights, exhaustive
verification, or size. What we have not found a precedent for is the
combination — every table-shaped decision point of a real self-hosting C99
compiler behind one kernel, with no implicit fallback on model rejection, verified by
enumeration over the whole domain.
开箱:浏览器 / Node / Bun wasm_run;Python 仅在 ujs/construct/ 做出货。
import { bootRuntime, wasm_run } from "./ujs/core/wasm_run.js";
await bootRuntime(new URL("./ujs/core/ujs_full.wasm", import.meta.url));
console.log((await wasm_run("return 1+2;", {}, {})).ok);| 规格 | ujs/prd.md(思维树 · 记忆宫殿) |
| 开箱 | ujs/README.md · Host ujs/uxe/HOST_ABI.md |
| 论文 B | research/ujs-paper-outline.md · research/README.md |
| 演示 | cd ujs && npm run demo |
| 验收 | ./tests/ujs.sh · cd ujs && npm run test:uxe:all |
| 本地产物 | python3 -m ujs web-build |
| 发版附件 | ./ujs/scripts/release-artifacts.sh → dist/(上传 GitHub Release) |
与 C 线共享构造代数;不把 Web 并入 unisa 交付范围。
Current version, Latest, verification state and the active plan are maintained only in PRD: current status and plans. The seven-stream / eighteen-network contracts, seal inventory and verifiable progress are in PRD: pipeline design.
The decision-network table above describes classic finite fact decisions; the seven-stream byte-pipeline contract is in the same section. Current whole-stream networks and their limits are documented in the pipeline design. Exhaustive network/table equality is distinct from C semantics, stream-stage compatibility, platform execution and package/container self-construction. Classical no-Python self-hosting and fixed-package model-driver self-hosting retain their separate evidence scopes.
Clause coverage from archive/testing-c43-legacy/tests/c99/clauses.tsv, one row per normative subclause of ISO/IEC 9899:1999 + TC1-TC3 (WG14 N1256); headings and informative clauses are excluded:
| Part | Subclauses | Covered | Partial | Unsupported | Covered |
|---|---|---|---|---|---|
| Language (clause 6) | 98 | 95 | 1 | 2 | 96% |
| Environment (clause 5) | 16 | 13 | 2 | 1 | 81% |
| Library (clause 7) | 349 | 242 | 40 | 67 | 69% |
| Annexes F, G | 19 | 0 | 0 | 19 | 0% |
This table is frozen text. Its check, tests/c99ledger.py (gate c99-ledger), verified that
every listed probe existed and ran in a gate suite, that no language clause was left unmapped, and
that this table matched the ledger. The check, the ledger and the old gate are archived under
archive/testing-c43-legacy/tests/ (c99ledger.py, c99/clauses.tsv); nothing runs them today.
The current coverage list is tests/coverage.tsv.
What this compiler does not do, so a reader does not have to find out by
experiment. Every line was measured against the shipped unisacc.com, and the
reproduction is one short program. The table exists because an external trial
of 0.0.12 reported time lost on exactly these, documented nowhere.
| Limitation | What happens | Reproduce |
|---|---|---|
__LINE__ before a line continuation (C99 6.10.8) |
A token that sits BEFORE a backslash-newline on the same line reads one line too high (int m = __LINE__; int \\ then n = __LINE__; gives 3 3 where gcc gives 2 3); diagnostics at such a token are off by one the same way. Tokens after the join, and every later line, are right |
the two-line example |
| No complex types (C99 6.2.5, 6.3.1.6-7, 7.3, Annex G) | double _Complex z; is rejected; the 0.0.20 reference names it (not covered: complex types, with an UNCOVERED record), released compilers say expected ';' or unknown identifier; <complex.h> is not provided |
double _Complex z = 1.0; |
| No trigraph replacement (C99 5.2.1.1) | "a??=b" stays a??=b; C99 requires ??= to become # in translation phase 1 (gcc/clang also skip it unless -trigraphs) |
printf("%s", "??=") |
__LINE__ / __FILE__ |
Both work on the reference. __LINE__ is the PHYSICAL line of the token -- spliced headers and continuations notwithstanding -- and inside a macro body it is the line of the INVOCATION, which is what gcc and clang report; tests/c/n17-line.c is the six-value probe (9 1010 11 12 15 16, identical to cc). __FILE__ is the path as given on the command line, and reaches the front end through the \0cli/source resource when the driver passes no SRCPATH. This row is kept, struck through, rather than deleted because it was the external trial's most-reported omission |
printf("%d %s\n", __LINE__, __FILE__) |
long double is double |
sizeof(long double) == 8; no extended precision |
printf("%d", (int)sizeof(long double)) |
| Private calling convention between generated code | Not SysV / AAPCS64: arguments are pushed on the tape stack left to right, r9 is the frame pointer, the result is in rax, the caller pops. Unstable -- it follows the regmap and abi tables and external code must not rely on it. Interop with outside code goes through libunisacc's carriers, not through this convention. prd W-16 |
examples/apps/xgui.c writes a raw syscall stub by hand and must save r9, rcx and r11, because r9 doubles as syscall argument 6 and syscall clobbers rcx/r11 |
| Windows product host calls remain a named boundary | include/unisacc_ffi.h has macOS, Linux and Windows loader branches. The reference Windows back end has conditional imports, but the current product route rejects .hostaddr/.hostcall by name until its Win64 bridge matches the reference; it must not emit a wrong image |
a Windows program using __hostcall |
| Missing C library surface | No regex.h or strings.h; setjmp.h is present with compiler intrinsics for setjmp/longjmp. In 0.0.20 (development): struct timespec, clock_gettime (on macOS both clocks read gettimeofday, so CLOCK_MONOTONIC is the wall clock at microsecond grain), nanosleep, sleep, usleep, execl/execlp/execle. Since 0.0.19 on Linux and macOS: sockets (sys/socket.h, netinet/in.h, arpa/inet.h IPv4, netdb.h without DNS, sys/select.h), processes, sys/wait.h, the full errno set and the time functions. Since 0.0.18 sys/stat.h, fcntl.h, poll.h, termios.h, sys/ioctl.h, isatty and dirent.h on macOS exist on Linux and macOS (kernel calls, no host libc); Windows refuses them by name. A minimal unistd.h (fd calls) arrived in 0.0.17; locale.h and sys/types.h in 0.0.13. |
#include <regex.h> |
| Identifiers: 63 significant characters | C99 6.4.2.1 requires that many, and both front ends keep them. The C reference refuses a 64-character name with error: identifier too long (max 63 characters) at its position, rather than truncating it -- truncation merged two distinct names that shared a prefix, and miniz's tinfl_/tdefl_ families are exactly that shape. The product accepts arbitrary lengths (E3 interns names by source position), so a program the reference rejects still compiles on the product; the asymmetry is deliberate, not a bug in either |
a 64-character identifier: the reference exits 1 with that message at the name, the product compiles it |
| Struct member count: 256 per struct | A limit separate from the table capacities below: one struct may hold at most 256 members. Both front ends refuse a 257th, worded differently -- the reference prints too many members with no position, the product prints error: not covered: structure member capacity at the member |
a struct with 257 members, then s.m0 = 1 |
| Shared table capacities are not deduplicated | The typedef, struct-tag and member tables are shared across translation units and not deduplicated: every unit re-registers the headers it includes, so N units of the same headers consume N times the entries. Counted rather than guessed -- one unit of the miniz headers contributes ~91 typedefs, and three units crossed the old limit of 256. The limits are now typedefs 1024, struct tags 512, members 4096 | compile the same header from three translation units and watch the typedef count climb |
| Headers and functions that are declared but absent | regex.h, strings.h, utime.h, pwd.h; system, strdup, sigaction. (unistd.h, fcntl.h, sys/stat.h, getline and time() exist since 0.0.18-0.0.19; clock_gettime, nanosleep, sleep/usleep and execl/execlp/execle and sys/un.h are in 0.0.20.) A program that needs one gets a missing-header error, or an implicit declaration and then undefined function. The 0.0.13 additions are size_t-style types, strerror, the stdio set (feof/ferror/ungetc/clearerr/setvbuf/tmpfile/vprintf) and errno codes. The rest is the FX-5 layering decision: L2 (declare and import the host's) is 0.0.14 |
#include <pwd.h> or a call to strdup |
| The private calling convention is not the host's | Code this compiler generates calls code it generated: arguments are pushed on the tape stack, r9 is the frame pointer and the result is in rax (prd W-16). It is explicitly unstable and external code must not depend on it; interop goes through the libunisacc carrier or the plan's bridge routes. This is why #21 above has nowhere to put the callee's address |
any two units, or nativeboot |
| Unit objects from before 0.0.20 | The 0.0.20 linker refuses a -funit object without the .unit 2 record (link: object predates .unit 2): older objects cannot say which global had an initialiser, so two initialised definitions of one name would be silently merged. It also refuses two initialisers for one object across units and an object only ever declared extern (link: undefined reference to NAME) |
recompile the objects with the new compiler |
__FILE__ in a SPLICED header; #line in a header |
The path reported is always the MAIN file's, and inside #included text __LINE__ is the line of the line as the user sees it in the header -- both matching gcc. #line N ["file"] (C99 6.10.4) is supported in the main file as of 0.0.14; the operand may also be one object-like macro whose body is a digit sequence (c-testsuite 00152); inside an included header it is refused with its position (not covered: this form of #line), as are other macro operands and escapes in the file name |
#line 7 inside a header |
Two things that look like limitations and are not. -ftrim-libc is on by
default and keeps library bodies only when a program reaches them by identifier
closure, so a symbol reached solely through a bundled header's macro expansion
can be reported undefined (R13-0 #03). And the
model compiler refuses to compile rather than falling back to the classic path,
by design.