Newest first. Add a dated entry for every substantial batch of work.
Entries describe their dated checkpoints; the temporary separate divergence
interface in the first two proof milestones was superseded by the combined
TuringComplete interface below.
Proved exact terminal serialization and byte decoding, then assembled
javaGenComplete from URM answer preservation and all-fuel divergence.
The witness and source-realization theorem use only Lean's standard axioms.
Registered derivedJavaGen with the Turpentine CLI and added source-round-trip,
answer, divergence and decoder regressions. A reflexive csimp proof lets
the compiler share its declaration index across closure rows.
Updated the spec, proof account and implementation comments. README language
matrices now use consistent notation and alphabetical order. Checked local
links and exact declaration anchors; replaced unavailable source pages with
verified archives. Validation: lake build, 1,856 golden tests, five property
groups, 34 javac cases, 315 site checks, and link/axiom audits all pass.
Proved urmSource_realized: every URM program and input has ordinary JavaGen
text that the existing loader recovers as exactly the total generated
artifact, including its inheritance closure and paths. The
source renderer
separates tokens with spaces; the existing compact renderer is unchanged.
The lexer proof covers legal identifiers, keywords and punctuation at its actual source-length guard. The parser proof covers arbitrary token coordinates, nested unary types, ordered superclass/declaration lists and the final end-of-input check. Token count plus one is proved sufficient for all parser recursion. Composition with the existing validation theorem establishes equality of complete prepared artifacts.
The ordinary runner evaluates the emitted successor example with input 2
to 3. The specification and universal compiler account document the exact
renderer and example. Byte-level answer decoding is now the remaining proof
gate before assembling javaGenComplete; no completeness witness is claimed.
Validation: lake build, all 1,852 golden tests, all five property groups and
34 real-javac conformance cases pass. The site passes 315 structural checks,
and all local documentation links and numbered source targets resolve. Both
the syntax round trip and full loader theorem use only standard Lean axioms.
Proved that the executable URM compiler always returns its generated artifact, including on divergent inputs. The totality proof covers shifted register allocation, legal and distinct generated names, declared constructor arguments, superclass parity, acyclic inheritance and all finite symbolic lookup obligations. The ordinary indexed validator's walk succeeds at its own class-count budget; ancestor heads are distinct, so its diamond filter retains every row and path. The proof applies to the real validator loops by definitional equality.
urmPrepared_halting, urmPrepared_divergence and
urmPrepared_answer_record now apply to the total artifact without assuming
successful compilation. Divergence still means .outOfFuel at every finite
budget, independently of decoding. The answer theorem still concerns the
actual third most recent proof frame.
The full TuringComplete witness remains pending: generated-source
realization and the byte-level answer decoder are the remaining proof gates.
No completeness witness or derived Turpentine backend is registered yet.
Validation: lake build, all 1,852 golden tests and five property groups
pass. All 34 real-javac conformance cases pass with no inconclusive result.
The regenerated site passes 315 structural checks; repository-wide local
links and exact numbered source targets resolve. The totality and unconditional
operational/retained-answer theorems use only Lean's standard logical axioms.
Extended the Turpentine backend to fixed-size integer and Boolean arrays. Each cell occupies a counter register; computed indices select through a finite dispatch chain, and nested reads preserve temporary values. Boolean guards short-circuit, including guarded out-of-bounds reads. An invalid access loops in the target; the source interpreter reports a runtime error. The shared FRACTRAN frontend retains its scalar-only default.
Added prefix sums, histogram, Boolean marks and Fibonacci-table examples,
with full source in the specification
and commands in both JavaGen documents. Their answers, 10, 2, 3 and
8, agree with the Turpentine interpreter. Array tests additionally cover
initialization, adjacent arrays, aliasing, bounds traps and the existing
maximum and sieve examples. Corrected the AST comment to match the reference
evaluator's RHS-before-index order for indexed writes.
Profiling the large sieve exposed repeated linear declaration searches in JavaGen validation. Indexed names, declarations and superclass heads now avoid those searches; reverse accumulation also removes repeated copying of expanding ancestor lists. Closure order and first-path selection for equal diamonds are preserved.
Validation: all 1,852 golden tests and five property groups pass, including
40 array/source/bounds checks. All 34 real-javac conformance cases and 315
site checks pass; the four new examples produce 10, 2, 3 and 8, and
the existing maximum and sieve produce 9 and 15. All tracked Lean modules
build, including the validator's kernel-checked source-realization examples.
Proved the unbounded register representation and every complete instruction sweep in TapeProof.lean. Increment, saturating decrement and both test branches preserve all register blocks; a return pass restores tape orientation. Every flow instruction has strictly positive subtype cost, including self-jumps. Arbitrary located structured-counter executions now simulate in the ordinary target evaluator.
URMProof.lean composes the
existing URM forward theorem through the tape invariant to the represented
answer and normal halting. Divergence.lean
connects the generated prologue and continuing dispatcher invariant to
all-fuel exhaustion for divergent URM inputs. compileURM_halting and
compileURM_divergence concern the actual successfully returned artifact.
The latter is independent of output decoding and excludes runtime errors.
ObservationProof.lean
locates the exact final answer frame in the actual successful execution and
proves its Letter_1 count equals the arbitrary source answer. Ground
inheritance erases the live query but preserves that frame. Generated-name
validity and injectivity, constructor padding, reversal and unbounded unit
counting are proved separately; textual serialization/decoding remains open.
No TuringComplete witness yet: uniform compilation, generated-source
realization and correctness of the textual result decoder remain open.
Validation: the isolated checkpoint passes lake build, all 1,813 tests
and five property groups. Real Java conformance passes all 34 probes; the
site passes 315 checks. Documentation links and declaration anchors resolve.
The new main theorems use only propext, Classical.choice and Quot.sound;
there are no proof placeholders or custom axioms.
Implemented the JavaGen backend for closed nonnegative
scalar computations with an integer answer. It reuses the lightweight
Turpentine-to-Minsky pass, merges straight-line increments, compacts control
states and emits ordinary JavaGen through the shared sweeper generator.
Compilation does not evaluate the source, including on divergent loops.
Added --to javagen / --via javagen and standalone --compiled-answer.
The answer observation uses the ordinary subtype step with indexed closure
lookup, retaining only the final control tape rather than a full history.
It reads register zero at successful completion. Compiled queries are closed;
the numeric answer-hole certification script remains a separate workflow.
No end-to-end compiler certificate or --tc backend is claimed. Source
errors at zero divisors and failed assertions follow the shared counter
pass's existing conventions; tests and documentation record the differences.
Moved the executable sweep generator to the language folder, preserving compatibility exports for its existing simulation proofs. Added differential fixtures against the Turpentine reference interpreter, fragment rejection, nested and infinite loops, assertion behavior and proof-record/answer-mode agreement. Updated the spec, compiler guide, README status and workplan.
Validation: lake build passes. All 1,813 tests and five property groups
pass, including the 65 new compiler/reference/fragment cases. Sum, sum of
squares, factorial, Fibonacci, integer square root and GCD return 10, 30,
120, 55, 4 and 21 respectively. Standalone emitted-source execution passes,
as does Java export and javac acceptance of a generated zero program.
The Java conformance suite reports 34 passes, no failures and no inconclusive
results; generated sweeper fixtures are unchanged. All 315 site checks pass.
The documentation audit checks 1,151 local links, including 156 numbered
source links, with no issues. The runtime import graph has no Mathlib or
cslib dependency. The compiler guide records the measured emitted sizes.
Added an experimental executable URM compiler: the existing structured-counter translation becomes a finite flow graph, then a sweeping transducer over unary register blocks, then ordinary JavaGen source. Loop bodies are emitted once and iterations use control-flow edges; compilation never executes the source. The compiler uses the ordinary validator and a decidable certificate for symbolic lookup, the actual initial query and closed execution mode. The experimental decoder counts output units in the final control query retained by the existing proof-record evaluator.
Proved the two-step symbol replacement, three-step end turn and three-step
halt against the actual subtype evaluator. The generic sweep simulation
preserves finite halting runs and all-fuel divergence under continuing source
invariants; the checked artifact theorems target evalPrepared. A generated
one-state symbol-duplicating sweeper has proved source realization, a checked
lookup certificate and all-fuel divergence despite its changing query/tape.
Proved flatten_length, generated instruction/continuation locations,
structured-counter execution preservation through arbitrary nested loops,
and forward URM answers at the flow graph's actual terminal node. These
establish the upper and lower simulation layers. The middle flow/tape
invariant, URM divergence composition, answer-decoder correctness, uniform
compiler success and uniform source realization remain open. No
javaGenComplete or certified Turpentine backend is claimed. General numeric
candidate queries for independent Java result certification are also pending;
the existing numeric recurrence examples retain that workflow.
Added compiler-generated source fixtures, a drift-checking Lean generator, 13 counter/URM compiler regression cases plus round trips and divergence prefixes, and real-Java acceptance probes for two generated counter programs. Updated the plan, compiler/proof accounts and specification. The current route reuses existing counter arithmetic with the paper's read/write and turning mechanism; a separate ordinary Turing-machine adapter is no longer the first step. Recorded the initial-query/mode certificate obligation in CONTRIBUTING.
Validation: lake build, all 1,748 golden tests and all property groups pass,
including the new compiler regressions. All 34 real-javac conformance cases
pass on Java 11.0.15. Generated fixtures are current; the documented URM
successor example evaluates to Except.ok (some 3). All 315 generated-site
checks pass. The proof audit uses only Lean's standard axioms, with no
sorry or native-evaluation axiom; the documentation/link audit passes.
Implemented the JavaGen specification, total parser,
validated symbolic inheritance closure, pure fuel-based subtype evaluator,
runner and Java exporter. Closed queries retain their successful derivation,
including arguments later erased by ground inheritance. Numeric queries
carry one answer hole: symbolic execution infers a padded unary numeral,
then concrete execution checks the original query specialized with it.
The single-command Python driver additionally checks that same computation
with real javac, compiling declarations separately and treating resource
exhaustion as inconclusive. Unchecked warnings are errors and annotation
processing is disabled.
Added original Fibonacci(10), factorial(5), summation, positive-input addition, zero, inheritance and divergence examples, plus 48 golden tests and source round-trip/stability/payload properties. The arithmetic fixture generator emits finite recurrence declarations without calculating their answers. These are not Turpentine compilations or evidence of a universal compiler. The spec quotes complete examples and documents both the single-command Fibonacci certification and separate Java export/compilation commands.
The computability development now proves
completed-run stability for both modes, LawfulProgLang, injectivity of
unbounded natural encodings, source realization of the loop example, and
its positive-cost divergence at every finite fuel. The universal answer
protocol, tape-machine construction, URM bridge and their answer/divergence
proofs remain open. Keep the public URM contract and first use the paper's
tape-machine construction; SKI is possible but requires a new application
and duplication simulation. TC is the next priority, ahead of a direct
Turpentine backend or performance tuning.
Updated the workplan, roadmap, compiler/proof accounts and testing guide.
The README threshold is now met, so added the language README, status rows
and generated-site catalogue entry. Recorded in CONTRIBUTING.md why result
certification must retain the original query and check declarations first.
Validation: lake build passes; focused JavaGen tests and properties pass;
difftest.sh passes, including 32 JavaGen checks on javac 11.0.15 with
wrong-answer rejection. The Fibonacci certification prints 55; the documented
export and separate javac commands succeed. Missing-JDK, zero-fuel and
nonnumeric-query failure paths pass; generated fixtures are current. All
315 generated-site checks pass. The new proof audit uses only standard Lean
axioms, with no sorry or native-evaluation axiom. All 1,748 full-suite
tests and the property checks pass. The final documentation audit checks
1,118 local links, including 156 numbered links, with no issues.
Created ilya/java-generics from master and read Radu Grigore's
Java Generics Are Turing Complete (POPL 2017), concentrating on the
subtyping machine, Turing-machine reduction and Simper compiler. The
JavaGen design adopts the unary contravariant core
and replaces Simper with Turpentine through the existing certified URM
pass. It specifies proposed syntax, validation, fuel/rejection behavior,
Java export, module boundaries, testing and staged proof obligations.
The first proof gate is answer retention: the paper's successful subtype query recognizes halting, but its ground halting rules erase the tape. The proposed observation retains the successful derivation so a marked answer block can be decoded; that proposal still needs an executable experiment and proof. No JavaGen code, runner, compiler or completeness witness is claimed. Moved JavaGen from the candidate roadmap to active design and recorded the general recognition-versus-answer obligation in the contribution checklist. README integration is deferred until the spec, parser, interpreter, runner and tests are all in place, as requested; this threshold is now recorded in the project documentation policy.
Validation at the requested commit: lake build passed, the documentation
audit found no issues in 1,059 local links, and all 305 generated-site
checks passed. The full lake test suite was still running when the user
requested an immediate commit; no failures had been reported at that point.
Both README status tables now link bespoke compiler results to compiler notes and computational-class results to per-language computability accounts. Added the missing accounts for Whitespace, Ook! and Brainloller, retaining Brainloller's pixel-walk proviso. The main README now explains matrix statuses in bullets and gives shorter accounts of lawful execution and compiler correctness; the separate bespoke-compiler rationale is folded into the compiler overview.
Refreshed MU's computability overview with the checked fixed-cell runtime milestones and current obligations. Recorded the proved returning width growth and adjacent-fill obstruction, and updated the audit's completeness contract to include divergence preservation.
CertifiedCompilerNoIO spec diverges L now describes closed computations:
spec : Src → Nat → Ans → Prop, diverges : Src → Prop, and every target
run uses Input.empty. There is no targetInput parameter. The derived
Turpentine certificates and direct bespoke Subleq/Whitespace certificates
use this interface. The final API also removes targetInput from
TuringComplete: every compiled URM/input pair runs on Input.empty, so
the derived construction needs no input encoding or side condition.
CertifiedCompiler retains arbitrary caller input, its explicit encoding,
and completed-trace and divergence preservation. The input-reading Velato
witness is bespokeVelatoIO; tests now call it directly. correct_answer
forgets traces without losing the input argument. toClosed and toClosedOf
replace the previous automatic toCertified conversions: closing fixes source
input to empty and requires proof that its target encoding is also empty.
The explicitly named bespokeWhitespaceIOClosed and bespokeVelatoIOClosed
make that restriction visible. Closed means a fixed execution interface,
not a syntactic proof that a program contains no reads.
Documentation, contribution policies and declaration links follow the API. Historical progress entries below describe earlier interfaces. Finite input storage and fuel-bounded finite traces are unchanged. Preservation of observations during divergence is deferred to issue #1; the exploratory observation-proof file was removed. Infinite input is a separate extension.
The documentation/comment sweep also corrected stale backend restrictions,
finite-trace descriptions, module paths and declaration links. Investigated a
reported repeated rebuild with the Subleq isqrt.turp command: unchanged runs
performed zero build jobs, all 3,057 local artifact timestamps stayed unchanged,
and lake --no-build exe succeeded. Documented direct executable invocation
for batch compilation to avoid Lake's dependency-graph checks.
Validation: the full library and Turpentine executable build pass. All 784
axiom reports are clean (751 use only the standard logical axioms; 33 use none).
The documentation audit passes 1,053 local links, including 210 numbered links
to 107 distinct declarations. The generated site passed all 305 checks and its
playgrounds passed all 45. All 1,700 golden/compiler tests and both Velato
round-trip checks pass. The branch is ready for the requested commit, push and
fast-forward merge into the repository's default branch, master.
Every language's computability development now lives in its own folder,
with Main.lean as its entry point. Shared URM, counter and divergence
infrastructure lives under Langlib/Computability/Common/. The certified
Turpentine backend proofs moved to Turpentine/Compile/Certified/;
Compiler/ is not used. All imports, proof links and documentation paths
follow the moves, while declaration namespaces and compiler functions stay
unchanged. Every per-language computability account is linked directly from
its language README and indexed from both main READMEs. Named witnesses and
divergence proofs link to their exact declaration lines. The main README's
TuringComplete description is now concise.
Turpentine/Divergence.lean defines divergence using the actual source
interpreter's exit at every finite fuel. It proves completed-run stability
and structural divergence inversions for sequences, conditionals and loops,
including the distinction between a divergent loop body and a terminating
body followed by a divergent next iteration. These facts stay free of
Mathlib and do not infer divergence from the absence of a decoded answer.
They are foundations for the requested mandatory compiler-divergence fields
and upgrades of the existing certified fragments.
Validation is in progress for this layout checkpoint; compiler-contract strengthening remains the next stage.
After committing and pushing the all-eleven proof milestone (052b87e),
folded preserves_divergence into TuringComplete and removed the temporary
extension and duplicate witnesses. All eleven original <lang>Complete
names now include both answer and divergence proofs. The generic
halts_iff, result_iff, halted_run_result, output_valid, and
error_free theorems live in Langlib.Common.TuringComplete.
The proof layout avoids circular imports: <Language>/Simulation.lean
contains the original compiler, forward proof and language instances;
<Language>/Divergence.lean proves continuing execution; the original public
<Language>.lean imports both and assembles the witness. Compiler functions,
public import paths and derived Turpentine APIs are retained. The stronger
URM-to-target contract does not automatically strengthen the separate
Turpentine-to-URM forward specification. Brainloller still uses its existing
decoded-program interface, with the pixel-walk proof a separate obligation.
MU's proof terms remain unchanged; its documentation-path comments follow
the moved computability account.
Updated the plan, contribution/proof policies, shared interface and testing
notes, every affected language README/spec, and per-language computability
accounts. All twelve docs/computability-<language>.md accounts now live
in docs/<language>/computability.md, including the negative and unfinished
claims. Incoming links, relative links within the moved pages and code
comments follow those paths. Every numbered source link in the root docs
and under docs/ points to its exact declaration line. Old statements that SKI and
Unlambda divergence remained open are corrected. The bounded-storage
explanation now uses halting reflection, while keeping effectiveness of the
compiler an explicit meta-theoretic requirement.
Validation: lake build passes all 8,956 jobs, including MU. All 1,700
golden/compiler tests and both Velato round-trip checks pass. All 761 axiom
reports are clean (728 use only the standard logical axioms; 33 use none).
The available external differential suite passes six cases, with unavailable
references/runners skipped. The documentation audit checks 1,099 local links
and 240 numbered links to 116 distinct declarations, with no failures.
The requested continuation is a uniform computability folder layout, followed by divergence-preserving compiler correctness, the Turpentine-to-URM proof, and upgrades of the derived and certified bespoke compilers.
Unlambda now preserves divergence for its unchanged compiler. The decisive
lemma stops an abstracted application just before its call: the guard can
terminate with a branch closure, and the dispatcher body can terminate with
the next encoded state, without assuming the recursive call terminates.
Positive CEK prefixes compose through that call under arbitrary
continuations. An increment-only initialization lemma reaches the loop from
the actual compiled term. unlambdaDivergencePreserving inherits
unlambdaComplete exactly; all eleven stronger witnesses are now proved.
The next stage was to move divergence preservation into TuringComplete and
remove the temporary extension, migrating every client and document. MU’s
Lean sources remain unchanged. Validation of this proof-only milestone:
full build passes (8,945 jobs), and all 770 axiom reports are clean.
The complete test suite will be rerun after the interface refactor.
Added DivergencePreservingTC in Langlib/Common/Computability.lean as a
separate extension of TuringComplete. Its divergence field constrains the
exit itself: every finite target fuel budget on a divergent URM input must
yield .outOfFuel. This rules out both errors and spurious normal halts
whose output the decoder rejects; a decoded-result iff alone would not.
Proved halts_iff, result_iff, output_valid, and error_free, plus
halted_run_result and TuringComplete.simulates_at_completed_run. The
latter compares completed runs at a common larger fuel using lawfulness,
so even the old interface excludes early errors on halting source inputs.
The new field supplies the divergent-input case. The consequences, shared
progress helpers, new witnesses and Unlambda groundwork are in the axiom
audit.
All eleven existing TC witnesses, their compilers and derived Turpentine compilers remain unchanged. Their claim is forward answer preservation; ten separate divergence-preserving witnesses now inherit them exactly. Whitespace and Subleq prove positive block progress, FRACTRAN proves nonempty rule simulation including self-jumps, Thue and Piet continue their dispatchers, Brainfuck and Velato iterate terminating counter bodies, and SKI proves positive head reduction plus strictness of compiled continuations. Ook and Brainloller transport the Brainfuck result, with Brainloller’s pixel-walk obligation still separate.
Unlambda now has positive fragment-job execution, exact zero-output buffer preservation, positive strict-fixed-point unfolding under arbitrary continuations, and unconditional error freedom. Its guard/body path back to the recursive call still needs an operational proof; no stronger witness is claimed for it. Progress and this remaining obligation are tracked in the migration table and Stage 8 of the plan. The documentation and language READMEs now make this distinction, and the contribution policy explicitly rejects a decoded-result iff as a substitute for the execution obligation. MU's proof development is unchanged; it still has no TC witness to upgrade.
Validation: full lake build passes (8,945 jobs). The expanded axiom audit
has 758 reports, all using only propext, Classical.choice, and
Quot.sound, or no axioms. All 1,700 golden/compiler tests and both Velato
round-trip checks pass.
Available external differential tests pass six cases; other sections skip
when their runner or reference interpreter is unavailable.
LowTrit.lean extracts the low bit of an arbitrarily wide zero/one marker
with two crazy operations. The first scratch starts at ...2220; the
second can contain either previous result bit. The first operation collapses
all higher trits, and the second leaves the marker's low bit as natural zero
or one. Running the same operations with an all-ones accumulator restores
the first scratch and leaves one in the second. pair_call executes both
operations and the connecting pointer reset in nine real steps, preserving
code, records, both widths and I/O. test and reset specialize it;
marker_zeroOne establishes the extraction precondition throughout a
rotation, and test_marker connects an actual call's result to divisibility
of the rotation count by the state's current width.
BitBranch.lean proves a conditional jump through a natural bit in two
steps for one and three for zero. The zero path executes the no-op at
address 1 before the stable jump at 2; the one path lands on 1 and proceeds
directly to 2. The extra data-pointer increment selects the other adjacent
continuation word. Both paths encrypt address 1 exactly once within its
74/70 no-op orbit, preserve both continuation landings, and leave the
accumulator, widths and I/O unchanged. Its frame preserves caller code,
flags and records placed outside addresses 0, 1 and the two target landings.
bit-branch.mu, the seventh generated runtime source, initializes that
no-op and tries flags 0,1,1,0. Each outcome runs in both no-op phases;
the four continuations are reached at instructions 10, 13, 16 and 20,
followed by a halt at 21. Fifteen new tests cover initialization, flags,
records, both phases, untouched nonempty input, strict loading, and the
halt boundary at starting widths 10 and 37. The complete source is given
in sparse transliteration in the spec.
The nine-step extractor's result is adjacent to fixed restoration/return
words, rather than independent branch continuations. PaddedCrazy.lean
resolves that record-space conflict: its seven-step working call visits
two no-ops twice, restores every code word, and leaves two free slots
immediately after the result. Two adjacent operand records execute in
fourteen steps without a pointer reset. LowTrit.test_padded proves the
marker test through this layout, preserving both branch continuations,
return landings, code, widths, I/O and the remaining memory frame.
This does not yet close a marker scan. The padded test returns with its
data pointer beyond the result and records; the caller must reposition it
for dispatch, restore that move's code on both paths, reload the marker and
all-ones constant, and connect scratch reset to the next iteration.
CONTRIBUTING.md records the need to check record compatibility. The plan,
audit and runtime account distinguish these proved primitives from a
terminating scan and the remaining completeness simulation. The new source
tests dispatch on prepared flags; it does not claim to initialize or connect
the extractor.
Validation: full lake build, all 1700 lake test cases and both Velato
round-trip checks pass. All 672 axiom-audit reports use only standard
logical axioms. The generator's --check passes; the new source's sparse
transliteration matches byte for byte. Both documented 21-step runs halt
successfully with no output. The padded-call additions are proof-only and
were built and audited after the unchanged executable regression run.
GrowingMarker.lean connects rotation, width growth and marker reset in
87 actual MU instructions. A 15-step entry route and an eleven-step return
route connect the existing services. cycle doubles the width, regenerates
one in the same marker cell and restores the complete resident invariant.
repeat_cycles reaches width 2^n*w; unbounded_width gives actual runs
above every fixed bound, and neverHalts covers every fuel prefix. Input,
output, constants, marker-adjacent records and future distant return reads
are preserved. This is unconditional growth; scan exit, carry/borrow,
conditional overflow retry and the completeness simulation remain open.
MarkerCycle now supports either its original reset continuation or the
growth continuation, with a precise memory footprint for each. Its original
public interfaces remain available. Routing.lean extracts the control-step
lemmas shared by both cycles. The new routes use eighteen additional no-op
cells; kernel-checked encryption orbits describe every reachable phase.
The sixth generated source, grow-loop.mu, initializes all 24 runtime
no-ops and the reset constants in a finite startup. It reaches the first
rotation entry at instruction 1331 and returns every 87 steps thereafter.
The default setup reaches width 18, then grows to 36, 72 and 144; starting
at 37 reaches 74, 148 and 296. Seventeen new tests inspect both routes,
restored marker and code, constant values, records, and untouched nonempty
input. The source has 12006 cells; returns_of_fill checks the new seed
phase and proves that even the smallest permitted distant read lies beyond
the source. CONTRIBUTING.md records this source-size check for future
initializer changes. Finite synthesis identities are proved; the complete
source-to-resident-invariant theorem remains open and is tested by execution.
The runtime account, proof tracker, audit, plan, spec and README now track this milestone and its remaining obligations. The spec includes a complete sparse transliteration of the new source.
Validation: full lake build, all 1685 lake test cases and both Velato
round-trip checks pass. All 654 axiom-audit reports use only standard
logical axioms. The runtime generator's --check passes, and the spec's
new transliteration matches the generated source byte for byte. Both
documented 1592-step runs produce the expected fuel diagnostic with exit
status 2 and no program output.
MarkerCycle.lean closes the routing gap between rotation and marker reset.
A nine-instruction route rotates the marker at 3200 using code at 529,
restores the rotor and router, and enters the existing reset. A seven-step
return route closes the cycle. cycle proves the resulting 50 actual MU
steps restore Ready w; repeat_cycles proves arbitrary repetition;
neverHalts covers all fuel prefixes, including those inside a cycle.
Both widths and all input/output state are preserved. The marker's adjacent
records remain 3201:270 and 3202:529 throughout; no fresh marker,
constant or record is consumed.
Address 529 is both a rotation instruction and a reset landing. Preserving
its printability was insufficient to compose the old reset theorem with
this route. MarkerReset.Traced now records the exact number of encryptions
there, and call_traced proves there are two. call_rotator specializes
this to preservation of word 74. The old call and call_power contracts
remain available. CONTRIBUTING.md records the need to track exact phases
when a landing also serves as executable code.
The return route uses three no-ops at 526–528, alternating independently
between 74 and 70. Their closure, decoding, and rejection as direct source
instructions are kernel-checked. initializer_values checks the natural
operand pairs that synthesize them. The original marker-cycle.mu, generated
by scripts/gen-mu-runtime.py, contains 4202 source cells. Initialization
and a bootstrap reset reach the rotation entry after 76 instructions;
subsequent visits occur every 50 steps. The default setup reaches width 16,
and the same source works at 37. Fifteen new tests inspect both routes,
changed no-op phases, restored working words, constants and records through
nine cycles, with nonempty input left unconsumed.
This is a repeating rotation/reset routine, not a terminating scan or an
overflow loop: it has no exit branch and does not enter the growth service.
The symbolic theorem starts from Ready; complete loader/prologue
reachability is still tested by execution. The plan, runtime account,
proof tracker, spec and README now distinguish the completed shared-record
routing from integration with growth, scan exit, counter arithmetic,
source initialization and the remaining completeness theorem.
Validation: full lake build, all 1668 lake test cases and both Velato
round-trip checks pass. All 636 axiom-audit reports use only standard
logical axioms. The runtime generator's --check passes, the spec's
complete sparse transliteration matches the generated source byte for byte,
and both documented 526-step runs return the expected fuel diagnostic and
exit status 2 with no program output.
Marker.lean proves a constant-preserving path that clears any natural
containing only zero and one trits and rebuilds one. The initial accumulator
is loaded by rotating an all-ones constant, independently of width. No input
instruction or EOF assumption is needed. zeroOne_power admits a marker
3^k for any k, with no working-width bound.
MarkerReset.lean implements that path in 34 actual MU instructions:
six working calls, four pointer resets and two visits to a move/no-op router.
call and call_power restore the marker to one, preserve the resident
constants and return records, and restore the router phase. The frame also
preserves all memory outside the marker, router and printable continuation
landings. Input, output, output-closed state and both widths are unchanged;
the fixed pointer destinations require only maxWidth ≥ 8. These are
execution theorems through run?, not hypothetical marker updates.
The new original marker-reset.mu, generated by scripts/gen-mu-runtime.py,
has 4202 source cells. It bootstraps its constants, rotates the same physical
marker, resets it and halts without output in 103 instructions. At the
default width the setup reaches 16 and the marker becomes 3^15; at width
37 it becomes 3^36. The bootstrap enters with the future mask still zero,
so its first pass through the reset is covered by execution tests, not by
call. The resident reset entry is reached at instruction 61 and returns
at 95. Eleven new tests check both widths, intermediate marker and constant
values, restored router, unconsumed nonempty input, halt, and strict rejection.
The rotation wrapper is single-use. An unbounded caller still needs to
compose rotation, reset, and growth with compatible return records:
work_call binds the operand's adjacent record to the operation's code
address. Restored rotation and crazy-write records cannot simply be treated
as interchangeable. CONTRIBUTING.md records this requirement. The runtime
account, proof tracker and plan now distinguish the proved reset from this
integration, runtime scan exit, carry/borrow, source initialization, and the
remaining simulation theorem. There is still no MU completeness witness.
Validation: full lake build and all 1653 lake test cases pass, as do both
Velato round-trip checks. The axiom audit has 626 reports (606 with explicit
dependencies), all using only standard logical axioms. The generator's
--check passes; the spec's complete sparse transliteration matches the
source byte for byte; both documented 103-step runner commands halt with
exit 0 and no output.
ReusableGrowth.lean proves an eleven-instruction growth call that restores
both working moves, returns through an unconsumed table, and preserves the
code's reusable no-op orbit. call_resident preserves a complete Resident
invariant: working code, return records, printable continuation landing,
and the distant return read for every future width, not only the current
one. The source operand and all other data and I/O are preserved. This
closes the previous growth-code restoration obligation; supplying a new
rotated one for overflow retry is still the caller's job.
The three middle cells use the closed encryption orbit 41, 102, 96, 60, 51.
All phases decode to no-ops at 437–439, but none is a legal source word there.
Both facts are kernel-checked. Initialization.lean supplies
initialize_cell, a three-step crazy/move/crazy write with explicit scratch,
target and code separation and width stability. Three checked value
identities instantiate the synthesis used by the example's initializer.
The complete loader/prologue composition remains a regression test, not a
symbolic initialization theorem.
grow_return_of_read factors the five-step growth segment around an
extensional distant-read hypothesis; the existing grow_return theorem
keeps its old statement as the fill-backed specialization. This lets
Returns.frame preserve reads across repeated calls. Returns.of_fill
establishes them from the actual natural-seeded fill and a finite source
prefix. seed_return checks the concrete example's fill. The loader phases
from the penultimate source address (7000), not the source length (7002).
The new original grow-twice.mu, generated by scripts/gen-mu-runtime.py,
contains 7002 source cells. It initializes the three no-ops, then calls the
same growth code twice using separate one-markers. Its default-width run
establishes width 18 and grows to 36 and 72; at starting width 37 it grows
to 74 and 148. Both runs halt after 63 instructions without output. Ten
new cases inspect initialization, both returns, code phases, marker values,
return records, the exact halt boundary, and strict-loader rejection.
Distinct prepared markers do not demonstrate unbounded marker reuse.
The runtime account, proof tracker and plan now distinguish the completed
resident service from remaining marker reset, runtime scan exit, carry/borrow,
overflow retry and general source realization. The README and spec include
usage and a complete sparse transliteration, verified against the generated
source. CONTRIBUTING.md records the fill-phase and runtime-no-op/source-word
distinctions so subsequent work does not repeat those mistakes.
Validation: full lake build passes. The expanded axiom audit passes with
only standard logical axioms in all 600 dependency reports, including all
11 new public results. The generator's --check passes, and both documented
63-instruction runner commands halt with exit 0 and no output. lake test
passes all 1642 cases and both Velato round-trip checks.
The reworked construction now has five proof modules, described in
the runtime account. The interpreter
and existing URM-to-Counter compiler remain unchanged. There is still no
TuringComplete MalbolgeUnshackledLang witness.
Counters.leanrepresents finitely many unbounded natural counters in fixed cells. Constructive initialization preserves any original fill and all cells outside the finite register file. Update and capacity lemmas state the arithmetic boundary conditions without pretending to execute MU.Runtime.leanproves actual three-step rotate/crazy and pointer-reset calls: restore the working code, return through preserved tables, expose operand changes, and preserve the rest of memory and I/O. Ordinary pointer resets explicitly require width stability.Rotation.leanconnects normalized MU values to padded rotation windows. It proves full-cycle restoration and the one-marker low-trit test, including absence of early return.RotationLoop.leangives a concrete six-instruction loop, arbitrary repeated passes of that same finite code, and restoration of the operand after a full window. There is no assumed pass-existence premise. There is also no runtime marker branch: attaching one remains essential.Growth.leanproves five actual instructions that grow the width and return through residue one of untouched fill. It exposes all five code writes; restoration for repeated growth remains open.
Two original 3004-cell source examples, rotation-loop.mu and
grow-once.mu, are generated by scripts/gen-mu-runtime.py. Fourteen new
regression cases use the actual loader and evaluator: inspect pointer and
operand restoration over repeated cycles, grow 16 to 32 and 37 to 74,
check the exact halt boundary, and reject the permissive data under strict
loading. The source generator has a drift check, and the spec gives complete
sparse transliterations. These tests do not replace the still-needed
symbolic source initialization theorem.
The plan and tracker distinguish checked foundations from remaining scan
exit, carry/borrow arithmetic, repeated growth, initialization and simulation.
CONTRIBUTING.md now records that arbitrary proof-indexed iteration is not
an implemented exit test, and that code restoration alone does not restore
operands or a complete calling convention. The base module's remaining
width-bound and escalator overstatements have been corrected.
Validation: full lake build and lake env lean scripts/axioms.lean pass;
all 23 new public foundation results are audited, with no nonstandard axioms.
The generator's --check passes. The documented runner commands were run:
the loop exhausts 195 instructions with exit 2; both growth runs halt after
17 instructions with exit 0 and no output. lake test passes all 1632 cases
and both Velato round-trip checks.
The MU completeness effort is re-scoped. There is still no
TuringComplete MalbolgeUnshackledLang witness, and the previous assessment
that a walk pass was the only substantive missing piece was incorrect.
New kernel-checked results in
Langlib/Computability/MalbolgeUnshackled/Obstructions.lean:
finite_natural_supportbounds the natural keys of any finite memory map.restTable_adjacent_nonzero_leadshows that, with natural fill seeds, at least one of any adjacent pair of natural addresses has a nonzero repeating trit.not_regMem_of_natural_fillcombines these into a contradiction for the oldRegMeminvariant, for any positive stride and nonempty register file, even after finite writes. It assumes the fill equation; the general mutable-loader invariant remains separate work.no_adjacent_two_cycle_crazyrules out instantiating the three-instruction branch as consecutive period-two working cells.flag_branch_mark_reuseshows that reusing the marked branch's consumed operands yields zero instead of its intended target.widthBounded_update_dmakes explicit that the stored-value invariant places no restriction on the data pointer. The old inference from a finite alphabet to bounded storage was unjustified.
The proof audit records the corrected
scope of the existing theorems and a revised fixed-cell counter design.
It draws on Matthias Lutter's MU Brainfuck interpreter, whose source is
copyright-dated 2016, predating the MalbolgeLisp evidence cited before.
The HeLL source, LMFAO 0.1.5, and Johansen's canonical Haskell interpreter
were inspected; source hashes are recorded, and no external code is copied.
The useful runtime mechanisms are a rotation-width scan, carry/borrow
arithmetic, and width growth with a return path. These still need Lean
operational proofs; the audit gives their contracts and composition with
Counter.counterProgram_spec.
The plan, tracker, spec, and earlier construction notes now distinguish
these obligations from proved algebra. CONTRIBUTING.md records the need
for reachable invariants and source realization when a target uses raw
images: a convenient periodic background is not necessarily loadable.
Validation: lake build passes. The global axiom audit passes after fixing
18 pre-existing stale Whitespace references to lemmas moved into
Langlib.Turpentine.Certified.Shared; all audited dependencies are standard
Lean axioms. lake test passes all 1,618 tests and both Velato round-trip
checks. git diff --check is clean.
Langlib/Languages/Turpentine/Compile/Unlambda.lean, the backend
docs/unlambda/compiler.md had been describing as the most interesting one
in the library that did not exist. It exists, it takes the whole of
Turpentine — arrays, readInt, readByte, byte-exact output — and all
twenty conformance programs go through it and agree with the reference
interpreter.
It is the only backend here that does not compile a machine to a machine,
because Unlambda has no machine in it. A state is a nested pair of the
program's variables, a statement is a function from one state to the next,
; is composition, if is a boolean applied to two thunks, while is a
fixed point, an int is a sign and a Scott numeral, and an array is a Scott
list. The compiler builds all that as a lambda term with real binders and
then removes the binders by bracket abstraction, which is the step the
completeness proof already knew was delicate.
What call by value cost. Three things, one known in advance and two found by watching a program not finish.
- The textbook clause
[x] E = `kEfor anEwithoutxis unsound: it evaluatesEwhen the closure is built.abskeeps it for value expressions only, exactly asLanglib/Computability/Unlambda/Main.leandoes, and thesexpansion everywhere else is what makes a thunk a thunk. - Constructors have to be strict. A pair built as
λf. f (x+1) ycaptures the expression, not the value, and recomputes it at every projection; a loop's state is a chain of such pairs, so the cost of reading a variable doubled per iteration. A twenty-iteration loop could not finish in 200 million steps. - Everything that crosses a binder has to be a value. The runtime
library is bound by two dozen
lets, and aletis an applied abstraction. A subterm that is not a value cannot be carried past a binder with onek; it is expanded withs, which doubles it. Two dozen doublings is sixteen million, andcat.turpwould not compile. Writing each library entry asλa b. Z F a brather than`ZFmakes it a lambda, hence a value, hence onekper binder; the compiled body goes in as a thunk and is forced at the end for the same reason.
Where c is unavoidable. ?x and @ answer i or v, and v
swallows whatever the failing branch would have returned, so there is no way
to see a mismatch from the inside. Every test therefore runs under a
captured continuation and the match is what leaves; the code after the
test is the else-branch. cat.turp compiles to 22 342 applications with
exactly two cs in them, and no d anywhere — bracket abstraction over s
already delays everything that has to be delayed, which is the same reason
the completeness proof never needed either.
Bytes, not text. Unlambda is the one target whose compiled file is not
text: .x carries the byte it prints, so a program that prints byte 200
contains byte 200, and a String holding it would be written out as its
two-byte UTF-8 encoding and parse back as something else. The backend emits
a ByteArray, Langlib.Unlambda.parseBytes reads one (new, beside
parse), and Artifact in the runner grew an optional byte payload for it.
cat.turp compiled this way echoes binary input byte for byte.
Costs. hello.turp is 212 bytes and 412 steps; 99bottles.turp is
12.4 kB and 8.8 million steps; power.turp, which doubles to 16384 an
increment at a time because the arithmetic is unary, is 34.7 million. The
certified route on the same six-line sum.turp emits 41 235 167 bytes where
this one emits 3 868. All of it is measured in docs/unlambda/compiler.md.
Also. Langlib/Tests/CompileUnlambda.lean (28 cases, including the ones
where a Turpentine runtime error becomes e and the run simply stops);
unlambda added to the conformance suite, which is now 20 programs times 8
runners; scripts/gen-unl-examples.sh regenerating three compiled examples
under Langlib/Examples/Unlambda/compiled/; docs/unlambda/compiler.md
rewritten from a plan into a description.
Two pieces of the Turing-completeness effort, both in
Langlib/Computability/MalbolgeUnshackled/Main.lean, both axiom-clean. There is
still no TuringComplete witness; the tracker
docs/malbolge-unshackled/completeness-progress.md says what is left.
A branch into two natural addresses, in two crazy operations. The file
had two branches and neither was the right tool for a loop. branch_arith
reaches arbitrary targets in seven operations and consumes four shaping
constants, which a loop must restock every pass. flag_selects_address
costs three instructions but lands d on address 0 or 1, which is where
execution begins. flag_branch pays one crazy operation more than that and
buys those two addresses back: against ...111 and the natural 2 * 3 ^ j
it sends a blank flag to the address 2 * 3 ^ j and a mark to 3 ^ j, for
any j the compiler likes, both naturals, both anywhere in memory. The
first constant is the ...111 the ladder and the register probe already
keep. flagAddr_gadget runs it in three instructions, two crazy cells
and a movd that leaves d on the cell holding the address.
Two is the least possible, and the argument is the same tritwise one that
runs through the rest of this development. A single column of the crazy
table sends the blank flag to 1 or 2 at every trit position, so a
one-operation result repeats 1 or 2 for ever, is not a natural, and a
jump into it lands in the memory fill, which restTable_not_printable says
can hang. Two columns composed give seven of the nine possible pairs,
including (0, 0) above position j, which keeps both targets natural,
and (2, 1) at j, which makes them differ. This does not contradict
no_accumulator_flag: that rules out computing a uniform value from the
accumulator, and neither target is uniform.
One asymmetry a gadget author needs. On the blank path the second constant cell ends holding exactly what it held, so it restores itself; on the mark path both constants are consumed and a pass must restock them.
The walk, which was the blocker. Everything built before this batch
reached statically known addresses; nothing did data-dependent iteration,
and inc, dec and the loop condition all need it. Two of its three parts
now exist. walk_iterate is the induction: n passes of a k-step pass
cost k * n steps, where n is a tape length rather than anything the
compiler knows, and the layout makes a pass free of address arithmetic
because regAddr's slot stride is the pass length. walk_branch_target
is the exit: feeding the cell a walk stands on to flag_branch aims
control at 3 ^ j while marks remain and at 2 * 3 ^ j at the first
blank, so the walk stops exactly at the tape boundary.
What a pass finds ahead of itself. A walk steps into cells no loader
wrote, and RegMem asks every cell above a tape's length to be blank.
Untouched cells hold the memory fill, and the fill is never blank:
searching every pair of printable seeds finds no pair putting ...000
anywhere in the six-value table. That is measured rather than proved, and
leadAt_even with crzTrit_zero_ne_zero is the structural reason. So a
pass has to normalise the cells it is about to use.
fillAt_slot makes that affordable and closes the circularity the first
design runs into. With a slot stride divisible by 6, a given offset holds
the same fill value in every slot, because the fill sees the address only
through its residue mod 6. So the value ahead is a compile-time constant,
crz_two_steps converts it in two operations, and a pass can write the
next slot's constants using its own. The constant block propagates one
slot per pass, the loader writes slot 0, and the walk carries it forward:
the escalator argument applied to data rather than to addresses.
What is left is one pass — the hypothesis walk_iterate takes — with three
concrete jobs in it: re-entry, which two_sweep is for, restocking the
branch's constants, and propagating the block. The blocker is now that
narrow.
A TraceLang for Velato. Langlib/Languages/Velato/Trace.lean proves
the two bookkeeping laws (the trace's output events are the output; its
input events followed by what the cursor has left are the stream) and
Velato/Faithful.lean the faithfulness law. Faithfulness needed one idea
the whitespace and subleq proofs did not: Velato's interpreter runs whole
sub-runs rather than single steps, so the two-stream simulation must also
say that the two runs consume the same bytes, or a statement could not be
followed by the rest of its block. Faithful.seq is that composition, and
instance : TraceLang VelatoLang sits beside ProgLang VelatoLang.
The hand-written Velato backend is proved correct on a fragment,
behaviourally. Langlib/Languages/Turpentine/Compile/Certified/BespokeVelato.lean
gives bespokeVelato : TurpentineCompiler VelatoLang and bespokeVelatoIO : CertifiedCompiler BehavesWithAnswerNulFree VelatoLang with encodeInput
and encodeTrace both the identity: the compiled program runs on the
source's own stream and performs its events, reads included. It is the
first behaviourally verified backend in the library whose fragment reads.
The fragment is scalar int/bool with no initialisers, the expression
language without / and %, if, while, print/println of strings,
integers and booleans, and x := readByte(). The specification restricts
the stream to one with no NUL byte, because Velato's Input stores 0 for
a NUL and at end of stream alike and the backend cannot tell them apart;
that is stated in the specification rather than hidden in encodeTrace.
The proof is about the shipped generator: its partial was removed and its
for loops rewritten as recursion, with no change in behaviour.
A divergence found on the way. printByte is out of the fragment for
a real reason, not a proof gap: Velato prints a char as the UTF-8
encoding of its code point, so printByte(200) writes C3 88 where
Turpentine writes C8. docs/velato/compiler.md records it and a golden
test pins it.
Shared source-side lemmas.
Langlib/Languages/Turpentine/Compile/Certified/Shared.lean now holds everything
the certified backends need from Turpentine and nothing about any target:
fragment predicates, evaluator inversion, evalExpr_hasTy, the initEnv
unfolding, the answer epilogue and its decoder, and the two
specifications. BespokeWhitespace.lean imports it and shrank from 4449 to
about 3840 lines with no change to what it proves.
Zero warnings. The whole project now builds without a single Lean
warning: 183 unused simp arguments, unused hypothesis names and a few
never-executed tactics in Computability/Fractran.lean, and a local
variable named S next to the S combinator in Computability/Ski.lean.
Tests: 1570, all passing (43 new for the Velato backend).
Velato (Daniel Temkin, 2009) is a language whose source code is a MIDI file: the pitches, in the order the file sounds them, are the program. Commands are intervals from a command root the composer may move at will, so a program keeps its meaning under transposition — and a piece of music is, if the intervals fall right, also a program.
The language. Note.lean pins the two readings of an interval the
language needs (exact for commands, coarse for expressions, which is what
lets a composer stay in the scale) and the digit encoding that falls out of
reserving the unison and the perfect fifth. Parser.lean follows
velato.net's tables, cross-checked against the 2009 C# reference; where the
two disagree the spec page says which we follow and why. The substantive
case is If, whose branch in the reference reads no condition, advances an
extra note, and loops on a tautology, so it cannot return.
The parser records what each note turned out to be, in the parser rather than in a second pass, so the labels under an engraved staff cannot drift from the grammar that produced them.
Turing complete, and the proof is the interesting part. Every other
backend in the library lays the counter machine's registers out side by
side: brainfuck a tape column each, subleq an address, Piet a stack slot.
Velato cannot — a variable is a MIDI note, so a program has at most 128 of
them, while counterProgram may ask for arbitrarily many. One register per
variable would be a compiler that works for small programs and fails for
large ones, which is precisely the failure
docs/agent-brief-completeness.md warns about.
So the unbounded state lives inside a cell rather than across cells. The
whole register file is one number, 2^w0 * 3^w1 * 5^w2 * ..., in a single
variable — middle C; the other 127 are free. Increment multiplies by the
register's prime, decrement divides by it, and "is register r nonzero" is
"does the r-th prime divide the number". The primes are built by a
Bertrand-bounded search rather than taken from Nat.nth Nat.Prime, which is
noncomputable: TuringComplete.compile has to be a function that runs, and
the differential tests run it.
And the theorem stands on a semantic decision, which is stated rather than
assumed. The encoding needs unbounded integers. The reference compiler
emits C# int; under that reading Velato has at most 128 variables of
finite width, hence a finite state space, hence a decidable halting problem
and no completeness at all. The specification names no width, so both
readings are defensible; docs/velato/spec.md gives three reasons for
taking the unbounded one and states the finite-state converse as an open
proof rather than pretending the question does not arise.
Compilers. The hand-written Turpentine backend is the shortest in the
library, because Velato is not a machine: it has while, if/else, named
variables and unbounded integers, so the backend is nearly a direct
translation. Its content is in the four places the languages differ — no
arrays, Euclidean versus truncating division (corrected, which needs
statements, which is why the expression compiler returns a prelude),
short-circuiting that has to survive that prelude, and no boolean type. All
fourteen compilable examples produce byte-identical output to the reference
interpreter. derivedVelato is the certified route, one line off the
completeness witness.
Seeing and hearing it. A Velato program is music, so the runner engraves
it — the same Scene going to PDF, SVG and a raster image, with no
dependency outside Lean, because requiring an SVG rasteriser to look at a
program is a poor trade. It also synthesises audio directly, so
scripts/velato-audio.sh plays a program on a bare checkout.
A finding from the differential tests. VelatoPy prints a char as a
character only for codes in 32 … 127 and prints the number otherwise, so a
program ending in a newline prints a trailing 10 there. The C# compiler
emits a character literal and writes it whatever it is. We follow the C#
reference, which the Python one names as definitive; docs/TESTING.md
records it.
Two claims corrected during the work. A docstring said the derived
backend's output was "much shorter" than other targets'; it is not —
sumsq.turp is 509 kB against subleq's 1.8 kB, because the five statements
carry primes as decimal numerals and Velato spends a note per digit. And a
generated example's header claimed seven of its notes were the shadowed
tune's; three were. Both now report measured numbers, the second computed by
the generator so it cannot rot.
On hiding a program inside a tune, which is the obvious game and worth being precise about: you cannot make a Velato program play an arbitrary melody, because within a statement the pitch classes are forced. What you can do is change key at every statement boundary, which is what velato.net says root changes are for, and that gets about a third of the notes onto the tune. The examples report what they actually achieved. The reliable technique is the author's own: the program is the first track and the tune is the tracks after it, which the language ignores.
1527 tests pass; the axiom audit reports only propext,
Classical.choice and Quot.sound.
AGENTS.md was a hand-made copy of CLAUDE.md and had already drifted: it
was missing the "Example programs" requirement, the whole graphical-languages
policy (derived images, scripts/render-docs-images.sh), and the
Turpentine/Compile/Certified/ section with its warning about name resolution. An
agent that read AGENTS.md — Codex, Cursor, Gemini CLI all look for that
name — was working from stale rules.
AGENTS.md is now a symbolic link to CLAUDE.md (git records it as mode
120000), so the two cannot diverge again: there is one file, reachable under
either name. CLAUDE.md opens with a "This file" section saying so, telling
a tool that rewrites the link into a regular file to restore it, and pointing
at CONTRIBUTING.md for the contribution checklist, which the conventions
file never referenced before. CONTRIBUTING.md points back.
Follow-up to the entry below: CertifiedCompilerNoIO, CertifiedCompiler
and TuringComplete now require LawfulProgLang (the I/O-aware one
also LawfulTraceLang) instead of offering lawful upgrades on the side.
The reason is semantic, not stylistic: against an unlawful interpreter the
∃ fuel conclusion can be satisfied by treating fuel as an input channel —
halt with the right answer exactly at fuels that encode a halting URM
trace — so a language whose programs compute nothing could have claimed a
completeness witness. halted_stable pins fuel to its budget role, which
makes the requirement part of what the statements mean. Since every
language already has its instance, nothing else changed: the whole library
rebuilt without touching a single witness or bespoke proof.
An adversarial pass over Langlib/Common/Compilation.lean and
Langlib/Common/Computability.lean found the definitions sound but with
one structural soft spot and two misleading docstrings, all now fixed.
LawfulProgLang/LawfulTraceLang.ProgLanghad no laws, so the∃ mconcluding every correctness statement said nothing about the fuel bound a runner actually picks. The new classes state fuel stability (a completed run, trace included, is a fixed point of more fuel);CertifiedCompilerNoIO.correct_stable,CertifiedCompiler.correct_stableandTuringComplete.simulates_stableupgrade every∃ mto "every fuel from some point on". EveryProgLangtag has an instance — proved per interpreter inLanglib/Languages/<L>/Stability.leanby one uniform induction (brainfuck's lemma serves Ook! and brainloller too; the bounded Befunge-93 core's proof lives next to that model) — and all threeTraceLanglanguages have the trace counterpart.TuringCompletedocstring now says out loud that the proposition alone is not the completeness claim: aClassical.choicewitness could inhabit it for Deadfish, no axiom audit would object, and what rules the cheat out is the convention thatcompileis a plaindefthat#evalcan apply.halting_decidabledocstrings claimed "noBoundedStoragewitness coexists with aTuringCompletewitness" as if it were a corollary. It is a meta-theorem about computable witnesses, and by a subtler argument than halting-problem undecidability (the one-directionalsimulatesbreaks the naive reduction; the recursion theorem does the work). Both docstrings now say so, and the danglingdocs/computability.mdreference points atdocs/agent-brief-completeness.md.TraceLang.trace_faithful. The trace laws could not stop a trace from underreporting its reads; the new third law can: a halting run, replayed on any stream sandwiched between the claimed reads and the original, is the same run, so omitting a read the behaviour depends on is refuted by the truncated stream. Proved for whitespace and subleq by a two-stream simulation (Langlib/Languages/<L>/Faithful.lean; the line reader's half lives inLanglib/Common/Io.leanasreadLineGo_faithful), and for FRACTRAN for free. Finding the right statement was the work: the law is false for erroring runs — whitespace'sreadnumparse error prints the offending line while consuming nothing — which is why it covers halting runs only, and the docs say so.
docs/malbolge/compiler.md used to say not planned, and gave a good
reason: Malbolge is 59049 words of 59049 values, so it is not Turing
complete, so no total backend from a Turing-complete source can exist and
any backend would be bounded by the machine rather than by our effort.
That reasoning is still correct. What it got wrong was the conclusion —
the demonstration turns out to be cheap, and it fits more than expected.
Langlib/Languages/Turpentine/Compile/Malbolge.lean compiles every
input-free Turpentine program whose output fits. The trick is to give up
on looping in the target: nothing it emits is executed twice, so
self-encrypting cells cost nothing (nothing reads them again) and
position-dependent opcodes cost nothing (the assembler picks each cell
after it knows the address). Control flow — loops, if, arrays,
arithmetic — is resolved by running the source on Turpentine's own
interpreter at compile time, exactly as the Unshackled backend does.
Three things had to be worked out that Unshackled did not need.
Getting c and d apart. movd reads a loaded cell, and a loaded cell
is a byte, so nothing in the image can name an address in the data
row. rotR is the way out: it is a cyclic rotation of the ten-trit
word, so rotating a byte carries its low trits over the top and
manufactures a large value from a small one. The prologue rotates one
cell as many times as the layout needs, and seeds tabulates every
address reachable that way. A jmp at address 2 skips c past addresses
3..125, which is what frees them to hold the pointers at all.
The cost of a byte. Constants come from Olmstead's loader oversight —
characters outside 33..126 are stored unchecked — which leaves 163
usable values at every address. Because they are bytes, their top four
trits are zero, and against a zero trit the crazy operation is a fixed
map, so the accumulator's top four trits are not steerable. That would be
fatal if the target were a value; it is not, because < writes
a mod 256 and some 230 of the 59049 words end in any given byte. On the song,
9% of bytes need no operation at all, 35% need one and 56% need two:
about two and a half cells of code per byte.
Where the wall is. The two rows are the same length, because d
advances whether or not the instruction reads memory. So two words go per
word of code and the longest code row the machine has room for is
29157 cells — checked in Lean, not asserted. A program that wants more
is refused with the number of bytes that would have fitted:
the program prints 25592 bytes ... enough for the first 15191.
The song fits. Langlib/Examples/Malbolge/compiled/99bottles.mal is
57514 of the 59049 words — 97.4% of the machine — and halts in 28363
cycles, one per code cell, against about fifteen million for Iizawa et
al.'s hand-written 99bottles.mal, which has real loops where this has
none. They print the same 11459 bytes;
Langlib/Tests/CompileMalbolge.lean runs both and compares digests, which
is the prettiest test in the file.
Also checked in: compiled/hello.mal (247 cells), sort.mal (197),
primes.mal (251) and sieve.mal (308), all derived files written only
by scripts/gen-mal-examples.sh, which verifies each against its source's
output and whose --check catches a stale one. sort.mal is quoted in
full on the spec page in a stated transliteration and read line by line,
because at 197 cells the whole layout is visible: the descending no-op
ramp, the doubled letters of the rotation loop (rotr and movd are
adjacent opcodes, so the same character at consecutive addresses means one
then the other), the alternating code row, and the data row of constants
with padding under every out. That program never uses the crazy
operation at all — every byte it prints is one ASCII character, and
rotating right is division by three when the low trit is zero, so it
prints 1 by rotating 147.
Input stays out, and not for want of room: crz is tritwise, so no chain
of crazy operations against compiled-in constants can produce a value that
depends on a byte the compiler has not seen. That is the same
obstruction that stops the Unshackled backend, where there is no size
bound at all.
Registered as the malbolge target of turpentine compile --to and
exec --via; 50 new tests in Langlib/Tests/CompileMalbolge.lean;
docs/malbolge/spec.md, docs/README.md, docs/PLAN.md,
docs/TESTING.md and the language README updated to match.
docs/piet/spec.md gained a Programs as PNG section. PPM is what the
runner reads and PNG is what everyone else trades, so the section works the
conversion through in both directions: scripts/ppm-to-png.py for a
whole-number enlargement (one block per codel, --no-grid), sips for a
one-pixel-per-codel thumbnail, and sips -s format pbm back to a PPM the
interpreter runs at --codel-size 24. Every command on the page was run;
the round trip really does print 7.
The lesson the failures teach is the point of the section: a smoothly
resampled program is not a program. sips -z blends the block edges and
the interpreter stops on unknown colour (127,0,0) at the second codel,
and the grid-line variant is off in both dimensions besides. Enlarge with
nearest neighbour or not at all.
docs/piet/img/add.png is the first PNG on the Piet page and, like every
picture there, a derived file: scripts/render-docs-images.sh generates it
with the very command the section quotes, and --check now compares PNGs
in the Piet directory as well as SVGs.
Langlib/Examples/Turpentine/99bottles.turp is the whole song, ninety-nine
verses down to none, in thirty-odd readable lines. It exists to be compared
rather than read: its 11459 bytes are byte for byte what Malbolge's
99bottles.mal prints and what the hand-built Unshackled port
99bottles.mu prints, checked with cmp and not by eye. It reads nothing
as written, so unlike primes-mu.turp and sort-mu.turp it needs no
input-free twin — it is already in the Malbolge Unshackled backend's
fragment.
Compiled and checked in. compiled/99bottles.mu is the third derived
artifact under Langlib/Examples/MalbolgeUnshackled/compiled/, and the
first one big enough to say anything about the backend's cost: 64886 cells,
92602 bytes, verified against the source's own output at rotation widths 10,
37 and 300. It is 82% of the 78790 cells the hand-written port spends on the
same song — the compiler's only direct comparison against a person, and it
comes out ahead. At 32346 code cells for 11459 output bytes it also pins the
cost model down at 2.82 cells per printed byte, the fixed 194-cell overhead
having finally vanished into the rounding (2.85 for the primes, 2.95 for the
greeting, 3.08 for the sort).
One song, three suites. The Malbolge suite rebuilt the song privately to
check 99bottles.mal. That definition moved to Langlib/Tests/BeerSong.lean
so the Turpentine golden test can compare against the same string rather
than against its own output recorded after the fact — which is the only
thing that makes the new test say something.
What is not tested by lake test. compiled/99bottles.mu is in none of
the compile suites: one run costs some fifteen seconds, because the
interpreter's cost grows with the size of the program. It is checked by
scripts/gen-mu-examples.sh, which now compiles all three sources and runs
every program it emits — the same trade the hand-written 99bottles.mu
already had.
A documentation pass over every status table, prompted by the Malbolge Unshackled backend landing without the top-level matrix noticing.
The matrices. README.md's row for malbolge-unshackled still said
planned; it now names the bespoke backend, its source, and the two facts
that go together — the completeness claim is open, so there is no witness
to derive a compiler from, and the backend cannot compile read, because
reading needs the same machinery the completeness proof does. The list of
bespoke targets in the same file said three languages; there are eight, and
six of them are unverified rather than all of them. Langlib/Languages/Turpentine/README.md's
example table was a two-column header over three-column rows, so a third of
it did not render at all; it is now a real third column saying what each
file was written under, with sum.turp, primes-mu.turp and sort-mu.turp
added and suite/ pointed at.
Four places still said nothing inhabits CertifiedCompiler.
bespokeWhitespaceIO landed in the commit before last, and
docs/verification.md was in the odd position of marking whitespace
**yes** in its behavioural column and then denying it in the paragraph
underneath. docs/README.md, docs/PLAN.md and README.md had the same
sentence.
Two places said arrays are outside the certified fragment, while two
other places in the same two files said they are in. They are in:
sieve-tc.turp compiles and answers 15. Checked by running all twelve
-tc examples — eleven compile, sort-tc alone needs subtraction.
docs/TESTING.md had aged badly. It said scripts/axioms.lean did not
exist (it audits 523 declarations and is clean), that no URM differential
suite existed (there are seven), and it invoked the audit with a --run
that is not how it is run. It gained a malbolge-unshackled section — the
one language whose reference interpreter randomises the thing a
differential test would compare, which is why a width sweep replaces it —
and a fourth entry for scripts/gen-mu-examples.sh --check, the
derived-file check that now sits beside render-docs-images.sh --check.
Line anchors and links. Every #L anchor in the repository was checked
against the file it points into; six had drifted, one by 51 lines. Two
table rows had an unescaped | inside a code span — docs/conformance.md's
|| row and docs/unlambda/spec.md's row for the | builtin — which
splits the cell in every markdown renderer, so both rows were rendering
with the wrong number of columns. And the compile example that told a
reader to write hello.mu into the repository root now writes it to
/tmp, which is what every other language's compile example already did.
Every command quoted in the pass was run: lake build and lake test
clean (1376 tests), scripts/gen-mu-examples.sh --check up to date, and
the axiom audit sorryAx-free.
Stage 6 milestone 2 opened with a question: do whitespace's parseNumLine
and Turpentine's parseIntLine accept the same lines and agree on the
value? docs/whitespace/compiler.md had asserted they do, and the plan
said finding out was worth the trip.
They did. On every line a reader can produce, by two accidents that
cancel. Turpentine trimmed with String.trimAscii, which strips newlines
as well as blanks — unobservable, because readLine? never leaves a
newline inside a line. And its digits went through String.toNat!, which
skips underscores and panics on a non-numeral — unobservable, because the
all Char.isDigit guard in front of it rejects _ first. Two languages
reading a number the same way for reasons neither of them states.
What did not survive was proving it. String.toNat! is a
String.Slice.foldl, and core ships no lemmas about slice folds at all, so
certifying the coincidence meant developing that theory first — a
disproportionate amount of work to establish something that ought to hold
by construction. So there is now one parser, Langlib.Common.parseNumLine,
sitting beside the readLine? both languages already shared, and the
agreement is definitional rather than accidental. Turpentine's accepted
language is unchanged on every input a program can be given; the whitespace
and Turpentine golden suites, and the trace suite that runs the two
interpreters against each other, all pass unchanged.
This is the prerequisite for the rest of milestone 2, which is the proof:
SimS relating the two cursors, readInt joining the fragment, and
encodeInput becoming the identity instead of "run the target on nothing".
CertifiedCompiler has an inhabitant. bespokeWhitespaceIO is the
hand-written Turpentine-to-whitespace backend proved behaviourally
correct on the output fragment, and its encodeTrace is the identity:
the compiled program does not re-encode the source's I/O into a target
convention, it performs it, byte for byte and in order. §1.4 of
certified-compilation.md had been titled "what is proved behaviourally:
nothing, yet" since the definitions landed. It is not called that any more.
Three details decide whether the statement means what it looks like it
means, and all three are in the definition rather than in prose. The
specification is stated at answerProgram p — the source with the
epilogue the compiler appends — because the epilogue's newline and answer
are events the compiled program really performs, and naming only the
source's own events would describe a different program. encodeInput
ignores the source's input stream, which is honest only because the
verified fragment cannot read; when readInt joins it, the input events
will have to match too. And the source-side specification is not a trace a
compiler author picked: behavesWith_wf says the events it names are a
real run's.
The proof reuses the answer-only one rather than repeating it. The old
end-to-end theorem became bespokeCompile_core, which reports the same run
three ways — it halts, its output decodes, and its trace is the body's
trace followed by the epilogue's two events — and both the answer-only and
the behavioural theorems are corollaries. What the behavioural one adds is
an inversion of the epilogue on the source side, and that turned up the
one thing worth warning the next person about: seq runs its second half
at one less fuel, so answerProgram's two appended statements need two
fuel of their own, and a bound too small to reach them contradicts the
hypothesis that the whole thing halted.
That same fuel arithmetic is why toCertifiedOf back to HaltsWithAnswer
is not in this commit. At equal fuel it is false-shaped: a body that
halts with exactly n leaves nothing for the epilogue. Closing it needs
fuel monotonicity for Turpentine.exec, which this library deliberately
does without, since Reaches carries fuel exactly. Nothing is lost —
bespokeWhitespace proves the answer-only statement directly, against a
sharper specification, and toCertified gives the erased direction free.
Five new tests compile with the behavioural compiler, run it, and compare
the two event lists outright. Milestone 1 of Stage 6 is done; milestone 2
is readInt, and with it the first proof that a compiled program reads
what its source reads.
Langlib/Examples/MalbolgeUnshackled/compiled/primes.mu (348 cells) and
compiled/sort.mu (268 cells) are the first Unshackled programs in the
library that nobody wrote and no search found. They are compiler output:
the primes up to 30, and six numbers sorted.
Both sources needed twins. primes.turp reads its bound and sort.turp
reads its six numbers, so the backend refuses them by name; primes-mu.turp
and sort-mu.turp fix the bound at 30 and seed the same six literals
sort-tc.turp uses. The -mu suffix is the -tc convention applied to a
different restriction — -tc means no I/O at all, -mu means no input —
and the difference is the point: these keep the streaming output a register
machine cannot produce.
They are derived files on the model of the graphical languages' pictures.
scripts/gen-mu-examples.sh is the only thing that may write them, it
checks each compiled program against its source's own output as it goes, and
--check fails on a stale one. The division of labour is worth stating,
because it is not the obvious one: the test suite checks that the compiler
produces the right output for both sources (recompiling from scratch) and
that the files in the tree are Unshackled programs printing the right thing
(loaded by Unshackled's own loader, run at two rotation widths, with nothing
from the compiler involved) — but only --check catches staleness, exactly
as with the images.
Nine new tests, 71 in the file. sort.mu is quoted in full in both
docs/malbolge-unshackled/spec.md and the compiler page, in a stated
transliteration (a cell in 33..126 as itself, a data cell as its code point
in angle brackets) that is generated and verified to decode back to the file
byte for byte — the first draft of it, typed by hand, was missing the final
character. The whole layout is legible in it: the three-cell prologue, the
descending ramps that padding always makes, the two pointer cells, the data
row, the 64-cell gap and the code row.
The hand-written whitespace backend's simulation used to say that a
statement's code reaches the same heap. It now says that it reaches the
same heap and performs the same I/O events, and the witness is one list
used on both sides: not "the target's output re-encodes the source's" but
the compiled program prints the bytes the source prints, in that order.
print("..."), print(e) and println(e) for an int or a bool are in
the verified fragment, and thirteen new tests run each of them through the
reference interpreter and through the backend and fail unless the two agree
byte for byte.
Two things fell out of it that were not in the plan.
The epilogue had to move. bespokeCompile appends a statement to make
the specification's single Nat observable, and reading it back used to be
"parse the whole output as a decimal numeral". A program that prints for
itself breaks that, so the epilogue is now println(""); print(answer);
and decodeAnswer reads the digits after the last newline. No extra
restriction on the fragment is needed to justify it: toString (answer : Nat) is all digits, so the epilogue's newline is provably the last one in
the output, whatever the program said first.
The fragment had to become type-checked. print(e) is the first
construct whose code depends on the expression's static type — outnum for
an int, a jz between two string constants for a bool — while the
reference interpreter renders the runtime value. A program that stored a
boolean in an int variable would print true where its compilation
prints 1, and nothing ruled that out, because the encoding erases the
difference: a bool and the integers 0/1 are the same whitespace cell.
So Agrees carries the typing as well as the value, checkFragment
rejects an assignment whose right-hand side has the wrong type, and
evalExpr_hasTy proves static and runtime types agree. That proof is
shorter than it sounds, because the reference semantics does most of it:
evalBin throws on operands of the wrong shape, so an addition that
produced a value at all produced an integer. Only three forms need more —
a variable, whose type comes from Agrees, and && and ||, which return
their right operand and so need the induction hypothesis.
Also worth recording: the output is carried through the simulation as the
string that was appended, not as raw bytes. That is what keeps the whole
output decodable, since String.toUTF8 distributes over append, so a run's
output stays the encoding of one string rather than a byte array nobody can
parse. reaches_bytesCode, by contrast, deliberately does not name the
bytes it wrote; Whitespace/Trace.lean recovers them from the trace.
What is left in Stage 6 milestone 1 is the packaging: bespokeWhitespaceIO : CertifiedCompiler, with spec at answerProgram p and encodeTrace
the identity. docs/certified-compilation.md §1.4 still says "nothing,
yet", and will until that instance exists.
The library's hardest target has a backend:
Langlib/Languages/Turpentine/Compile/MalbolgeUnshackled.lean, over the
programs that do not read input. Written up in
docs/malbolge-unshackled/compiler.md.
The surprise was how small the assembler is. Unshackled's reputation rests
on the instruction at c being (mem[c] + c) mod 94, so that code is not
relocatable; but a compiler that only needs a cell to run once can solve that
in one line — (opcode - addr) mod 94 lands in 0..93, and adding 94 when
it is below 33 lands in 94..126, so every instruction is printable at
every address. The residue arithmetic that dominates hand-written Malbolge
costs nothing here. What is expensive by hand is a cell that has to run
twice, which is a different problem, and not this backend's.
The layout follows from one observation: c and d both advance by one
after every instruction, so they keep a fixed distance. That gives a code
row and a data row running in parallel, each crazy cell reading the
data cell directly below it. Three cells of prologue set the distance up, and
the first two of them decide their own contents: movd at address 0 is the
word 40, so d lands on 40 and the pointer cell is address 41 whatever else
the compiler does.
Two things had to be discovered rather than looked up.
The proof's constants cannot be loaded. crz_two_steps says two crazy
operations take any accumulator to any target and computes the constants, but
toTwoConst picks a value whose repeating trit is 2, and a source
character is a code point, whose repeating trit is 0. Loadable constants
exist because the crazy operation is tritwise and, above both operands,
five of the nine trit pairs work rather than only (0,0) — so a constant can
be padded upwards until it lands on a code point the loader accepts.
twoStep enumerates those paddings most-significant-first, and a sweep over
all 16384 accumulator/target pairs below 128 at eight address residues finds
one every time, never larger than 6641.
Data cells are the loader's bug. A character outside 33..126 is stored
unchecked — Malbolge's accident, Johansen's default, spec decision 5 — and
that is the whole data channel: jump targets and crazy constants ride in as
characters above ~. The emitted file therefore needs the loader's default
setting; --strict rejects it, and a test asserts that it does.
Tests are in six suites, Langlib/Tests/CompileMalbolgeUnshackled.lean:
differential against Turpentine's own interpreter (including every byte from
1 to 127 through printByte), the same programs at seven starting rotation
widths from 10 to 300 (the backend emits no *, so the width must not
matter, and it does not), an audit of every emitted cell read back the way
the loader reads it, pinned cell counts, a straight-line check at a fuel
bound of n + 4, and ten refusals.
The gap is input, and it is not more code generation. The first version of
this paragraph got the reason wrong and a parallel session caught it, which
is worth recording because the correct version is sharper. Two crazy
operations against compiled-in constants can turn an unknown value into a
uniform one: crz (crz a ...222) ...000 = ...000, which is crz_absorb
and is the first step of the verified branch pipeline. Individual columns of
the table are non-constant, but two of the nine compositions are. What a
chain of crazy operations cannot produce is a uniform value that depends
on the accumulator: crz is tritwise, so each output trit sees only the
input trit at its own position, and two inputs differing at one position
agree at every other, while ...000 and ...222 differ everywhere. That
sharper statement is now no_accumulator_flag in
Langlib/Computability/MalbolgeUnshackled/Main.lean, proved by the session that
caught the error — so a branch flag provably has to be read from something
already uniform, which is what forces the unary register encoding rather
than merely recommending it. A comparison still cannot be collapsed without
*, and * is mandatory anyway for addressing, which is the argument
widthBounded_step1 actually proves.
One consolation prize, and it works: crz (crz a k) k with k all ones
below the width of a is the identity (the 0 ↔ 1 transposition twice),
and k is a plain natural, so it loads. Two crazy operations therefore copy
an unknown accumulator into a memory cell, and a movd through that cell
turns the copy into an address. inputProbe in the backend is a 2207-cell
hand-built image that does it: it reads one character, dispatches 128 ways
through a table at addresses v+1, prints AAA for a and CCC for c,
and echoes anything else — with no rotation anywhere, at every rotation
width. It is not wired into compile, because one dispatch is not a machine;
it is there so the mechanism the input half needs is checked rather than
sketched. End of input is the interesting failure: above the width of k the
column applied is k's lead twice, which sends 2 to 1, so ...22
copies to ...1222…2, whose leading trit is 1, and the jump lands where no
loader ever wrote.
The third and last of the trace semantics, and the shortest: subleq has one
instruction, two of whose forms do I/O. Subleq.State records the run's
events, Langlib/Languages/Subleq/Trace.lean proves the two TraceLang laws
from the same invariant as whitespace's, and instance : TraceLang SubleqLang now sits beside ProgLang SubleqLang. Reading at end of input
consumes nothing and so records nothing, which is the honest report: no byte
crossed the boundary.
Both backends the library has proved answer-correct can therefore now be stated behaviourally. That was the whole point of the exercise.
The interesting part is what the tests found. docs/certified-compilation.md
has always said encodeTrace for the subleq backend is the identity, on the
grounds that it hands the target the bytes the source read and wrote.
Nothing checked it, and there was room to doubt, because subleq does not
have an "print this integer" instruction: it prints integers through the
printint runtime routine, which builds a decimal numeral by repeated
doubling on top of a self-modifying calling convention. It emits exactly the
bytes Value.render does.
println(-12345) turpentine >45 >49 >50 >51 >52 >53 >10
subleq >45 >49 >50 >51 >52 >53 >10
Ten new tests, three of them that cross-check. The cost was the same as
whitespace's and is worth recording because the next backend will pay it
too: the completeness proof builds machine states with positional ⟨…⟩
literals, so a fifth field meant threading an events parameter through
thirty-odd of them. They are now stated for an arbitrary prior trace, which
is the more useful form. The subleq input branch also had to be rewritten
from a shared let into two branches so that a proof can case on the read —
the third time that pattern has come up, after Turpentine's
a[i] := readByte().
A correction to the encoding committed earlier, and an improvement worth
the change. Blank ...000 with mark ...111 makes set, clear and test one
operation each, but the test is destructive: a mark reads back as ...222
and needs a second operation to restore. The test is the loop condition, so
it runs on every iteration of every compiled loop, which makes it the one
place worth optimising.
Take blank = ...000 and mark = ...222 instead. The accumulator
...111 then satisfies crz ...111 b = b for both values, so testing
leaves the cell exactly as it was, and the value it leaves in the
accumulator is the cell's own content: Value.zero for blank, Value.eof
for mark. Those are precisely the two flags branch_arith consumes, so the
probe feeds the branch with no conversion (probe_feeds_branch). The test
accumulator is itself loaded self-restoringly, from a blank accumulator
against a cell holding ...111, so the whole probe is two chain links and
every cell it touches comes back unchanged.
The price falls on set: no single operation takes ...000 to ...222,
so setting a mark costs two visits to the cell and hence two gadgets.
Paying there to make the loop condition free is the right trade, since the
condition runs once per iteration and set once per command. Both
encodings are kept in the file, with this one marked as the one the
compiler uses.
Two more layers, and the compiled-command unit now exists.
enter_chain is the prologue that positions d. movd is the only
instruction that moves it, and a re-enterable movd must sit at residue 60
or 64 modulo 94 while a chain starts at 82, so the two cannot be adjacent;
one stable jmp bridges them. Two instructions: movd reads a pointer
cell holding D - 2 and re-aims d, then jmp reads the cell at D - 1,
holding A - 1, and drops control at A. Afterwards c = A and d = D,
exactly what chain_run wants, with the accumulator untouched.
gadget_run composes prologue and chain: 2 + 2n steps that position d,
fold n operands into the accumulator, and leave the result both in the
accumulator and in the last operand cell, every jmp cell still standing.
Its hypotheses are stated on the initial memory, the prologue's two
writes being transferred across by its frame, so a caller reasons about one
memory rather than a chain of intermediate ones. chainFold_congr is the
small congruence that makes that transfer work.
That is the unit a compiled counter-machine command is built from. What
remains is to instantiate it for inc, dec, emit and loop, write the
assembler that lays the cells out, and run the induction on Ev.
chain_run composes n links, laid out at stride 94. The stride is
both forced and convenient: a re-enterable crazy must sit at residue 82
or 86 modulo 94, so putting the links 94 apart lands every one of them on
the same residue, and a single word then serves for every crazy cell and
a single word for every jmp. Link i occupies A + 94i and
A + 94i + 1, jumps to A + 94i + 93, and control resumes at
A + 94(i+1); the 92 cells in between are never executed and need no
words. Data sits after the code, operand i at D + 2i and jump target
i at D + 2i + 1, since d advances two per link.
The theorem runs the chain in 2n steps: the accumulator folds the
operands (chainFold), each operand cell keeps its intermediate, each
crazy cell is encrypted once, every jmp cell comes back unchanged, and
a frame condition names the only cells touched. That last pair of clauses
is what makes the chain re-enterable and what let the induction go through:
at each step the next link's cells are shown untouched by all the previous
ones.
This is the executor a compiled gadget runs on: straight-line arithmetic of any length, laid out mechanically, with one induction behind it.
The layout problem had a solution I had been walking past. Laying a gadget
out as one contiguous row forces padding into the gaps between working
cells, and padding is the awkward part: a re-enterable crazy must sit at
residue 82 or 86 modulo 94, while the cells between fall wherever they
fall, including the sixteen residues at which no two-cycle word is harmless
in both phases.
Interleaving removes the problem entirely. Put a jmp immediately after
each working cell and let it carry control to the next one. A jmp never
encrypts itself, so it is stable for the whole run and the control path is
identical on every pass, while the working cells alternate between their
instruction and a no-op. The cells jumped over are never executed and need
no words at all; only the landing cell is encrypted, and encryption keeps a
printable word printable.
d advances two per link, so each link owns two data cells at a known
stride: the operand the crazy reads at D and the address the jmp
reads at D + 1, both placed statically. chain_link proves one link in
two steps, with a frame condition naming the only three cells it touches
and, crucially, a clause saying the jump cell comes back unchanged. That
clause is what makes a chain re-enterable, and it is jmp_cell_stable
cashed out in the form a compiler can actually use.
Turpentine now records its I/O the way whitespace does: an events field
on State, four recorders, and the same invariant proved in
Langlib/Languages/Turpentine/Trace.lean — the trace's output events are
the output, and its input events followed by what the cursor has left are
what the stream started with.
Turpentine is not a ProgLang, so there is no TraceLang instance to
inhabit. What it needs the trace for is to be the τ in a specification,
and that is TurpentineBehavesWith p σ n τ result: the I/O-aware
refinement of TurpentineHaltsWith, naming the input stream and the events
the answer-only version drops. behavesWith_wf is the part that keeps it
honest — whatever τ a program is specified to perform, its output events
are the bytes a real run emitted and its input events are a prefix of the
stream it was handed. A compiler proved against this is constrained by what
the program does, not by a trace chosen to make a proof go through.
Two wrinkles worth recording. The induction is on the fuel and then on
the statement, because seq consumes no fuel: exec (n+1) (s₁; s₂) runs
s₁ at the same bound and a smaller statement. And a[i] := readByte()
shared its two ends of input through a let, which no tactic could case
on; it is now written out as two branches. The semantics is unchanged,
including the part worth being explicit about: a failed store rolls the
read back, and the trace rolls back with it. The run that reports the error
is the run that did not keep the byte.
The payoff is that encodeTrace = id stopped being a prediction. Fifteen
new tests, and six of them run a program through the reference interpreter
and through the hand-written whitespace backend and fail unless the two
performed the same events in the same order — decimal rendering, boolean
words, readInt consuming its line terminator, and reads and writes
interleaved. docs/PLAN.md Stage 6 still has to prove it over a fragment.
It is now being checked on every lake test.
The counter machine wants set, clear and test on a register cell, and the
encoding decides what each costs. Taking blank = ...000 and mark =
...111 makes all three cost exactly one crazy operation, which is the
least the language allows, since p is the only instruction that writes.
Reading Olmstead's table by the accumulator trit: against ...000 a blank
becomes a mark; against ...111 a mark becomes a blank; against ...222 a
blank gives ...000 and a mark gives ...222.
The third row decides the architecture. p leaves its result in the
accumulator as well as in the cell, so testing a register cell against
...222 puts exactly the flag branch_arith wants into the accumulator:
Value.zero for blank, Value.eof for mark. The zero test costs one
instruction and needs no broadcasting, which the crazy operation could
not do anyway, being tritwise. That settles the representation question in
favour of unary registers, by the table rather than by taste, after several
candidates that all needed a loop just to test for zero.
The test is destructive on a mark, which reads back as ...222, but one
more operation against the same constant restores it and a blank survives
both untouched, so the pair is a non-destructive test whichever the cell
held (register_test_roundtrip).
A short follow-up. Counter.CState records only how many bytes were
emitted, because every byte a compiled program emits is the same. So the
output half of the witness costs almost nothing: emit becomes a gadget
that sets the accumulator to a fixed printable natural and executes one
<, and the answer decoder is decodeBytes bs = some bs.size.
The byte is 42, '*': one UTF-8 byte, not ...22 (which would close the
output stream) and not ...21 (a newline), so doOutput takes its
ordinary branch. doOutput_star and step1_out prove the step,
outClosed_of_step1_out records that emitting leaves the stream open so
emits compose, and decodeBytes_append_star reads the count back.
That settles emit and decodeOutput, two of the pieces a
TuringComplete witness needs. What is left is the register
representation and the gadgets for inc, dec and loop, the assembler
that places them, and the induction on Ev.
Two things this batch: the target for the compiler is now fixed, and the composition obstacle is cleared.
The target. Langlib/Computability/Common/Counter.lean already carries the
target-independent half of every completeness proof: counterProgram
compiles a URM program and its inputs into a structured counter machine
with four commands (inc, dec, emit, loop), and
counterProgram_spec proves the simulation. So a Malbolge Unshackled
witness does not need a dispatcher or a program counter at all. Ev is a
big-step relation with structured control, so the compiled program is
generated by recursion over the command tree, exactly as the Brainfuck
backend does. Two items drop off the remaining list.
The obstacle. A compiled loop body is re-entered, and every executed
cell is overwritten by its own encryption. The two-sweep discipline said a
row of two-cycle cells run twice comes back to itself; what was missing was
the wrapper that drives both sweeps. two_sweep is it. A row at b+1 … b+L
plus one jmp at b+L+1 runs in 2L + 2 steps: the work sweep folds the
operands into the accumulator and leaves every cell encrypted once, the
jmp reads the first table entry and lands control back on b+1
(encrypting b, never itself), the no-op sweep encrypts each cell a second
time and restores the two-cycle words, and the jmp reads the second entry
and leaves. Since the row is restored, the gadget may be entered any
number of times, so a loop body built from such gadgets is itself
self-restoring, and nesting composes.
Underneath it, row_run generalises crazy_run and nop_run to a mixed
row, which is what a gadget really is: a re-enterable crazy cell must sit
at residue 82 or 86 modulo 94, so working cells are spaced and padding fills
the gaps. rowFold is the fold it computes, applying the crazy operation at
the working positions and skipping the padding.
Remaining for the witness: the register layout, gadgets for the four
commands built from two_sweep, the assembler that places them, and the
induction on Ev.
cat.ws copies its input. Nothing the library could say about it
distinguished that from a program that reads everything and then writes
everything, because a RunResult records the bytes that came out and
nothing whatever about the bytes that went in. It does now:
cat.ws on "hi" -> <104 >104 <105 >105
Whitespace.State carries the run's I/O events, most recent first so
recording a byte stays O(1), and the four I/O instructions push to them.
Langlib/Languages/Whitespace/Trace.lean proves the two TraceLang laws
from a single invariant on a reachable state: what the trace says was
emitted is the output, and what it says was consumed, followed by what
the cursor has left, is what the stream started with. The second half is
stronger than the prefix law it implies, and being an equation is exactly
what makes it survive a second read — the residue is what the next read
draws on. Only the four I/O instructions disturb it, so the other twenty-odd
cases of the induction over exec are the hypothesis itself, discharged by
exact.
instance : TraceLang WhitespaceLang now sits beside ProgLang WhitespaceLang, where FRACTRAN's does. FRACTRAN's came free from
ofInputFree, since it provably never reads; whitespace's is the library's
first for a language that does.
Two costs that were not in the plan. ByteArray.toList is defined in core
by a private loop with no lemmas at all — not even that it is Array.toList
of the array inside — and both trace laws are stated about it, so
Langlib/Common/Io.lean proves that bridge and the three consequences.
And the whitespace completeness proof builds machine states with positional
⟨…⟩ literals, so a seventh field meant threading an events parameter
through every block lemma; they are now stated for an arbitrary prior
trace, which is the more useful form anyway.
Eight golden tests pin the interleaving, which is the one thing the two laws deliberately do not determine: both are satisfied by a trace that reports every read before every write. The suite re-checks both laws on each run before comparing, so the tests and the theorems cannot drift.
neverHalts_of_invariant covers loops that must not stop. A simulation
needs the opposite: TuringComplete demands the compiled program halt with
the right output whenever the machine it simulates does. Three lemmas give
that shape. exec_run?_add splits a run anywhere, so a proof can reason
gadget by gadget and stitch the pieces. exec_halts_of_run? is the ending:
a run that arrives at a cell decoding to halt reports Exit.halted with
the output it accumulated, restated at the language interface as
image_halts_of_run?. And run_of_measure is the loop rule: an invariant,
a measure that strictly decreases each pass, an exit at zero. In a
simulation the measure is the simulated machine's remaining step count, so
this is what turns "the URM halts" into "the compiled program halts".
An encoding note that saves a whole gadget. branch_arith decides on a
cell holding ...000 or ...222, which are Value.zero and Value.eof.
A register cell storing a unary digit as blank-or-mark in exactly that
encoding is a branch flag, so the zero test costs no instructions
(branch_on_mark). That is worth having because the crazy operation is
tritwise and cannot aggregate across trit positions: a zero test on a wide
number would need rotations and a loop, while blank-or-mark needs nothing.
It is an argument for the unary register representation, made by the
arithmetic rather than by taste.
Two things, and the second was easy because of what the first one was not.
Langlib/Languages/Turpentine/Compile/Piet.lean, reachable as
--to piet --bespoke and tested in Langlib/Tests/CompilePiet.lean. Two
compilers stacked: the first lowers Turpentine to a flat list of lanes
— straight-line runs of Piet commands ending in a goto, a two-way branch
or a halt — and holds no geometry; the second lays lanes out as corridors
wired with white and holds no Turpentine.
The layout rests on the four facts recorded last commit, each checked
against evalGrid rather than reasoned about. Lane i is a corridor on
row 2i; odd rows are white, which is what keeps two corridors from
merging. A jump is one clockwise circuit and no commands at all: right to a
wall, down a wire column, left to the target's entry column, up to its
row, right onto its first block.
The constraint that is not obvious is that lanes' end columns strictly decrease down the picture. A branch's wire falls down its own lane's end column, crossing every lane below it, and may only do that because a lower lane ends further left and so cannot reach that column. Getting that backwards is the one way to build a picture that looks right and runs wrong.
Halting is the shape the prototype found, and the probe confirmed both halves: a bar of three entered from above through its middle codel halts, and the same picture with a bar of one escapes back into the white it arrived through and runs forever.
Variables live on the stack, reached with roll — O(depth), the price of
having no heap. Constants are built rather than spelled out, since a push
pushes the block's codel count: 16384 costs 51 codels instead of 16384.
opFor_advance proves the generator's colour arithmetic inverts opFor on
all 17 commands at all 18 colours, by decide, so no Mathlib crosses into
Langlib/Languages/. With colorOfRgb_toRgb that is both round trips: the
image is the grid the compiler built, and the grid is the commands it
chose. The layout is proved by nothing and carried by test.
Two bugs the tests caught rather than the design avoided. Lane 0 has to
be the program's, not the trap's, since execution starts on row 0. And the
compile-time stack depth is a property of the program point, not of the
path walked to it: letting the counter run on through both branches of the
division correction put every variable access after a / one slot too low,
which surfaced as gcd.turp printing 42 where the reference printed 21.
The generator now checks that two joining lanes agree.
All 20 conformance programs compile and match the reference, all four sign
pairs of Euclidean division among them, and Piet is now the suite's seventh
compiled runner. Sizes, measured: answer := 2 is 98,338 bytes through the
certified route and 8,757 through this one, which also prints its answer.
Arrays, which were the last refusal. An array lives on the stack like
the scalars, but the roll amounts stop being literals: reaching a[i]
rotates by a distance known only at run time, and roll consumes the
distance, while the read needs it twice and the write three times.
Recomputing i is out (it may be any expression) and a spare copy on the
stack does not survive, because it sits inside the region the rotation
disturbs. The fix is two scratch slots below every variable: a rotation
that reaches an array element cannot reach something deeper than every
element, so the scratch slot's index after the rotation is the index it had
before.
Every access is bounds-checked, and has to be — an index off the end would
rotate the wrong distance and silently corrupt the variables below it,
which is worse than stopping. The check fits in one lane rather than
two: both halves of 0 ≤ i < n are a greater, both results are 0 or 1
so their conjunction is a product, and neither can trap so there is nothing
to short-circuit. That is worth more than it sounds, because a lane is two
rows and its branch is two wires, and the interpreter's per-step cost grows
with the picture's area: folding the two checks into one took sieve.turp
from 410 x 80 codels to 400 x 68, and the twenty conformance programs
from 73 seconds to 45.
What remains is the price of having no heap: every element access is
O(depth), so a loop over an n-element array is quadratic, and
sieve.turp is the slowest program in the suite by a factor of three.
The hand-written half of the conformance suite goes from 4 of 20 to 4 and
20. Whitespace takes the lot, and the reason is the language rather than
the effort: outnum prints a number, so nothing needs brainfuck's
divide-by-ten printer; cells are unbounded signed integers, so fact and
power need nothing special; and the heap is integer-addressed, so an
array index is an add and a retrieve.
Whitespace has no comment syntax because it does not need one — every
character that is not a space, tab or linefeed is ignored — so the .ws
files carry a mnemonic in brackets before each instruction's tokens and the
author's prose in braces, and are still exactly what the interpreter runs.
The prose is underscore-joined because a comment may not contain a space, a
tab or a linefeed: all three are code.
Two of the twenty earned their keep. divmod.ws is the one program whose
answer whitespace cannot give directly, since its div and mod floor
where Turpentine's are Euclidean; the hand-written program reaches the same
correction the compiler emits, independently, which is exactly the
agreement the suite exists to notice. And every array program writes each
cell before reading one: our heap defaults to zero and would have let a
lazier program pass, but the authors' wspace crashes on a cell that was
never stored.
Input.readLine? was a partial def, which Lean compiles to a constant
with no equations, so nothing whatever could be proved about the bytes a
line read consumes. Four interpreters call it — whitespace's readnum,
Turpentine's readInt, Thue and FRACTRAN — and the hole was already
load-bearing: Langlib/Common/Computability.lean records it as the reason
the whitespace completeness witness loads its registers from compiled-in
constants instead of from its input stream.
It is now well-founded. readLineGo recurses on data.size - pos, which
a successful read strictly decreases; readLineBytes? returns the raw
bytes, so the numeric-parser lemmas that milestone 2 needs will not have
to go through String.fromUTF8!; and readLine? is the thin String
wrapper the callers already used. Behaviour is unchanged, edge cases
included — an unterminated final line is still returned, the newline is
still consumed, empty lines still come back empty.
The point of the exercise is the three cursor facts, proved for read?,
for the worker, and for both readers: reading never swaps the stream out,
the cursor only advances, and it never leaves the data. Those are exactly
what TraceLang.trace_inputs needs, and needs for every whitespace
program rather than only the fragment a compiler proof covers — a trace
that composes over two reads has to know the second read sees the stream
the first one left. First step of Stage 6's behavioural certification for
whitespace; the plan is in docs/PLAN.md.
With the architecture settled on a finite self-modifying code region, the
frontier was making a gadget survive repeated entry. The discipline turns
out to be simpler than loop.mu's jump-restore trick, and it needs no
jumps at all.
A cell holding a word of the 70 ↔ 74 cycle alternates instruction,
no-op. So run the gadget row twice: the first sweep does the work and
leaves every cell in its no-op phase, the second executes the same cells
as no-ops and returns each to its original word. crazy_run was already
the work sweep; nop_run is the other, and it constrains d not at all,
since a no-op reads no operand. encrypt_encrypt_two_cycle and
row_restored close the circle.
Getting from one sweep to the next costs one cell: a jmp, stable because
it never encrypts itself, reading a target table that d walks. The same
cell fires at the end of both sweeps and reads a different entry each
time, back to the top after the work sweep and onward after the no-op
sweep. That is loop.mu's cell 155 doing a job with a name.
Traced against the interpreter, with two crazy cells at residues 82
and 86 of one 94-block: both fire on the work sweep, both no-op on the
second, both hold their original words afterwards, and the third sweep
begins identically to the first.
Two facts make the layout easy, and both follow from compiling to an
Image rather than to source, where the loader's checks do not apply.
Padding is universal: at every one of the 94 residues there is a code
whose whole xlat2 orbit is harmless, so gaps between working cells cost
nothing. For loadable source only 14 residues work, which is why loop.mu
needed 201 cells. And each instruction has exactly two 2-cycle
residues, four apart: crazy at 82 or 86, movd at 60 or 64, jmp at
24 or 28. A working row therefore places two cells of a kind per 94-block
and pads the rest, and the assembler's placement problem is short
arithmetic.
Item 1 of the remaining list is now closed. Next: assemble the register file from the verified primitives, then the dispatcher.
The frontier was re-enterability: a dispatcher re-executes its cells, and
every cell is overwritten by its own encryption. Two architectures were on
the table. (A) A finite self-modifying code region whose xlat2 orbits are
managed across passes, which loop.mu demonstrates at three cells. (B)
Never re-execute a cell at all: run the unbounded computation through fresh
memory. (B) is genuinely available to a compiler, which builds its
Image directly and so chooses all six entries of rest; the
restTable_not_printable obstruction binds load, not compile. So the
choice had to be made on merits.
It is settled against (B), and by arithmetic rather than taste. A virgin
phase is the addresses congruent to j modulo 6, all holding one word, so
opcodes run (w + a) mod 94 across the phase. Addresses in a phase share a
parity, hence so do opcodes, and the split is exact: the even opcodes are
jmp, movd, crazy, nop, the odd ones out, inp, rotr, halt,
four per phase per 282-cycle and no other option. An all-even background is
a compute engine with no halt, which looks ideal until one notices it has
no rotr, and widthBounded_step1 already proved rotation mandatory for
unbounded storage. A rotating background is an odd background, and then
rotr_forces_halt: since 81 - 39 = 42 and 42 is a multiple of 6, a
rotr at address a puts a halt at a + 42 in the same phase.
halt_forces_rotr is the converse, so the two are inseparable.
Fresh-memory execution therefore costs a halt-dodge every 42 addresses of every rotating phase, on top of the computed jump targets the finite route needs anyway. It is a tax, not a contradiction, and the docs say so; but it buys no simplification, so the development commits to (A): a finite self-modifying code region with managed orbits. Both doc pages record the decision and the reasoning.
The next layer is now well posed: take the eight-instruction branch gadget
and make it survive repeated entry, the way loop.mu's three cells do.
docs/malbolge/spec.md grew from a reference card into something you could
learn the language from, and four example programs joined it, each verified
by running it.
The new examples are nop.mal (DP, two characters, credited to Ben
Olmstead and apparently the only Malbolge program its author ever wrote),
answer.mal (mtve, 28 straight-line instructions that print 42),
hello-world.mal (the punctuated hello world that circulates today — which
prints Hello, world., not Hello, World! as Wikipedia claims), and
99bottles.mal (Iizawa et al., 2005, 22561 instructions, the program that
settled whether Malbolge can branch). The 99-bottles golden test does not
quote its 11459 bytes of output; it regenerates the song from a four-line
Lean function.
Two things in the spec are ours rather than folklore. The first is the
generated memory tail: reading the fill one trit column at a time, the
column map has exactly three cycles, of lengths 2, 2 and 3, so the tail
repeats with period 2, 3 or 6 — and since no cycle is constant, some word
in every period has a nonzero top trit, hence a value of at least 19683,
hence unexecutable. Running off the end of a Malbolge program always
hangs, within six steps. (Wikipedia says the fill has period 12; that is
the other operand order, not the reference interpreter's.) The second is
the three worked program analyses — answer.mal traced instruction by
instruction, cat.mal's five-address loop with its 9-cycle at address 37
and the jump that re-encrypts the j at 60, and truth.mal's branch,
which turns the input byte into a two-entry jump table in seven cycles.
Every claim of that kind was checked against the interpreter before it
went in, and two earlier drafts of those paragraphs were wrong.
Also folded in: the xlat2 cycle structure (six cycles, lengths
2 + 4 + 5 + 6 + 9 + 68 = 94), the 2-cycle and immutable-no-op address
tables, the store/load idioms as identities about the crazy operation,
a section on normalized Malbolge, and a fuller history — Cooke's account
of the genetic algorithm that stalled at hello wor before the beam
search worked, and the pre-2005 "99 bottles" that turned out to be a
straight-line printf of a uuencoded gzip. 1168 tests pass.
Langlib/Computability/ is for Turing-completeness results and their URM
bridges. The two correctness proofs of the hand-written Turpentine
backends were sitting there because that is where the TurpentineCompiler
vocabulary happened to be, not because they belong. They now live in
Langlib/Languages/Turpentine/Compile/Certified/, one file per target, under the
namespace Langlib.Turpentine.Certified.
Two things had to be said out loud for the move to work. First,
Compile/Certified/ is a documented Mathlib exception under Langlib/Languages/,
alongside Compile/Derived.lean and the --tc half of Main.lean: it is
proof-side, and nothing a runner imports may reach it, so the executables
still compile without Mathlib. Second, Langlib.Turpentine.Certified
nests inside Langlib.Turpentine, and Lean resolves enclosing namespaces
before opened ones, so a bare exec in the subleq proof silently became
the front end's statement executor. It is now Langlib.Subleq.exec, and
CLAUDE.md carries the warning for the next proof that moves in.
Everything else is mechanical: imports, the two test modules,
scripts/axioms.lean, and every doc link and line anchor. The axiom audit
prints Langlib.Turpentine.Certified.* and still shows nothing but
propext, Classical.choice and Quot.sound. 1164 tests pass.
Short addition completing the value-level primitive set. The crazy
operation has exactly two per-trit bijections: reading a cell through
...222 (row x = 2 of the table swaps trits 1 and 2) and writing
through ...111 (column y = 1 swaps 0 and 1). One read-write hop is
therefore the 3-cycle 0 ↦ 1 ↦ 2 ↦ 0 applied to every trit
(hop_eq_vmap), so three hops are an exact copy (hop_hop_hop): a value
moves three cells downstream without the program ever knowing what it
was. The constants restore themselves; only the source cell is consumed.
With the mux (branch_arith), straight-line rows (crazy_run), the
branch gadget, the escalator, and now data movement, every value-level
primitive a register file needs has a verified instance. What remains for
the witness is composition: a re-enterable dispatcher, the register
layout, the assembler, and the URM simulation induction.
branch_arith said seven crazy operations against computable constants
turn any accumulator into either of two jump targets. branch_gadget now
runs it: eight instructions, seven p cells executing the pipeline while
d walks the seven constants, then a movd through a pointer cell that
re-aims d at the written target. After exactly eight steps the cell
under d holds t₀ or t₁ by the flag, the accumulator holds the same,
and a frame condition says nothing else changed. The final jmp is
generic (step1_jmp) and belongs to the caller. Everything is verified
against step1, whose only bridge to the reference interpreter is
step1_sound, so none of it can drift from exec.
The lemma worth keeping is crazy_run: a row of k consecutive p
cells computes a fold of the crazy operation over the operand row, proved
by one induction, with the operand cells holding their intermediates and
the code cells their encryptions afterwards. Straight-line arithmetic of
any length is now one lemma application, not a proof per instruction.
step1_eq and per-instruction corollaries (step1_crazy, step1_movd,
step1_jmp, step1_nop), plus run?_add/run?_one for composing runs,
are the step-level kit it runs on.
Not yet done: re-enterability. A gadget executed once leaves its cells
encrypted; inside a dispatcher its orbits must cycle, the problem
loop.mu solves for three cells. That, plus restocking the spent
constants, is the dispatcher design problem, and it is the next layer.
Remaining after that, toward TuringComplete: the register
representation, the dispatcher itself, an assembler for the mod-94
layout, and the URM simulation induction.
Langlib/Languages/Turpentine/Compile/Fractran.lean: Turpentine to a
Minsky machine to fractions, reachable as --to fractran --bespoke and
tested in Langlib/Tests/CompileFractran.lean. FRACTRAN already had a
certified compiler derived from its completeness witness; what the
hand-written one buys is a fraction list short enough to read — 160
fractions for sum.turp where the derived route needs about 3,550 bytes
of them.
Three things the construction turns on, all recorded in fractran/compiler.md.
No instruction may name its own state. inc r; goto s at state s
lowers to p_r * q_s / q_s, which reduces to p_r / 1, and a denominator
of one divides every state — the rule would fire everywhere. The two
macros that want a self-loop (clearing a register, and the infinite loop a
failed assert becomes) use two-state cycles instead, and toFractions
rejects a self-reference rather than emitting one.
The epilogue is measured, not guessed. A layout reserves scratch for an expression nesting the program may never reach, and every register the epilogue clears costs a prime and two states. Compilation runs twice: the first pass only to learn the highest register the code mentions, the second to clear exactly those. It halved the output.
The answer needs no decoding. answer is register zero and gets the
prime two; every other register and every state gets an odd prime; the
final 1 / q_s consumes the state prime. So the run ends on exactly
2 ^ answer, no earlier state is a power of two, and --out pow2 prints
the answer once, in decimal.
A correction to that last claim's framing. The page had said the
certified route leaves the caller to factorise, and that is not true: its
cleanup phase reduces a halting store to 2 ^ R₀ as well, and it keeps a
control marker on an odd prime at every instruction boundary, so
--out pow2 reads certified output too. Checked rather than reasoned
about — the derived sumsq.turp prints 30 under it. What separates the
routes is size (5074 bytes against 2125 on that program), not
decodability, and the page now says so.
And primes-tc.turp finishes. The page had it filed under "a program
that will not finish"; it halts after 60,872 steps, in about three
seconds, printing 10. What is expensive there is the size of the
integers, not the number of steps — trial division makes every % a
counting loop over a state whose digits grow with the register values. The
section is rewritten around the run that actually happens.
A branch in Malbolge Unshackled is a jmp whose target cell holds a
computed address, so the whole difficulty of branching is arithmetic: turn
a data value into one of two chosen targets using only p, and do it with
the same instruction sequence in both cases, because this language
cannot choose code per-case at runtime. branch_arith settles it: seven
crazy operations against constants computed from the two targets send any
accumulator to t₀ when a flag cell holds ...000 and to t₁ when it
holds ...222.
The pipeline is absorb, load, shape. Absorb: crzTrit (crzTrit x 2) 0 = 0
for every trit x, so two operations against ...222 then ...000
forget the accumulator entirely (crz_absorb), and the second constant is
self-restoring, since the operation writes ...000 over the cell that
held ...000. Load: one operation, crz 0 flag, gives the uniform value
...111 or ...222. Shape: the three columns of the crazy table, as maps
of the accumulator trit, compose to every function {1 ↦ p, 2 ↦ q} at
depth four, and to only eight of the nine at depth three, missing
(1, 0); the cols table holds a kernel-checked witness for each pair,
and the four constants k1Of … k4Of are built from it per trit
position (map2, the tritwise combinator that crz itself is an
instance of).
Everything is constructive and runs: for targets 100 and 200 the four
shaping constants evaluate to ordinary naturals (118, 145, …), and #eval
confirms the chain from arbitrary accumulators. What remains for a working
branch is the machine half, laying the seven constants along d's walk
and jumping through the written target, which is sequencing of the kind
loop.mu's proof already demonstrates.
The Malbolge Unshackled docs said a backend should avoid * entirely,
since the rotation width is read by exactly one instruction and dodging it
buys correctness at every legal width. The width algebra, now proved,
shows that advice is right for control and wrong for storage, and the
wrong half matters more.
Three width facts (width_crz_le, width_rot_le, width_succ_le): the
crazy operation never widens a value; successor widens by one trit but
applies only to c and d, which no instruction can store; rotation is
the one operation that can widen a stored value. Assembled per instruction,
widthBounded_step1: a step that does not rotate preserves any width
bound W ≥ 13 on the accumulator and on all of memory. Bounded-width
values form a finite set, and j and i read their targets from memory,
so in a rot-free run every teleport lands in a fixed finite set of
addresses, forever. Unbounded storage cannot come from stored pointers
in a rot-free program; the only residual route is d's one-cell-per-step
walk with self-extending code at the frontier, which nobody should want
to certify.
The positive half is the escalator, also proved. Rotating the value 1
at rotation width w moves its single set trit to the top of the window:
rot_one : Value.rot w (Value.ofNat 1) = Value.ofNat (3 ^ (w - 1)), a
value of width exactly w (width_rot_one). A j through it raises
maxWidth to w and the rotation width doubles
(growRotWidth_double). Rotate 1 again and the next minted address has
width 2w: widths 10, 20, 40, … This rot/movd feedback is the
language's only supply of unboundedly many nameable addresses, which is
the mechanism-level meaning of "Unshackled", and it is the allocator of
any compiler targeting the language.
Consequence for the completeness plan, recorded in
docs/malbolge-unshackled/compiler.md: control gadgets stay rot-free and
keep every lemma proved so far; the register file cannot, so the witness
will be stated against the reference rotation policy the ProgLang
instance pins (start at 10, exact doubling), and correctness at every
legal policy is deferred. Both pages now say this in place of the old
advice.
skiComplete : TuringComplete SkiLang, axiom-clean. Both halves of the
functional route are now proved, and the second did not come free from
the first even though the two languages share their combinators. See
ski/computability.md.
What did not transfer, and why. Unlambda is call by value and SKI is
normal order, so the compiled terms are different programs, not different
spellings of one. And Unlambda has an output instruction while SKI has
none: a run's whole observable is the normal form it prints, so the answer
has to be a term. It is a tower of Ks ending in I, one K per unit,
and decodeOutput counts them.
What normal order gives back. Nothing has to be forced before it is stored, so an increment leaves the unevaluated application that computes it, a loop's branches need no guard, and the ordinary fixed point works. The register file needs no nil case either, since the counter semantics only admits indices below the bound, and that takes a binder off every cell: bracket abstraction triples a body per binder, so the cell a nil case would need costs about ten times what this one does.
The lemma the whole file rests on. hstep is the spine-only fragment
of the interpreter's leftmost-outermost step, and it commutes with
application with no side condition: applying a term to an argument can
only make a redex at the root if that term is i, k x or s x y, and
all three are head normal forms, so a term a spine step applies to is none
of them. eval_K is the only place the proof leaves the spine, and it is
what builds the answer: the normal form of k X is k applied to the
normal form of X.
Point-free combinators, checked by running them. There is no bracket
abstraction pass in the file. Every combinator is hand compiled from the
lambda expression its docstring records, and every behavioural lemma is a
fixed number of spine steps with the arguments left opaque, which rfl
checks. That works here and did not in Unlambda, because normal order never
inspects an argument it has not reached. A wrong hand compilation cannot
survive: the chain then does not reduce to the term the lemma claims.
The cost, and what the tests can therefore cover. Langlib.Ski.step
rescans the whole term to find each leftmost redex, so a run costs the size
of the term times the number of steps. The empty URM program compiles to
1004 combinators and finishes in 50 ms; a URM program with one instruction
compiles to 9121 and does not finish in twelve million steps, which take
four minutes. So the tests are in two suites: the URM one covers what runs
end to end, and a counter-machine suite covers the half that is new here,
against an executable counter interpreter, in milliseconds.
derived skiComplete is wired as turpentine exec --via ski --tc, which
makes SKI the one target in the library that reports an answer without
having an output instruction.
unlambdaComplete : TuringComplete UnlambdaLang, axiom-clean. The first
completeness result in the library that is not a machine simulation: the
target has no store and no jumps, so the argument is bracket abstraction
applied to a program written in a lambda notation that exists only inside
the proof. See unlambda/computability.md.
The counter machine is now shared. The register-machine half of the
brainfuck proof was never about brainfuck. Cmd, its big-step semantics,
and the URM-to-counter compiler with counterProgram_spec moved to
Langlib/Computability/Common/Counter.lean, leaving brainfuck with the tape
layout that is actually its own. Thue already reused them and now says so
by importing the shared module. A new backend therefore has four commands
to interpret and nothing else: increment, decrement, emit a byte, and a
while loop.
What the second half looks like. A register is a Scott numeral, the
file holding them is a Scott list with every index unrolled at compile
time, and the answer comes back in unary, one byte per unit of register 0,
which leaves nothing for the decoder to prove. Both data predicates are
behavioural rather than syntactic, because applying the successor to a
numeral gives a term that branches like m + 1 without being the numeral
literal for it.
Three things call by value forces, and they are the content of the proof rather than incidental:
- The textbook bracket-abstraction clause
[x] e = k efor anewithoutxis unsound. It evaluatesewhen the closure is built, so anethat prints prints at the wrong time and anethat loops loops unconditionally. Restricted to closed value expressions it is sound, and it is not optional: without it a Scott numeral costs3 ^ ncombinators instead of4 * n. - A loop's zero test has to wrap both branches in an abstraction and force the chosen one afterwards. Unguarded, the body runs once on a register that is already zero, and then forever.
Ydiverges, so the fixed point is the strict variant, defined as a substitution instance so that unfolding it is an identity rather than an appeal to an extensionality the equivalence does not have.
Counting the machine's own steps. Counter.lean gained EvN, the
same big-step relation with a step count, and EvN.split. The loopS
premise is a derivation for b ++ Cmd.loop r b :: cs whose two halves are
not subderivations of it, and the compiled loop needs them separately;
counting the steps is what lets the simulation recurse on a number.
And the compiler that comes with it. derived unlambdaComplete is a
verified Turpentine-to-Unlambda compiler with no backend written, reachable
as turpentine exec --via unlambda --tc. It is correct and impractical:
adding one to one compiles to 1.4 million combinators and sixteen million
machine steps, and factorial of five does not finish in two billion. The
compiler page now also carries a correction, since it used to recommend
the bracket-abstraction clause the proof has shown to be unsound.
SKI is the open half of the functional route, and Unlambda's witness does not carry over to it: SKI is normal order rather than call by value, and it has no output instruction, so its answer has to be a normal form rather than a stream of bytes.
A short follow-up with one finding, which sharpens what crz_two_steps
buys a backend. exec_crazy writes out both memory effects of a p step,
and the first is the constraint: p writes its result to mem[d], the
cell it just read the operand from, so a constant is destroyed by being
used (crazy_consumes_operand). A value cannot be built by returning to
one cell and combining against it repeatedly; every crazy operation needs a
fresh constant. The only infinite supply of constants in a loaded image is
the 6-periodic fill, which offers six values, so a loop that builds
arbitrary values has to regenerate its own constants rather than read them
off a table. That is now the sharpest open question for the backend.
The second effect explains a runtime error the test suite already had a
case for. The crazy operation writes at d and the encryption that follows
reads at c. If the two coincide, the encryption sees the result of the
crazy operation, which is essentially never a printable word, and the
interpreter crashes. c and d start equal, so a prologue has to separate
them before any arithmetic happens; rotcrash.mu is that mistake in three
characters.
The compiler page for Malbolge Unshackled argues that a backend should
avoid * entirely, since the rotation width is read by exactly one
instruction and dodging it makes a backend correct at every legal width.
That trade is only worth taking if the crazy operation alone computes
enough. It does, and the bound is exact.
crz_trit proves the operation is tritwise, at every position and in the
repeating trit, so the question reduces to nine cases of Olmstead's table.
Reading it by rows: an accumulator trit of 0 reaches 1 and 2, one of 1
reaches 0 and 2, one of 2 reaches everything. So one operation is not
enough (crzTrit_zero_ne_zero: a 0 can never produce a 0) and two
always are, because every row reaches 2 and the row for 2 reaches
everything:
theorem crz_two_steps (a : Value) {t : Value} (h : t.Normalized) :
∃ k₁ k₂, Value.crz (Value.crz a k₁) k₂ = tAny value becomes any other in exactly two p operations against chosen
constants, and the constants are computed rather than searched for. Since
a compiler owns what sits in memory, that is the primitive a data-driven
branch needs: a branch is a computed jump-table entry, and writing one
costs two crazy operations.
The supporting lemma is value extensionality, ext_of_trits: normalised
values with the same repeating trit and the same trits are equal. Without
it a tritwise argument cannot conclude an equation between values, and
stripLead and padTo both had to be shown invisible to trit first.
docs/malbolge-unshackled/compiler.md was rewritten in the same batch to
carry all of the Malbolge Unshackled findings in one place: the three
obstacles with the theorem for each, the closed-off route through virgin
memory, the rotation width reclassified from hardest obstacle to avoidable
one, the verified loop a dispatcher can be built on, and what a backend
still has to solve.
Langlib/Examples/MalbolgeUnshackled/loop.mu is a 201-cell program the
loader accepts whose execution settles into a three-step cycle, and
Langlib.Computability.Unshackled.Loop.neverHalts proves that cycle runs
for ever: at every fuel bound the interpreter reports outOfFuel, so no
halt and no runtime error, ever. It is the first LangLib theorem asserting
anything about a Malbolge Unshackled run of unbounded length.
The cycle is movd at 154, then jmp at 155 twice:
c=154 d=200 movd mem[154]=74
c=155 d=198 jmp mem[154]=70
c=155 d=199 jmp mem[154]=74 (restored)
Three cells carry it, and the reason each works is the point of the entry:
- 155 holds 37, which decodes to
jmpat an address congruent to 61 modulo 94. Ajmpnever encrypts itself, so this cell is never written for the whole run. - 154 holds 74,
movdat an address congruent to 60 modulo 94. It is encrypted twice per cycle, once by executing and once by being the first jump's target, and74 ↦ 70 ↦ 74isxlat2's two-cycle. So it is restored every pass. A cell that is both executed and jumped onto advances two orbit steps per cycle, which is what makes a two-cycle word survive, and it is the trick the whole construction turns on. - 153 is the second jump's target, encrypted once per cycle. The invariant does not track its word at all: encryption keeps a printable word printable, and printable is all this cell has to be.
The jump table is at 198 and 199, read at consecutive d, which is the
shortest spacing yesterday's gap_of_repeated_word permits; cell 200 holds
197, three below itself, which is what returns d each cycle. Designing
around that law is what made the program 201 cells rather than a handful:
the data values have to sit above 126 so the loader stores them unchecked,
and the movd residue is 60 modulo 94, so the loop cannot start before
address 154.
The proof is three step lemmas and a disjunction of three phase
predicates, each a few Memory.get equations plus the two registers.
Nothing is computed anywhere: get_set_self and get_set_ne push each
phase to the next, and neverHalts_of_invariant finishes. That is the
payoff of writing the invariant with get equations instead of memory
equality, and it is the shape an unbounded data-driven loop will need too,
where the reachable set is infinite and computation could not help.
Honest gap: the theorem covers every state in the cycle, but that loop.mu
reaches one, after a 154-step no-op prologue, is checked by running the
interpreter and by a golden test, not in the kernel. Kernel evaluation is
not a route: ten steps of run? on a loaded image takes seconds and does
not reach a normal form, because load and Memory are built on
Std.HashMap. Closing it means proving the prologue symbolically too.
No semantics were changed.
Second batch on Malbolge Unshackled, and it corrects the first. Yesterday's
entry said an unbounded loop would need cells from the long xlat2 orbits
phased so exactly one of a run fires per pass. That is not the mechanism.
The interpreter reads the word to encrypt after the instruction has run.
Every instruction leaves c where it was, so every instruction overwrites
its own cell, which is what decode_encrypt_ne makes bite. jmp has
already moved c to its target, so the encryption lands on the target and
the jumping cell is untouched. jmp is the only self-preserving
instruction in the language (jmp_cell_stable), and that is the whole
reason anything can loop. The reference semantics knew it: the comment in
Semantics.lean says the encryption is "after a jump that is the target,
never the jump itself". What was missing was the consequence.
So a loop is a stable jmp reading a table of targets while d walks
through it. Tracing cat.mu against our own interpreter shows exactly that:
from step 38 the control cycle is 37, 38, 60, 61, 61 and back, five steps,
with cell 61 firing jmp on two consecutive steps without changing, reading
its table at d and d + 1.
The full control state repeats with period 3060, after an 89-step
prologue. That is lcm 68 9 6 5 4 2, the lcm of the encryption table's
orbit lengths: the loop closes exactly when every cell it touches has come
back round. truth.mu on input 1 has period 408, which is 68 * 6. Both
are measurements against Semantics.lean, flagged as such in the docs.
Consecutive table entries turn out to be nearly forced, and that is a
theorem. Every cell of a loadable program must decode to one of the eight
opcodes at its own address, so if one target value appears at two addresses
g apart, g is a difference of two opcodes modulo 94
(gap_of_repeated_word). Only 43 of 94 gaps qualify, and of the small ones
only 0, 1 and 6. Not 2 (no_repeated_word_gap_two), which rules out the
shortest jump-table loop a compiler would reach for.
The reusable half is neverHalts_of_invariant: a set of states closed under
one iteration proves the run consumes every fuel bound without reporting a
result, restated at the language interface as image_neverHalts and
not_halts_of_invariant. It goes through step1 and step1_sound, whose
only job is to be provably the body of exec, so none of this can drift
from the reference semantics. The point of the invariant shape is that the
predicate is written with Memory.get equations rather than memory
equality, so discharging one needs neither hash-map comparison nor a long
kernel evaluation; get_set_self and get_set_ne (via LawfulBEq Value
and LawfulHashable Value) are the only memory facts required. An
unbounded loop over an unbounded counter will need exactly this shape, since
there the reachable set is infinite and computation would not help.
Still open, and now a bounded task: no P has been written down and
discharged for an actual image, so LangLib does not yet assert that any
particular Malbolge Unshackled program runs forever.
No semantics were changed. The only edits outside
Langlib/Computability/ remain the visibility of five internal helpers
(natTritsAux, padTo, succTrits, doOutput, doInput, step), which
proofs have to be able to name.
Malbolge Unshackled is one of Stage 8's open positive claims.
This is the start of it. There is no TuringComplete witness
yet and this entry does not claim one; what landed is
Langlib/Computability/MalbolgeUnshackled/Main.lean, axiom-clean, containing the
layer a witness has to be built on and the two theorems that say why the
obvious constructions do not work.
The ProgLang instance names the language: a program is a loaded Image,
the parser is the loader, the runner is evalImage at the default
configuration.
The arithmetic of addresses is proved rather than sampled.
succ_ofNat says 3-adic successor is ordinary increment on the naturals,
modClass_ofNat says the decreed residue of a natural is n % 282, and
decode_at_ofNat puts them together: the instruction a cell holds is a
function of its word and its address. exec_hang, exec_halt and
exec_step are the three exits from the interpreter's dispatch, and
exec_of_hang proves Johansen's hang never halts, never errors and never
emits.
Then the two obstructions, which are the point of the entry.
decode_encrypt_ne:xlat2has no fixed point and the 94 printable codes are 94 consecutive naturals, hence distinct modulo 94, so no cell executes the same non-nopinstruction on two consecutive executions. A loop whose body is a fixed instruction sequence is not expressible in this language. Loops have to be cycles through the encryption table's orbits, whose lengths are 68, 9, 6, 5, 4 and 2.restTable_not_printable: the 6-periodic memory fill that covers the addresses the loader never reached produces, at three of its six residues, values whose repeating trit is 1. Those are not naturals, so not printable, so executing one hangs. A program cannot walk off its own end into an infinite supply of fresh instructions. That strategy is the one thing Unshackled's infinite address space appears to offer over Malbolge, it is the first thing one reaches for, and it does not work.
The constructive half is alternatingCell, a table of eight cells built
from xlat2's single 2-cycle 70 ↔ 74, one per instruction, checked in the
kernel. Every instruction is available as a loadable period-2 cell, so
instruction choice is free; the residue is forced modulo 94, so instruction
placement is the real cost, and padding is scarcer than instructions
(only 14 of 94 residues admit a cell that both loads and stays harmless
through its whole orbit). The table also shows the phase is forced: an
alternating cell always fires on its first execution, never on its second,
which is why a loop cannot be assembled from two half-bodies of opposite
phase.
One finding is worth flagging because it contradicts the received wisdom in
docs/PLAN.md. The free choice of rotation width, described there as
something no other target has an analogue of, is read by exactly one
instruction. A compiler that never emits * never observes it, and is then
correct at every legal width rather than only at the reference minimum. The
price is that the crazy operation becomes the only arithmetic, which pushes
registers towards unary counters spread over memory cells, which is exactly
the resource Unshackled has and Malbolge lacks.
Next: loop construction from the longer orbits, phased so exactly one cell
of a run fires per pass. That is the HeLL assembler's technique and
everything else waits on it. docs/malbolge-unshackled/computability.md
has the full account, including what is cited rather than proved.
Every Piet example was straight-line — push, compute, print, stop — which
left the hard half of the language undemonstrated. Control flow in Piet is
geometry: a loop is a closed circuit through a white return corridor, and a
branch is pointer rotating the DP into one corridor or the other.
Four new programs in Langlib/Examples/Piet/, with golden tests:
count.ppm(40x3) prints 1 to 10. The first example with a cycle in it.truth.ppm(13x3) is the truth-machine, and at thirty-nine codels the whole loop skeleton is legible in one codel map.collatz.ppm(65x3) reads n and prints its hailstone sequence. The Collatz step wants a second branch and does not take one: with r = n mod 2, both cases are(n*(1+2r) + r) / (2-r), so it costs onemod, onedivand tworolls instead of a change of direction.mondrian.ppm(48x34) printsPiet, and everything below its top three rows is a painting in Mondrian's palette that the pointer never enters — which is the point: unreachable blocks cost nothing and constrain nothing.
scripts/gen-piet-examples.py lays them out, because nobody paints a loop
by hand. It implements the two codel geometries linearGrid and loopGrid
from Langlib/Computability/Piet/Main.lean — the ones the completeness proof
already uses — plus cheap constant building (a square with a correction
beats a block of n codels above about twelve). Its output is checked the
only honest way, by running the programs.
docs/piet/spec.md walks all four with their pictures;
scripts/render-docs-images.sh renders them, mondrian without --grid.
lake test is green at 1108 tests.
derivedPiet had been correct-by-construction since Piet's completeness
proof landed, and unreachable from the command line the whole time: the
backends table in Langlib/Languages/Turpentine/Main.lean had no piet
row, so --to piet was the example of an unknown target in
docs/certified-compilation.md. It is a target now.
The missing piece was a painter. The completeness proof produces a
Grid, and Grid is the parser's output type; nothing in the library went
the other way. Codel.toRgb in Langlib/Languages/Piet/Syntax.lean is the
inverse of the palette table colorOfRgb reads, Grid.toImage paints a
whole grid, and Image.toPpm3 writes it — so the emitted file is ASCII P3
PPM at codel size 1, exactly what lake exe piet reads.
Painting a codel and reading it back is proved to be the identity
(colorOfRgb_toRgb, twenty cases by rfl), which is the codel-level half
of "the image the compiler wrote is the grid it meant". The whole-grid round
trip is carried by test: Langlib/Tests/DerivedPiet.lean gained a second
suite that renders the PPM and hands it back to Piet.run, so the CLI's
actual path — codegen, renderer, parser, evalGrid — is what runs. All
982 tests pass.
The size and the speed, measured rather than guessed. A compiled
answer := 2 is a 3,516-codel image that prints 2 in about two seconds.
fact-tc.turp compiles in 1.4 s to 51135 x 3 codels and had printed
nothing after twenty minutes; sum.turp, which adds 0 through 4, compiles
to 30501 x 3 and behaves the same way. The cause is not the register
machine: Piet block-finding is a flood fill per step, so instruction cost
grows with image size while singleton normalization grows the image with the
program. docs/piet/compiler.md says so, with the numbers.
docs/certified-compilation.md needed a different unknown target for its
error example (befunge93 now) and gained a Piet block beside the FRACTRAN
one, since the two are the interesting artifact shapes: a fraction list plus
a starting integer, and a picture.
Documentation and one new script; no Lean touched.
Each of the fifteen docs/<lang>/spec.md pages now ends with an
Example programs section: three to six complete program texts in the
language, quoted in full, each with a paragraph on how to read it and what
it does. The texts are the files in Langlib/Examples/<Langname>/ wherever
one fits, and every claimed output was produced by running the program, not
recalled.
The two graphical languages show their programs. Piet's and
Brainloller's example sections now carry the rendered picture beside every
text, and each page ends with a "Rendering these pictures" subsection giving
the commands. The pictures were previously produced by hand-run commands
whose parameters lived nowhere; scripts/render-docs-images.sh now holds
them, regenerates all ten images byte-for-byte identically to what was
committed, and with --check fails if any is stale. docs/TESTING.md
records it as a third check alongside lake test and difftest.sh, and
CLAUDE.md makes "images are derived files, regenerated by that script" a
policy. Piet's "The examples, in colour" section was merged into "Example
programs" rather than left to say the same things twice.
Where a program is not text, it is transliterated and the transliteration is stated:
- Whitespace —
S/T/Lfor the three tokens, one instruction per line, with the disassembly beside it, since the real files show nothing at all in an editor. - Piet — the rendered SVG, the literal P3 PPM for
add.ppmandsquare.ppm(they are eight codels by three), a codel map writing each codel as lightness and hue, and the (hue steps, lightness steps) reading of every transition. - Brainloller — the rendered PNG, a codel map using the eight brainfuck
characters plus
↻/↺for the rotation colours, and the PPM for the three-by-threecat.ppm.
Two things turned up in the writing. docs/piet/spec.md claimed square.ppm
differs from add.ppm by two codels; it is one (0 192 0 becomes
192 255 192, turning in add into dup mul), now corrected. And the
smallest program that runs and halts in Malbolge and in Malbolge Unshackled
turns out to be two characters, QC — Q decodes as halt at address 0, and
the second character is there only because the memory fill needs two words.
CLAUDE.md records the section as policy, so new languages get one.
Three structural changes, no new language and no new compiler.
Turpentine moved to Langlib/Languages/Turpentine/. It was the one
language in the library living outside Langlib/Languages/, for no reason
except that it was written first. The namespace is unchanged
(Langlib.Turpentine, exactly like Langlib.Brainfuck under
Langlib/Languages/Brainfuck/); the module path, the lakefile's executable
root, two open private ... from module references and every documentation
link followed.
Langlib/Computability/Class.lean is gone, replaced by two modules in
Langlib/Common/ split by what they cost:
Common/Compilation.lean—ProgLang, and what it means to compile a language correctly. Free of Mathlib and cslib, deliberately, so that a hand-written backend can state and prove its own correctness without either reaching the interpreters.Common/Computability.lean—TuringComplete,BoundedStorage,BoundedRunand the decidability that follows from a bound. The one module inCommon/that needs cslib, and therefore the oneLanglib/Common.leandoes not roll up.
Derived.lean moved with the compilers it builds, to
Langlib/Languages/Turpentine/Compile/Derived.lean.
Certified compilation became generic, and acquired an I/O-aware
sibling. CertifiedCompilerNoIO spec L is parameterised by the source
specification, so agree and the new weaken are proved once for every
source and target; TurpentineCompiler L is that type at
TurpentineHaltsWith and everything already proved kept working
unchanged.
The new statement is the one the library did not have. A run's observable
behaviour is a Trace of interleaved inp/out events; a language opts
into reporting one with a TraceLang instance, subject to two laws tying
the report back to its interpreter; and CertifiedCompiler demands that
a compiled program reproduce the source's trace, under an encoding the
compiler declares as data, as well as its answer.
CertifiedCompiler.toCertified proves the behavioural notion implies the
answer-only one, so nothing already proved has to be reproved when a
backend is upgraded.
Nothing inhabits CertifiedCompiler yet, on purpose. The prerequisite is
per-language: an interpreter has to record its events. FRACTRAN got the
first TraceLang instance for free, since its run provably ignores the
input stream and TraceLang.ofInputFree discharges the side condition by
rfl. docs/PLAN.md Stage 6 sequences the rest.
lake build and lake test clean (979 tests); scripts/axioms.lean audits
the new definitions and reports the three standard axioms or fewer —
CertifiedCompilerNoIO.agree needs none at all.
A consistency pass over the documentation afterwards turned up three stale
spots, none of them caused by the refactor and all of them about which
proofs are done. docs/README.md's legend still described fractran and
piet as proofs under way; docs/agent-brief-completeness.md still told a
new agent to take those two next and listed thue as open and ook and
brainloller as uncollected; and docs/PLAN.md's Stage 8 table applied its
"PROVED" marker to four of the ten languages that have one. All three now
match the code: eight TuringComplete witnesses, three decided halting
problems, and unlambda/SKI, malbolge-unshackled and Turpentine itself
still open. Brainfuck's "bespoke correct" cell went from wip to -, since
no such proof has been started.
An audit of the fifteen docs/*/spec.md headers against the documentation
policy. Eleven already cited a reachable canonical source; the other four
cited something a reader could not follow, and Turpentine cited nothing at
all because it has no external definition.
- unlambda: named Madore's page without linking it. Now
http://www.madore.org/~david/programs/unlambda/, with the distribution
named as the file the page actually offers (
unlambda-2.0.0.tar.gz). - ski: had a bibliography with no locators. Schönfinkel and Curry now carry page ranges and DOIs.
- malbolge-unshackled: claimed a "Malbolge Unshackled page" by Johansen. There isn't one. The language is defined by his public-domain Haskell interpreter, http://oerjan.nvg.org/esoteric/Unshackled.hs (whose header dates it to Feb 2007), plus the deviations described on the esolangs page that links it as the reference implementation. The header now says so, and gains the Year field it was missing.
- brainfuck: cited
bf.tar.gzon Aminet, unlinked. The upload is Müller's own, June 1993, and is calledbrainfuck-2.lha: http://aminet.net/package/dev/lang/brainfuck-2. - subleq:
mazonka.comis down (HTTP 523 on every attempt), so the tool page now carries a Wayback snapshot beside it, and the Mazonka-Kolodin paper gets its arXiv link. - turpentine: not an esoteric language and has no upstream, so the
header now says explicitly that the page itself is the specification and
Langlib/Languages/Turpentine/the reference implementation, rather than leaving a reader to wonder what it was written against.
Every URL in every spec page was fetched: all 34 resolve except
mazonka.com, which is the one now archived.
pietComplete : TuringComplete PietLang landed, and with it derivedPiet.
The language whose programs are abstract paintings now has a
machine-checked completeness proof, and the proof is stated against
evalGrid itself: the DP and CC rules, the eight exits of every colour
block, the white slides and the halt are the ones the reference evaluator
implements, not a paper idealisation of them.
The arithmetic and the primitives landed earlier today. What closed the gap
was composition, and it went in five pieces. exec_toPivot runs the
dispatcher body: the corridor, then the switch and the pointer, which
are exactly the two commands a corridor may not contain, since they move
the chooser and the direction. The two branches out of the pivot were
already proved, so reaches_iteration is one whole turn of the loop —
corridor, pivot, pop, return corridor, back to the first codel of the
body with the chooser where it started, because the switch toggles it
once and the corridor's three blocked turns toggle it once more. exec_run
composes those over Cslib.URM.Steps, exec_entry covers the start slide
and the prologue that loads the register file, and simulation assembles
the whole thing and reads the answer out of the decimal the image printed.
Two things had to be said carefully. A program counter that is already past
the end of the source still runs one iteration, so the halted dispatcher
needed its own lemma; and the induction has to know that the intermediate
states of a halting run are not halted, which comes from cslib's
no_step_of_halted.
Also: StableCode now has a lemma per generator, which is what lets the
corridor claim anything at all about the dispatcher's own code.
975 tests. Every language in the library with a positive computational-class claim now has a machine-checked one, except Malbolge Unshackled, Unlambda and SKI, which landed as languages today and whose proofs are Stage 8 work.
Piet's completeness proof had a shape problem: the command traces were
verified against execOp, but nothing said what they computed, and the
image-level story was untouched. Both halves moved.
stackOf models the dispatcher's stack as a URM register file plus the
three control slots, and dispatchUpdate_step proves one dispatcher pass
performs exactly one Cslib.URM.Step. The argument is the masking one the
design rests on: a guard of zero makes an instruction the identity, the one
instruction the program counter selects applies its arithmetic, and J
writes its target to the fall-through counter exactly when both the guard
and the register comparison hold. runCode_dispatcherCode lifts that to a
whole iteration.
The geometry then needed one design change, and it came from a fact worth
writing down: a singleton colour block can never halt a Piet program.
Whatever codel the program arrived from is an unblocked neighbour, and one
of the eight selected exits steps straight back into it. The terminal block
is therefore an L of three codels, the smallest shape that can hide its own
entry, which also made its flood fill provable: ten worklist steps over a
symbolic grid, with the visited array tracked through three set! calls at
distinct indices. mkInfo then computes all eight exits in one simp, and
every one of them is blocked.
The white transits are proved too, including the three-turn return
corridor, whose variable-length leg carries the invariant that makes the
interpreter's revisit check fail: every remembered (codel, direction) pair
is either in another direction or strictly to the right of where the slide
now is. The three blocked turns leave the chooser toggled once, which is
exactly what the dispatcher's trailing switch was already compensating
for — the layout and the arithmetic agreed before either was proved.
What is left is composition rather than discovery, and
docs/piet/computability.md lists it: the two corridor instantiations, the
pivot, the induction over Cslib.URM.Steps, and the assembly. The image is
one column narrower than it was.
The three trees the 2026-09-01 handoff note left as "in flight and INCOMPLETE — verify before trusting: the agents died mid-task and their examples were never checked" are now finished languages. Everything was verified rather than assumed, and everything worked, which was not the expected outcome.
Each of the three gained a lakefile.toml runner, an import from
Langlib.lean, a golden-test suite in lake test, a language README, a
docs/<lang>/spec.md with its semantic decisions numbered, and a
compiler.md. Langlib/Languages/MalbolgeUnshackled.lean had to be
written; the other two root modules already existed.
The interpreters themselves needed no changes. The Unlambda machine
handles c and d exactly as its docstring claims, hello.mu prints
Hello, world! at three different rotation widths, and every example in
all three directories runs. Three test expectations of mine were wrong
before the code was: hello.mu ends with a newline, and KKSI is
((KK)S)I, which normalises to KI rather than K.
The spec pages record what the implementations already decided. The ones
worth naming: Unlambda's e exits (the two C interpreters in Madore's
2.0.0 distribution parse it as a second c, contradicting the
specification, the Java interpreter and the Scheme one); Unshackled's end
of input is ...22, which closes the output stream rather than printing
a byte; and Unshackled's encryption step can crash, because a rotated word
need not be a printable natural and Johansen's interpreter calls crash
where Malbolge would shrug. That last one has its own three-character
example now, rotcrash.mu, which is the only Unshackled program here we
wrote ourselves.
Which is the loose end. hello.mu, truth.mu, cat.mu and Unlambda's
quine.unl arrived with those unfinished branches and their authorship was
never recorded. They run, but Malbolge's own examples credit Cooke and
Scheffer by name and these credit nobody. Both READMEs say so and ask for
the attribution.
972 tests.
thueComplete : TuringComplete ThueLang landed, and with it
derivedThue, so the string-rewriting language now has a certified
Turpentine compiler like the machine-shaped ones. Post settled the
mathematics in 1947; what was missing was a check that a deterministic
interpreter following a particular strategy cannot wander off the
intended derivation, and that is where the work went.
The generator, the encodings and the rule-family separation lemmas were already in place. Three things closed the gap.
The first is small and does all the load-bearing: a phase token plus the
one character next to it determines which rule applies, because every
canonical family reads exactly one adjacent cell (reaches_phase_right_cell,
reaches_phase_left_cell). Combined with the unique @ marker, that turns
Thue.firstMatch — a search over a thousand rules and every position in the
string — into a function on represented states.
The second is reaches_exec, which lifts a whole big-step counter-machine
derivation to a run of the generated rules. Rule availability travels as a
subset of generate done code suffix, which shrinks on every step except
Ev.loopS, where the continuation becomes body ++ loop :: rest. That case
needed generate_append: generation is compositional in the code it
traverses, so unrolling a loop asks for no rule the loop did not already
generate. The macros it consumes are reaches_inc (which was already there),
reaches_dec, both sides of reaches_zeroTest, and reaches_emit.
The third is dispatch. reaches_finish seeks the counter holding
nextProgramCounter + 1, counts it down to nothing (which also clears it,
restoring the invariant the next macro needs), picks the destination, and
walks the token home. outcomes_functional, proved earlier, is what makes
the pick deterministic: several outcomes can share a unary count, but then
they name the same program counter, so they are the same rule.
Halting came out as the mirror of the control step. firstMatch_eq_control
says a source control marker selects that instruction's entry rule;
firstMatch_control_none says that once the program counter has run off the
end, no generated rule matches anywhere in the string, which is exactly
Thue's halting condition. The final state is then printed by
Config.finalState and read by decodeOutput_encodeState.
Two smaller things fell out. The left-moving return scan is now stated for an
arbitrary phase, so backPC reuses it and reaches_back_across and
reaches_back_home became corollaries. And the three counter scans share one
reaches_scan_prefix.
scripts/thue-cost.lean replaces the scratch runner the notes referred to,
so the sizes in docs/thue/computability.md are reproducible: the
one-iteration addition program is 1,211 rules, a 17-character initial state
and exactly 1,665 rewrites.
885 tests.
The finite-control count from earlier this week said nothing about a
step, so it settled nothing. It does now: malbolgeHaltingDecidable
decides halting for every loaded Malbolge image, which is the form
incompleteness takes in this library.
Three pieces, and only the first was the one the notes predicted.
Langlib.Malbolge.exec recurses at the front and returns early on a halt,
so exec (n+1) is not step (exec n). stepOnce is the loop body with
the recursive call replaced by "stop here" (exec_one is rfl), advance
makes halting absorbing, and exec_succ supplies the missing law.
RunWF is the invariant a reachable state satisfies, and runWF_exec
carries it through the whole run. This is where the arithmetic lives:
rotR, crz, encrypt, a read byte and maxWord each need their own
bound, plus Array.set! size preservation.
The configuration drops the output, because it grows without bound and no
instruction reads it, and config_ext proves the 59049-word control
determines the rest: memory by array extensionality, registers and cursor
by Fin injectivity, the input data because the run fixes it.
Two things fell out on the way. First, BoundedStorage demands its
finiteness laws of every inhabitant of the configuration type, which
Malbolge's input-dependent cursor cannot satisfy; but reading
halts_iff_search shows it only ever uses them at reachable
configurations. So Class.lean now also has BoundedRun, with the laws
stated there, the pigeonhole proof moved to it, and
BoundedStorage.toBoundedRun keeping every existing witness and
Deadfish.no_boundedStorage true as stated.
Second, the module could not be imported into a compiled executable at
all. deriving Fintype on MalbolgeCore produces a top-level value,
evaluated when the module loads, and enumerating 59049^59049 memories
overflows the stack immediately. Both Fintype instances are now
noncomputable, which is why Langlib/Tests/BoundedMalbolge.lean could
finally be wired into lake test, where it had never run.
718 tests.
compileToURM and its correctness theorem landed, which was the piece
everything else waited on, and with it Langlib/Computability/Derived.lean:
the TurpentineCompiler interface, the derived construction, agree,
and derivedWhitespace. All axiom-clean.
The payoff is the one the design promised. derived takes any
TuringComplete L and returns a verified compiler, so applying it to
subleqComplete gives a certified Turpentine-to-subleq compiler with no
subleq-specific work at all; checked by instantiating it and auditing the
axioms. Two languages now have a certified compiler, and any language
proved complete from here gets one for free.
668 tests.
The second completeness proof, and the easy one, exactly as predicted: a URM register is one subleq memory cell holding the value directly, because subleq words are arbitrary-precision and memory is unbounded. No encoding, no range cap, and not one lemma in the file carries a range side-condition.
The interesting part is the J instruction. The URM tests equality;
subleq branches on <= 0. Since registers hold naturals, equality is two
<= tests on the same difference in both directions, which comes to nine
subleq instructions. Straight-line instructions set their branch target to
the next instruction, so the branch is invisible and no sign reasoning is
needed for Z, S or T.
One documented trade: decodeOutput counts output bytes rather than
parsing decimal, because subleq's only output primitive is a single byte
and decimal printing would need a division routine plus a self-modifying
digit buffer, out of proportion to the claim. Byte-counting is a total
function of the output and invents nothing; the cost is output size.
18 differential tests, axiom-clean, 631 tests in the suite.
docs/certified-compilation.mdwritten: the pipeline (Turpentine -> URM -> target), the fragment it can accept, a dependency graph with dashed arrows for planned work, and the order of construction. Everything hangs off one missing piece,compileToURM, because the second arrow is free: it is thecompilefield of a language'sTuringCompleteinstance, which exists as soon as somebody proves that language complete.- Verified compilation is being modelled as a bundled
TurpentineCompiler Lstructure, not a class, precisely because we want several compilers per target coexisting (a derived one and an effective one) and instance resolution is built to pick exactly one. Agreement between any two instances is then a theorem about the interface rather than a testing practice. - Recommendation recorded: keep both kinds of compiler. The effective
whitespace backend compiles
gcd.turpto 532 bytes and accepts the whole language including I/O and negative integers; the derived one will be orders of magnitude larger and accepts only the I/O-free non-negative fragment. Neither subsumes the other. scripts/axioms.leanadded, closing a gap from the previous report: it prints the axiom dependencies of every completeness result, since a theorem resting onsorryAxtype-checks perfectly well.
The first entry in the TC proved column.
Langlib/Computability/Whitespace/Main.lean compiles cslib's unlimited
register machine into Whitespace and proves the compilation simulates,
yielding whitespaceComplete : TuringComplete WhitespaceLang.
#print axioms reports only propext, Classical.choice and
Quot.sound: no sorryAx, so the theorem is real. Since the URM computes
every partial computable function, so does Whitespace.
It is an instance of the uniform interface from Stage 8 rather than a
one-off theorem, so the next language states its result the same way and
the negative results will use BoundedStorage alongside it.
Not yet done for it: docs/computability.md, and the differential test
suite that runs compiled URM programs on our Whitespace interpreter. The
agent died before writing either.
Where things stand for whoever picks this up next. lake build and
lake test are green (582 tests), and ./scripts/difftest.sh passes 14
comparisons against four reference interpreters.
Done: eleven esoteric languages with specs, interpreters, runners,
examples and tests (brainfuck, whitespace, malbolge, befunge93, subleq,
fractran, thue, ook, deadfish, piet, brainloller); Turpentine with
arrays; three compiler backends (brainfuck for scalars, whitespace and
subleq for the whole language); the website under site/; cslib and
Mathlib as dependencies; a compiler.md for every language.
In flight and INCOMPLETE at the time of writing. Three agents were
still working, so the following are partial. They compile and do not
break the suite, but they are not finished, not wired into
Langlib.lean, Langlib/Tests/Main.lean or lakefile.toml, and have no
docs pages yet:
Langlib/Computability/{Class,URM,Whitespace}.lean: the proof is finished and axiom-clean (see the entry above). What is missing isdocs/computability.mdand a test suite.Langlib/Languages/MalbolgeUnshackled/,Langlib/Languages/Unlambda/andLanglib/Languages/Ski/: all four modules of each exist and the tree builds, with example directories started. None has tests, docs, alakefile.tomlentry, or an import fromLanglib.lean, so none is wired in and none has been run end to end. Verify before trusting: the agents died mid-task and their examples were never checked.
To resume: finish or discard the three items above, then continue
with docs/PLAN.md. The open stages are 5 (Velvet examples), 6
(verification proofs, nothing proved yet), 8 (computational class, the
TC proved column in docs/README.md is all no) and 9 (derived
compilers). Stage 4 still wants the IR layer (StackIR, TapeIR, RegIR),
which is what makes the Stage 6 proofs affordable.
Known loose end: Befunge-93 is not parametric over cell width. It
hard-codes unbounded Int cells, which is why the language we implement
is Turing complete while bef.c's byte-celled one is not. Making the
width a Config option would let both computational-class claims be
proved about one implementation, and would let the differential tests run
faithfully against bef.c. See docs/befunge93/spec.md.
- The brainfuck backend lands, the hard one, covering the scalar language. Integers are 16-bit two's complement in two cells each, chosen by measurement rather than taste: 8 bits cannot run collatz on 27 (peak 9232) or sumdigits on 9045, and 32 would double every cost for range nothing uses. The load-bearing trick is a division-by-two that computes quotient and remainder in one linear pass with a constant-size body, so byte comparisons are linear instead of quadratic; without it nothing runs. All eight scalar examples compile and match the interpreter, and collatz(27) prints 111 through 367 kilobytes of generated brainfuck. Arrays are not supported yet, and each of the six array constructs reports its own name when refused.
turpentine exec --via brainfuckworks, with the--eof zeroconvention the backend requires wired in. 582 tests.
- The status matrix now separates TC known from TC proved. The first is what the literature or our spec page argues, and can be wrong; the second is a machine-checked theorem in this repository, with a link. Every entry in the second column is currently empty, which is the honest state of things and the point of having the column.
- The table also records that a language cannot host a full compiler unless it is Turing complete. Malbolge gets a bounded fragment, not a planned full compiler: it has 59049 words for code and data together, so no total translation from a Turing-complete source can exist. Same for befunge93 (2000 code cells) and deadfish (no loops).
- Fixed a misattribution that had spread to three files: Malbolge Unshackled is Ørjan Johansen's (2007), not Matthias Lutter's. Lutter wrote HeLL and the first Malbolge quine.
- Malbolge Unshackled and Unlambda are being implemented, the latter to give the library a completeness argument by bracket abstraction rather than machine simulation.
- cslib and Mathlib are now dependencies, reversing the
dependency-free stance. The reason is duplication: without cslib we
would define our own register machine and Turing machine and then
re-prove the relationships cslib already has. The pinned revision
3951377eis the last one on Lean v4.33.0, matching our toolchain exactly, so nothing had to be upgraded to accept it. Mathlib stays confined toLanglib/Computability/so the interpreters keep compiling fast. - Marked as future work: restate the completeness results to reuse cslib's own machine-model equivalences, so we only ever prove simulations and borrow the rest. Deliberately not done yet, since the shape of our simulation statements has not settled.
- Stage 9 planned: derived compilers. A completeness proof already contains a verified compiler (from a register machine into the language), so composing it with one Turpentine-to-register-machine compiler yields a verified Turpentine compiler for every language proved complete, without writing a backend. That makes Stage 8 infrastructure rather than scholarship, gives the hard targets (thue, fractran, piet, malbolge) a compiler at all, and provides a test oracle for the hand-written backends.
- The plan is explicit that derived compilers are correct and unusable,
and that effective compilers stay separate: hand-written, practical,
and separately verified, with observational agreement between the two
falling out as a corollary rather than a third theorem. The I/O gap (a
register machine has none, Turpentine does) is stated up front with the
preferred resolution, a
URM+IOextension with an embedding from the plain URM.
- Array codegen in both backends, so whitespace and subleq again
accept the entire language. Whitespace gets arrays nearly free: the heap
is integer-addressed and an address is an ordinary stack value. Subleq
cannot name a computed address at all, so the backend does what subleq
has always done and patches its own operands before executing them:
an indirect load rewrites the
Afield of the very next instruction, and an indirect store patches three operand words. That is the reason insertion sort and a sieve run on a one-instruction machine. - Both check bounds and route to their existing traps, using a distinct forbidden address from the assert trap so the two failures stay distinguishable. 149 compiler tests (up from 105), 508 in total.
- Two corrections that came out of being challenged on the claims, both
worth recording as findings rather than typos:
- Malbolge: I had it as an open question and its compiler as "not planned". Both wrong. Malbolge is a bounded-storage machine (59049 words of 59049 values), so it is decidably not Turing complete; the open questions are about Malbolge-T and Unshackled. And people do compile to Malbolge (Iizawa et al.'s method, Lutter's HeLL), so a backend is planned, via a VM written in Malbolge whose data cells never execute and therefore never self-encrypt.
- Befunge-93: the classical "not Turing complete" claim is about
bef.c, whose playfield ischar pg[80*25]and whose stack is an unbounded-depth list ofsigned long: finite control plus a finite-alphabet stack, which is a pushdown automaton. Our implementation stores unboundedIntin both, which makes the playfield 2000 unbounded registers and the language Turing complete. The deviation was documented; its consequence was not. Both claims are now stated, and Stage 8 plans to prove the pair.
- Stage 8 gains a uniform interface: a
ProgLangclass (namedEsolanguntil 2026-09-01), aTuringCompletestructure bundling compiler and simulation, and aBoundedStoragestructure with the decidability theorem proved once, so the negative results are short instances rather than separate developments. The cslib connection is staged: mirror its URM now, bridge in aproofs/package later, keepLanglibdependency-free.
- Toolchain pinned to Lean 4.33.0 (down from 4.33.1). Everything builds and all tests pass; the downgrade also puts us on the toolchain Verso tags, should the site ever want it.
- Arrays in Turpentine: fixed-length, one-dimensional, bounds-checked.
Velvet's
MaxElemandInsertionSortare ported, plus a sieve. - Computability becomes a stated goal. Stage 8 of the plan gives every language a claim about its computational class and a route to a proof, against cslib's Turing machine and unlimited register machine. The README says so, CONTRIBUTING spells out what counts as evidence, and the status matrix carries a Turing-complete column. An SKI/Unlambda entry is planned so the library also has a completeness argument by bracket abstraction rather than machine simulation.
- An IR layer is planned (Stage 4): StackIR, TapeIR, RegIR, one per target family, so lowering passes and simulation proofs are shared rather than repeated per language.
- Every language now has a
compiler.md, describing what was built where a backend exists and a concrete plan where one does not, including arguments for why a general backend is the wrong thing to build for befunge93 (80 by 25 playfield) and malbolge (self-encrypting code). - The website landed:
site/, its own Lake package, 21 pages generated from the docs, three in-browser playgrounds (brainfuck, whitespace, deadfish) verified under node.
- Stage 4 opens for real: compilers from Turpentine to whitespace and
to subleq, both accepting the entire language rather than a
fragment. All eight Turpentine examples compile on both backends and
produce output identical to the reference interpreter, with one
documented exception (
cat.turpon whitespace, which cannot test for end of input and so dies there by design). - The interesting gaps are recorded rather than hidden: whitespace floors
its division while Turpentine is Euclidean, so the backend emits a
sign-correction sequence, checked against the reference on all 361
operand pairs in -9..9. Subleq needed none of that, its
-1EOF convention matching Turpentine exactly. lake exe turpentine compile --to <lang> [-o out]wires the backends into the runner. 445 tests, all passing.
- Piet and Brainloller landed, the graphical pair.
Langlib/Common/Image.leanadds an RGB image type and a PPM reader (P3 and P6) shared by both. Piet implements the colour wheel, DP and CC with the eight-attempt rule, white sliding per the 2004 clarification, and the 17 operations; blocks are flood-filled once so each step is constant time. Brainloller decodes pixels into the brainfuck core and also ships an encoder, so--encodeturns any brainfuck program into a picture. - 340 golden tests, all passing. Eleven languages plus Turpentine.
- Malbolge landed: the loader with its validity check, the ternary crazy
operation, rotate, and the post-execution encryption table, all verified
against a locally compiled
malbolge.c. Where Olmstead's spec text and his interpreter disagree (output and input opcodes are swapped in the text, non-printable words spin rather than halt), the interpreter wins, as the community holds. Cooke's 2000 hello world and Scheffer's cat run. - Differential testing now covers brainfuck (Cristofani's sbi), befunge93 (Pressey's bef) and malbolge (Olmstead's own): 13 cases, all passing.
- 296 golden tests.
- The front-end language WTF is renamed Turpentine (
.turp), after the solvent for a Turing tarpit; the pun is explained indocs/turpentine/spec.md. Everything moved:Langlib/Languages/Turpentine/, moduleLanglib.Turpentine.*, examplesLanglib/Examples/Turpentine/, runnerlake exe turpentine, docsdocs/turpentine/. - Thue and Befunge-93 landed (27 and 46 tests). 277 tests, all passing.
- Differential testing works for real:
scripts/get-references.shfetches and builds reference interpreters into a gitignored.difftools/(Pressey's bef so far), andscripts/difftest.shprefers them over PATH. Befunge-93 now passes 4 differential cases against bef v2.25. docs/verification.mdwritten: the shared correctness statement, per-backend proof structure, proof order, and a scoreboard.- Stage 4 in flight: compiler agents for Turpentine to brainfuck, whitespace, and subleq.
- Layout: language implementations moved under
Langlib/Languages/(module names gain theLanguagessegment; Lean namespaces stayLanglib.<Langname>). Turpentine stays atLanglib/Languages/Turpentine/as the front end. - Runners: no longer block reading a terminal stdin (empty input instead);
new
--verboseflag reports how a run ended. - Stage 3 (Turpentine) core implemented: deep-embedded AST with loop annotations,
lexer + recursive-descent parser, type checker, pure fuel-based
interpreter (unbounded ints, Euclidean
/%, short-circuit booleans, line/byte I/O), runner withrun/checksubcommands, 8 examples (isqrt and sumdigits ported from Velvet), 32 golden tests,docs/turpentine/spec.md. - Stage 2 fan-out: parallel agents implementing the remaining languages.
Landed so far: fractran (24 tests; PRIMEGAME prints primes via
--out pow2) and subleq (27 tests; Mazonka's-1I/O convention, label assembler). Total test count: 104, all passing. Still in flight: whitespace, malbolge, ook+deadfish, thue, befunge93, piet+brainloller. - Docs: README lists implemented languages and shows how to run programs;
docs/README.mdis now a status matrix (parser / interpreter / Turpentine compiler / verified compiler per language);docs/TESTING.mddocuments the golden-vs-differential policy per language; examples that read input carry usage lines in comments.
- Layout revision per project owner: everything lives under
Langlib/(no separateEsolangfolder); example/test subfolders are capitalised; the front end is spelled Turpentine.docs/ALTERNATIVES.mdrenamed todocs/RELATED.md; the dead wolflo/esolang-semantics link replaced by the live parent repo (ellisonch/esolang-semantics). - Plan additions: Piet and Brainloller confirmed as a graphical second wave; "Java Generics are Turing Complete" (arXiv:1605.05274) added to the roadmap; Brainfuck/Whitespace/Malbolge confirmed as must-haves.
- Stage 2 started: shared infrastructure (
Langlib/Common/: pure fuel-based execution model, input stream, runner scaffolding, golden-test harness) and the brainfuck exemplar (AST, parser with positioned bracket errors, zipper-tape semantics with three EOF conventions, runnerlake exe brainfuck, 9 examples, 21 golden tests, spec pagedocs/brainfuck/spec.md, differential-test script skeleton).
- Stage 0: repository scaffolded. Lake project on Lean 4.33.1, single
library
Langlib(esolangs,Common,Turpentine,Tests,Examplesall under theLanglib/folder), test driver stub. README, CLAUDE.md (project policies), CONTRIBUTING, Apache 2.0 LICENSE, .gitignore, docs skeleton (PLAN, PROGRESS, ROADMAP, RELATED). Initial language set chosen (see PLAN Stage 1).