Repository navigation
[v0.4] #27 stage 6: complete the closed Rust AST so the runtime is generated from the calculus #91
Description
Child of #27; plan in this comment, re-scoped by the maintainers' decision of 2026-10-14 (#27 comment). Delivery stage 6; depends on the authoring surface (stage 3 of #27).
Decision
The earlier proposal on this issue (declare the trusted Rust runtime a "minimal typed adapter") is withdrawn. The maintainers' decision: the closed AST is to be completed so that the runtime is generated from the calculus. There is the LexLean way and all others, and the LexLean way is chosen. No handwritten Rust text remains in the compiler's output.
Scope
Today the Rust a package contains is handwritten text: calculus/rust/runtime.rs (680 lines for the rust-core and rust-std profiles, recorded by SHA-256 in language/semantics-1.2.toml), the file headers printed by calculus/rust/ast.rs, and calculus/rust/package.rs (manifest template, lint allow-list, provenance). The closed AST (RustSyntax, 880-line printer) can express generated function bodies but not the runtime: the runtime uses constructs the AST does not have. Completing the AST means the runtime is itself a LexLean program (a runtime profile of the calculus) rendered by the Rust backend, and RustSemantics and the preservation library cover every construct it uses.
Constructs the current runtime text uses that the AST lacks (surveyed in runtime.rs after #81):
- checked and wrapping integer operations as primitives (
checked_add,saturating_sub,wrapping_rem,try_from,ok_or, bit shifts with range checks), the width families generated bymacro_rules!; - a
staticwork counter over an atomic with relaxed ordering (tick,work); - shared immutable buffers
Rc<str>,Rc<[u8]>, andRc<Vec<T>>, withRc::get_mut(unique-holder test),Vec::truncate,Vec::push,Vec::with_capacity, slices and iterators; - generic structs and enums with trait impls (
Clone,Dropwith an iterative release loop, theSameandKeytraits for equality and order); - closures passed by reference (
&mut dyn FnMut) for the list walk; - UTF-8 and decimal parsing/printing through the standard library (
from_utf8,to_string,charcounting).
Proposal (staged; each stage is a PR with its own proofs)
- Inventory and boundary. Fix the exact set of Rust constructs the runtime profile may use (a closed list, not "whatever the text uses") and which standard-library functions are primitives of the machine (their semantics stated in
RustSemantics) versus constructs the runtime defines. The unit of trust is then a short list of machine primitives, each with a stated meaning, instead of 680 lines of text. - Scalar runtime. Arithmetic, comparison and conversion items expressed in the AST over machine primitives; generated and differentially tested against the committed runtime on every fixture (byte-identical printed result is not required, equal behaviour and equal counted work are).
- Memory model.
Rc, a unique-holder test, a growable buffer, and the static counter inRustSyntax/RustSemantics, with the refinement statements that makeList,StrandBytesimplement the calculus values (a run is a prefix of its buffer, no write to a buffer another holder can see,cons/unconsconstant work, iterative release does not overflow). - Runtime as source. The runtime profile authored in
.lex.tex, rendered by the Rust backend, hashed into the package provenance andsemantics-1.2.tomlas today; the handwritten text, the printed headers and the manifest/lint tables become generated or data; the old text moves totests/as an oracle until the differential and counted-work gates (TC-07,RB-06,RB-08) pass on the generated runtime, then is removed.
DoD
- No handwritten Rust runtime text remains in the compiler; a gate fails on a planted runtime text constant (in the manner of
VR-19). - Every runtime item has its refinement statement kernel-checked; certificates A, B and E accepted on every committed root with the generated runtime.
- Counted work of every fixture is unchanged;
rust-corestill allocates nothing. - No semantic ID, eligibility verdict or certificate statement of the calculus changes; the compiler-semantics ID changes as the runtime's content address does.
Acceptance criteria
- Planted defects each turn a named gate red: a runtime item with no refinement statement, a refinement statement for a wrong item, a changed body caught by the differential, a write to a shared buffer, a reintroduced text constant.
- Full
just vv.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTodo
Activity
[-][v0.4] #27 stage 5: the Rust runtime and package text generated, or declared as the minimal typed adapter[/-][+][v0.4] #27 stage 6: complete the closed Rust AST so the runtime is generated from the calculus[/+]humuhumu33 commentedon Oct 14, 2026
I'd like to take stage 1 (inventory and boundary) of this issue, unless someone already has it.
It needs no library change, so it can proceed while #93 is in flight; stages 2 to 4 stay where the delivery order on #27 puts them. Stage 1 is also on #84's path: #84's new constructs (a borrowed local
&T,fold_listover the defunctionalizedFn<n>by reference,Vec::with_capacity) should land in the AST this stage bounds.Plan, in the manner of the stage designs on #27 and #93:
runtime.rs(and the headers inast.rs, the manifest and lint tables inpackage.rs) uses thatRustSyntaxlacks, and a proposed closed list of machine primitives, each with its meaning.A draft of (1) is ready. Two points I'd like the maintainers' view on when it lands: whether the work counter is a machine primitive or a
staticinRustSyntax, and whetherSame/Keystay as two closed traits.Generated by Claude Code
humuhumu33 commentedon Oct 14, 2026
#91 stage 1: inventory and boundary of the Rust runtime
Measured on
mainatd284b165(after #81 / PR #88). Sources:crates/lexlean/src/calculus/rust/{runtime,ast,package}.rs,compiler/src/RustSyntax.lex.tex,compiler/src/RustSemantics.lex.tex, SPEC §17.16 and the Rust machine paragraph of §17.What exists today
The runtime is two text constants:
CORE(72 lines, both profiles) andSTD(220 lines,rust-stdonly), each recorded by SHA-256 inlanguage/semantics-1.2.toml.Generated code reaches it only through
Item: 50 variants, 144 functions once each width is counted (36 width-free, 13 per width × 8 widths, plus 4 signed negations).RustSyntax(the formal AST) mirrorsast.rswithout origins: 21Exprconstructors, 17RustTypeconstructors, 3ItemDefconstructors (enum,apply,function).RustSemanticsevaluates every item as the calculus primitive it realizes (itemPrimitive), on erased values: anRcor box is the value it holds.RustSemanticshas no work counter: counted work is measured by the differential (TC-07,RB-06,RB-08), never proved.Outside the runtime, handwritten text also lives in
ast.rs::print(crate headers#![no_std],#![forbid(unsafe_code)]) andpackage.rs(Cargo manifest template,ALLOWED_LINTS, provenance).Every construct the runtime text uses, against the AST
Constructs the AST already has: literals,
matchwith tuple/option/result/ordering patterns,if,let, calls,!,Ok/Some/Noneconstruction,Rc::new(asbox),.clone(),i128::from(aswiden),&Tas a type.Constructs the AST lacks, by kind:
useof fixed pathscore::cmp::Ordering,AtomicU64,Ordering as MemoryOverflow(Clone, Copy, PartialEq, Eq, Debug)R<T>staticatomicWORK,tick,workStr,Bytes,Node<T>,List<T>Repr<T>::Run { buffer, len }Same,Key; impls for scalars,Ordering,(A, B),Str,Bytes; manualClone;Dropscalar!implwith&self/&mut selfmethodsStr,Bytes,Listpub modfixed_u8…fixed_i64macro_rules!scalar!,fixed!,decimal!equal<T: Same>,compare<T: Key>,append_list,length_list,index_list,slice_listwhile,while let,for,loop) withbreak/returnwalk,onto,chars,Same/KeyforStr,Drop,canonical_decimalwalk,onto,items,chars, the counting and slicing items,join,canonical_decimal&mutborrows andstd::mem::replaceList::take&mut dyn FnMutwalkand its 5 callers.0,.head,buffer[len - 1],&a.0[s..e]if let, match guardscons,Same for Strascastsas u64,as usize,as u128usize,u128,i128,char,&str,&[u8],Vec<T>,Rc<str>,Rc<[u8]>STDProposed boundary
The rule: a library function is a machine primitive only if the runtime cannot state it in the AST without a loss of meaning or safety. Everything else becomes runtime source in the AST.
Scalar primitives (per width u8…u64, i8…i64, plus
usize,u128,i128where the runtime needs them):checked_add,checked_sub,checked_mul,checked_div,checked_rem,checked_neg,checked_shl,checked_shr.&,|,^,!on integers;==,<,<=,cmp.try_frombetween integer widths andfrom(widening).Derived in the AST from those (no longer primitives):
saturating_subchecked_sub, else0wrapping_rem(int_rem,b != 0)checked_rem, else0(MIN % -1is the onlyNone, and wrapping gives0)if amount < BITS { Some(wrapping_shl) } else { None }checked_shl(it isNoneexactly whenamount >= BITS; same forshr)ok_or,unwrap_or,map,map_err,and_then,copiedmatchonOption/Result&&,||ifMemory primitives (
rust-std):Rc::new,Rc::clone,Rc::get_mut(the unique-holder test),Rc::try_unwrap(iterative release),Vec::with_capacity,push,truncate,len, indexed read.Text primitives:
core::str::from_utf8(validation) andstr::as_bytes, plusRc<str>/Rc<[u8]>construction from a slice.Everything else on text is stated in the AST over bytes. Character count is the count of non-continuation bytes.
Strequality and order are byte equality and byte order, which equal code-point order on valid UTF-8.split_exact,join, decimal parse and format become digit and byte loops over the checked primitives.These must reproduce today's tick counts exactly (#91 DoD: counted work unchanged).
Same for Strticks once per character pair plus once at the end, so the byte version ticks per non-continuation byte.Work counter: one primitive,
tick(units)(andwork()), meaning "addunitsto the run's counter". Its two-line implementation over a relaxedAtomicU64is the trusted part, so the AST needs neitherstaticnor atomics.Eliminated, not added:
macro_rules!: the backend emits each width's instance.&mut dyn FnMutand closure literals:walkbecomes a cursor loop in each list item.The closure by reference #84 needs is different:
fold_list(&closure, …)borrows the defunctionalizedFn<n>enum and calls itsapply. The AST already has&Tas a type andapplyas an expression; #84 needs&Ton locals, not trait objects.Data, not text: the crate headers become a field of
Profile. The manifest template andALLOWED_LINTSbecome typed tables printed by the AST printer. This is #91 stage 4; it is listed here so the boundary is complete.What the AST gains under this boundary
implblocks,CloneandDropimpls, a closed derive set, a closeduselist.breakandreturn, mutable local and assignment, field read, indexed read,&and&mutborrow of a local,ascasts between the integer types listed.usize,u128,i128,Vec<T>,&[T],&str, type parameters.rust::validaterule replacing the text testevery_runtime_loop_ticks: every loop body callstick, except a loop inside aDropimpl.Exprchanges its recursor, so every library theorem matching onExprgains a case (afflom's note on [v0.4] perf(backend): borrowed projections, fold as a runtime item, and list views as borrows in the rust-std machine (follow-up to #81) #84). This is why stage 3 (memory model) waits on the core-module surface ([v0.4] #27 stage 3: the LexLean authoring surface: a human-writable .lex.tex syntax, an SDK API, and no other authoring path #93 stages 3c/3d) and stage 1 does not.Open questions for the maintainers
Same/Key: keep two closed traits with generated impls (smaller change), or replace with per-type generated functions so the AST stays trait-free (larger change, no dispatch)? I recommend the closed traits, sinceItem::Equal/Comparealready take no type.staticatomic inRustSyntaxas [v0.4] #27 stage 6: complete the closed Rust AST so the runtime is generated from the calculus #91's scope lists?pub mod fixed_<w>: keep the module paths (no change to any rendering's call sites) or flatten tofixed_<w>_<op>? I recommend keeping them.RustSyntaxandRustSemantics(compiler/src/*.lex.tex) once feat(surface): the language-1.2 authoring notation for semantic modules (#93, stage 3a of #27) #94 and feat(#93): every language-1.2 source is written in the authoring notation (AU-04), fmt migrates the data form #96 merge. But stage 2 already adds a constructor (a primitive callee, or the scalar items as AST functions), and the DoD asks for a kernel-checked refinement statement per item. Both touch the preservation library, which is core-module data and editable only after [v0.4] #27 stage 3: the LexLean authoring surface: a human-writable .lex.tex syntax, an SDK API, and no other authoring path #93 stages 3c/3d. Is the intent that the new statements are semantic-module theorems while the existingExprcases are regenerated, or does stage 2 wait for 3c/3d?Not in this inventory
lower.rs,moves.rs,validate.rschanges beyond the loop rule.Generated by Claude Code
afflom commentedon Oct 15, 2026
Answers for stage 1 (@humuhumu33), as a maintainer, so #102 can leave draft.
Same/Key: keep two closed traits with generated impls.Work counter: a machine primitive, not a
staticinRustSyntax. Its meaning ("addunitsto the run's counter") is the whole of the trusted part.fixed_<w>modules: keep the paths.What stages 2 to 4 wait on. The binding constraint is not the notation. [v0.4] #27 stage 3: the LexLean authoring surface: a human-writable .lex.tex syntax, an SDK API, and no other authoring path #93 stage 3c (merged soon: feat(#93): the calculus and the Rust machine are sources; the authoring API; no writer owns a product source (AU-07, AU-08) #103, feat(#93): core modules in the authoring notation, with names (AU-09..AU-12); the language-1.2 libraries are written in it #104) makes the libraries readable and editable as text; it does not make an inductive type of the preservation library extensible. Adding a constructor to
Expr(orCorr) changesExpr.rec, and every row that Lean derived from it (casesOn,below,brecOn,noConfusion,sizeOf) and every proof that Lean's equation compiler produced against it is exported data: a person can read it, but regenerating it means re-running Lean's elaborator on Lean source, which is exactly what we do not accept as a source, and no surface of ours reproduces those constructions. My scope for [v0.4] #27 stage 3: the LexLean authoring surface: a human-writable .lex.tex syntax, an SDK API, and no other authoring path #93 3d (named binders and labels for kernel terms, no inference; see the [v0.4] #27 stage 3: the LexLean authoring surface: a human-writable .lex.tex syntax, an SDK API, and no other authoring path #93 comment) lets a person write a new closed proof term; it does not rewritebrecOntowers. So please design stage 2 on this rule: no new constructor ofExpr,Corr, orItemthat a library row or proof matches on. Scalar items as AST functions over existing constructors and machine primitives satisfy it if the primitives are table data ofitemPrimitive(a semantic module, so authorable and kernel-checked with the generated Lean), and new refinement statements are semantic-module theorems about the generated runtime.One thing I do not know and want measured before stage 2 is written down: does adding an
Itemconstructor break a library row? Please spike it on a scratch branch: add one dummy constructor toIteminRustSyntax, regenerate, and run the library compile (conformance_sp_12builds it) andcheck-calculus. If nothing in the library matches onItem, stage 2 proceeds as above. If something does, the options are (a) make the primitive set a table lookup rather than an inductive (the machine's primitive list is then data, not a type), or (b) decide that extending the library needs the full elaborator, which is its own issue with its own trust argument and I would rather not start it for this. Stage 3 (loops, locals, borrows,Rc,Vec) addsExprconstructors by definition, so it hits the same wall one level up; it waits for that answer and I will not pretend otherwise.Everything else in the boundary table is agreed. Merge #102 when its checks are green.
Generated by Claude Code
feat(rust): close the runtime's library boundary (RB-09, stage 1 of #91)
humuhumu33 commentedon Oct 15, 2026
Spike: does a new
Itemconstructor break the library? Yes. Measured onmainat514e95ad.What I ran. I appended one constructor,
spike, toItemincompiler/src/RustSyntax.lex.tex, and added aspikearm, copied from thenatAddarm, to each of the seven exhaustive matches onIteminRustSemantics.lex.tex. Then I built thecompilerproject. The generatedTargetSyntaxandTargetSemanticswere byte-identical tolanguage/preservation-1.2/modules/, andRustSyntaxdiffered by the| spikeline alone. Last, I compiled the library's generated Lean against the four machine modules under Lean 4.32.1, up toRustCorr.Result.
RustCorrItem.spikeThe first refusal:
The seven errors after it are
unknown constantcascades (rebuildTail.match_14,directPat,uninh.match_1,Corr), declarations the module never reached. So the compile names the first break, not every break.check-calculusis not informative here: it can only report that the shipped modules differ from the regenerated ones, which is the edit itself.Every break, read from the native data. I walked each declaration's type and value in the 13 core modules (9,247 declarations) and counted the ones that eliminate each type directly, through its
rec,casesOn,ctorIdx,noConfusion,below,brecOnor amatch_of its own:RustSyntax.ItemRustCorr13,RustSound10,RustLemmas8)RustSyntax.CalleeRustSyntax.ExprRust.CorrRustSyntax.RustTypeThe
Itemrows areplainItem's matcher, splitter and sparse cases,itemOperands_eq,takes_of_typedandsem_primWiden. This count is a lower bound. The library also applies matchers that Lean generates forRustSemanticsdefinitions, such asrunItem.match_1,runItem.match_3andwidened.match_1(23 equations). Those matchers change shape when a matched type grows.What this means for the stages.
Itemconstructors. Your option (a) is the way through: the primitive set becomes a table lookup thatitemPrimitivereads, rather than an inductive. That needs one more change than the spike, becauseplainItemandrunItemwould have to stop matching on the individual primitives. I have not measured that change.RustType, sousize,u128,i128,Vec<T>,&[T]and&stradd no library break.Exprconstructors, with 64 direct and 295 affected declarations, so it waits on the full-elaborator decision, as you said.The spike is on a local branch, not pushed. The analysis script reads
lexlean/core-module/2data and I can attach it.Generated by Claude Code
humuhumu33 commentedon Oct 15, 2026
A third route for extending the machine: regenerate what Lean derives, author only the proofs. This follows the spike above, measured on
mainat514e95ad.Why neither (a) nor (b) as stated.
Iteminductive changesItemonce, so it hits the same 31 declarations once. It also does not help stage 3, whose loops, locals and borrows areExprconstructors.nat_addhas to turn theNoneofchecked_addintoErr(Overflow), and the only source of failure inRustSemanticstoday is a fallible item called with?. So even the scalar stage needs one new construct, a raise.The observation. The loader already trusts Lean's derivation generators without trusting Lean source.
backend/core_v2.rs:187-213rebuilds every stored sparsecasesOnwithLean.Meta.mkSparseCasesOnand refuses the stored row unless its type and value are exactly what Lean makes. That is the spike's failure: the storedplainItem._sparseCasesOn_1is no longer "the sparse casesOn the elaborator makes" for the newItem.Most of each break is derived. I split the declarations that eliminate each type by whether Lean derives them (matchers, splitters, sparse
casesOn, theireq_N/else_eq,below/brecOn) or someone authored them:ItemitemOperands_eq,takes_of_typed,sem_primWidenLitCtorCalleePatExprRustType,CaptureReadProposal (c). Growing a machine type is one PR with three parts:
casesOnalready works this way. Matchers, splitters and their equations would get the same treatment: rebuilt from the stored matcher record over the grown type and compared, never taken from Lean source. This extends a trust the loader already places in Lean's meta code. It adds no elaboration of Lean source.casesOnover the grown type.Itemand 295 forExpr. The real number is what step 1 leaves failing, and I would measure it before writing step 2.What it buys. For
Item, three proofs. For stage 2's raise, oneLitor twoCtorproofs. For #84's borrowed projection or a loop, the 15Exprproofs plus theCorrtemplate work. That is bounded and known in advance, instead of re-elaborating the library.First step, if you agree: a prototype of part 1 for
plainItem.match_1alone, run against the spike'sItem. It answers two things: whether a matcher can be rebuilt from its stored record, and how many of the 31 still fail afterwards. I would not start part 2 before that number is in, or before 3d can state a closed proof term.Generated by Claude Code
humuhumu33 commentedon Oct 15, 2026
Route (c), measured: growing
Itemleaves two proofs to write. This is the prototype the proposal above offers, run onmainat514e95adwith the spike'sItem.spike.What I ran. I used the spike's regenerated
RustSyntaxandRustSemantics, and the library's generated Lean for all 13 modules. In a scratch copy only, I changed the core-module decoder in three ways:casesOnthatmkSparseCasesOnmakes differently is accepted and logged, instead of refused.The whole library then built under Lean 4.32.1 (all 13 modules, 135 s), so every failure is counted, not only the first.
Result.
casesOnremade by Lean (plainItem._sparseCasesOn_1,plainItem.match_1.splitter._sparseCasesOn_2)mkSparseCasesOnconstruction, which only has to keep what it makesRustSemantics.itemPrimitive.match_1.eq_1..eq_50and its splitter,plainItem.match_1.eq_2,plainItem._sparseCasesOn_1.else_eqitemOperands_eq,takes_of_typedItem.casesOnThree things the static count got wrong:
plainItem.match_1,itemPrimitive.match_1) load unchanged against the grownItem. So do their splitter abbreviations once the sparsecasesOnunder them is remade.sem_primWidenusesItem.ctorIdxbut still checks.accepts_of_typed,runItem_eq) load, because the proofs' statements do not change. So the 31 affected declarations come down to 2 proofs plus derived rows.What this means for the route.
Itemconstructor costs: the decoder keeping what Lean's derivation makes, for sparsecasesOn(done in this prototype) and for matcher equations (next), plus two one-case proofs in the 3d surface.Itemmatchers here.Expryet. Its static estimate was 15 authored declarations; by the same mechanism the real number is likely smaller. I can run the same measurement with a dummyExprconstructor next.Caveats.
RustSemanticsarms copynatAdd, so a real constructor's arms and proofs differ in content, not in kind.Generated by Claude Code
humuhumu33 commentedon Oct 15, 2026
Route (c), measured for
Expr: 11 proofs to write. This is the same prototype as theItemcomment above, run with one dummyExprconstructor,spike, whoseevalarm returnsstuck. Measured onmainat514e95ad; the whole library built under Lean 4.32.1 in 80 s.ItemExprcasesOnthe decoder remakeseq_def,else_eqrc_litValue.match_1)The 11
Exprdeclarations:RustBase:stableRustLemmas:rc_negate,rc_negate_halt,rc_read,rcb_flat,foldStep_specRustMatches:ctorOf_eq,loadOf_spec,rc_partRead,rebuildArg_rc,rebuild_valueTen are theorems whose proof fails at one
Expr.casesOn, so each needs one new case. Their statements do not change, so their dependents load. The eleventh,stable, is a structurally recursive definition: its value is built onExpr.brecOn, so the new case goes into its recursion, not only into a case split.Two things the
Itemrun did not show:rc_litValue.match_1has no wildcard covering the new constructor. So part 1 of route (c) has to rebuild a matcher from its stored record, as well as keep the remade sparsecasesOnand equation rows.Exprconstructor. So is production: the collection and iteration primitives are realized by self-recursive functions but registeredrecursion = false; a root overflows the stack at about 40k iterations #105's step 2, the template tail call realized as a loop.Since #104, part 2 can be written. The core-module notation of §17.21.8 (
AU-09..AU-12) states a closed proof term with named binders, asauthored_eq_symmshows. So the 2 + 11 new cases are writable in the surface today. The open work is part 1: the decoder keeping what Lean's own generators make (sparsecasesOn, matcher equations and splitters, and the one matcher).The decision this needs from the maintainers: is route (c) accepted for #91 stages 2 and 3, and for #105 step 2? If yes, I will start with part 1 as its own PR, with a register row, scenario, failing test and plants: the decoder admits a derived row only when it equals what Lean's construction makes for the current inductive, and refuses any other change. Then one PR per new construct, with its cases written in the notation.
Generated by Claude Code
Merge pull request #102: close the runtime's library boundary (RB-09,…
afflom commentedon Oct 15, 2026
Stage 1 is merged (#102, merge commit 70de63d); stages 2 to 4 stay open on this issue.
Stage 1 as this issue scoped it ("inventory and boundary: a closed list of what the runtime may take from Rust's library, and which of it are machine primitives"):
calculus::rust_librarylists the 21 machine primitives, each with its meaning, and the 79 library names the runtime text takes, each with one of three roles;RB-09(conformance_rb_09) refuses a name the table lacks, a row the text does not use, a row citing a non-primitive, and a primitive no row cites, with the plants recorded inVERIFICATION.md. It changes no rendered byte, package, golden or semantic ID (rb_07confirms).just vv, all four host legs,reproandhonestyare green on the merged head, and the register is at 380 IDs.What remains of the DoD, unchanged: the primitives' meanings stated in
RustSemantics(stages 2 and 3, where the scalar items and the memory model use them), the runtime generated from the calculus with no handwritten Rust text (stage 4), and the refinement statements kernel-checked. Stage 2 waits on the spike #102 names: whether adding anItemconstructor breaks a library row. #105 step 2 (template self tail calls as loops) waits on stage 3'sloopconstruct.Generated by Claude Code
humuhumu33 commentedon Oct 15, 2026
On stage 2 waiting for the #102 spike: the spike is answered. Adding an
Itemconstructor leaves the matchers loading unchanged. Lean remakes 2 sparsecasesOnand 53 derived equation rows, and only 2 authored proofs need a new case (itemOperands_eq,takes_of_typed); forExprit is 11 (issuecomment-6190972859, issuecomment-6198533402).I have since measured whether the decoder can take the derived rows from Lean instead of from the source. I realized every row whose name Lean reserves (matcher equations and splitters,
eq_N,eq_def,else_eq) through Lean's own reserved-name action. That reproduces 7,065 of the library's 7,071 derived rows exactly, once the decoder installs each row's metadata as it adds the row. The last 6 share one known cause: a sparsecasesOnnot yet visible to Lean when the next row is realized.Unless you object, I am taking stage 2 on route (c), in this order:
RustSemantics.Itemproofs, written in the core notation.If stage 2 changes whether the Bytes primitives reduce in the kernel, the PR will say so.
1 remaining item
fix(#91): the harness compiles shipped modules with their source root
afflom commentedon Oct 16, 2026
@humuhumu33, a scheduling question, not a design objection. #91 stages 2 to 4 are now the longest serial chain to v0.4: #92's boundary waits on stage 4, #105 steps 2 and 3b on stage 3's
loop, and everything after is CI-bound (about two hours a landing). Most of the other v0.4 lanes now have a second contributor session (#89 stage 3 certificate A, the #92 pilot). Could #91 have one too?Which slices could run in parallel with yours without colliding on
RustSyntax/RustSemantics, as I read the DoD and your order (decoder, scalar items, the twoItemproofs, then stage 3, then stage 4):calculus/rust/package.rs(manifest template, lint allow-list, provenance) and the file headerscalculus/rust/ast.rsprints. This touches neitherItemnorExpr. The open question is what "data" means when.lex.texis the only authoring surface: I would read it as a language-1.2 module (a typed record of the manifest and the lint set), rendered by the Rust backend like any other, but that is a decision for you and me before anyone builds..lex.tex(stage 4 proper) cannot start before stage 3's constructs exist, so it stays behind you.Exprconstructors (loop, mutable local, borrow) and the memory model collide with the scalar items in the same inductives. I would not split those.So the proposal is slice 1 only, as a contributor session on a branch of its own, draft, stacked on main; nothing from it is on your path. If you would rather do all of #91 yourself, say so and I will not start it. If you agree, tell me whether the "data" reading in 1 is right, and anything about the headers or the provenance I have not seen. Also welcome: your estimate of the number of PRs and landings left in stages 2 to 4, so the plan on #15 has real numbers.
Generated by Claude Code
fix(#91): a certificate rendered outside the module system leaves the…
fix(#91): certificates-total-limit records the longer decoder's bytes
fix(#91): a row private to another module is data
Revert "fix(#91): a row private to another module is data"
fix(#91): the text library's private names name the module it compile…
chore(#91): regenerate the records the text library's private names move
chore(#91): regenerate the records #160 brings under the old language…
humuhumu33 commentedon Oct 17, 2026
Yes to slice 1 as a parallel session. It touches no inductive of mine. What it does share with my lane is the language-1.2 identity. Answers to your three points follow, measured on
mainat243f0c73.(a) Slice 1: agreed, with one coupling to plan for
The two lanes share no inductive, but they do share the ID.
rust_rendererhashes all seven files ofcalculus/rust/, includingast.rsandpackage.rs.schemas/rust-provenance.schema.jsonis embedded. So every slice-1 landing moves the language-1.2 ID, and so does every PR on my chain. Whichever lands second merges main and regenerates every lock, golden and package. #155 has merged main five times since it was marked ready, most of them with a regeneration, and a sixth is in progress now. This does not argue against the split. It argues for three things:package.rs, the header lines ofast::print, the provenance schema, and the new module. Notruntime.rs,lower.rs,RustSyntaxorRustSemantics.printkeeps splicingruntime::CORE/STDuntil stage 4. My stage 2 will editast.rsandlower.rsoutsideprint, so the textual merges should be clean.recursion = false; a root overflows the stack at about 40k iterations #105 3b. On production: the collection and iteration primitives are realized by self-recursive functions but registeredrecursion = false; a root overflows the stack at about 40k iterations #105 we agreed (issuecomment-6203599898) that therust-provenancerecord of a root's effects goes with the next change that moves the ID anyway. That was to be production: the collection and iteration primitives are realized by self-recursive functions but registeredrecursion = false; a root overflows the stack at about 40k iterations #105 step 2, which now sits behind my stage 3. Slice 1 already edits the provenance writer and its schema, and it lands well before stage 3. The other half of 3b should go with it: theproduction-1.2.tomlsentence that says recursion "stops the realization with an explicit failure", when in fact the process aborts. I am glad to hand 3b to that session if you agree.(b) "Data" as a language-1.2 module: yes, with four refinements
A language-1.2 module is the right reading. Decision 1 on #27 makes
.lex.texthe only authoring surface, so a Rustconsttable (today'sALLOWED_LINTS) is a second surface. A typed record also lets a property be stated as data and checked, rather than restated in prose. Four things I would settle before building:lib.rs. The Rust backend renders programs to Rust source.Cargo.tomlis TOML. Sopackage.rskeeps a canonical TOML printer over the record. That leaves a printer, likeast::print, but no text constant holding manifest content, which is what the DoD and its planted-constant gate are about. Nothing incrates/lexlean/srcreads values out of acompiler/srcmodule today, so how the renderer obtains the record is new and needs design. The two options are evaluating the module, or decoding its semantic data. This is the first design point for that session.language/andschemas/, notcompiler/src/. Today a lint change moves the ID becauserust_rendererhashespackage.rs. After the move, the module's committed data must sit underlanguage/(as the generated machine modules do underlanguage/preservation-1.2/modules/), or its hash must be recorded insemantics-1.2.tomlbesiderust_runtime_core. Otherwise a changed lint set would not change LexLean's identity.#![no_std]is exactlyallocation = falseon the target row oflanguage/production-1.2.toml. The header should be derived from that row, so the fact lives in one place. This corrects my stage-1 inventory, which proposed a new field ofProfile. Also,#![forbid(unsafe_code)]inlib.rsrestatesunsafe_code = "forbid"inCargo.toml's[lints.rust]. I would keep both in the output (alib.rsbuilt outside Cargo still forbids unsafe code), but generate both from that one lint row. One caveat: deriving fromproduction-1.2.tomlkeeps a TOML source in the chain. Whether the target table itself becomes a module is a separate question that slice 1 should not take on.correctnessgroup."ALLOWED_LINTShas no group field, and as far as I can find, no gate binds the SPEC table to the code. A record of{lint, group, reason}, withgroup ≠ correctnesschecked, makes that claim checked rather than prose. The ten load-bearing plants recorded in VERIFICATION.md then become "drop a row from the module".Provenance needs nothing beyond 3b. It is already computed values under a schema (
lexlean/rust-provenance/1), with no handwritten content other than the key set the schema fixes. Itsruntimehash stays as it is until stage 4.(c) PRs left in stages 2 to 4
This is a count with its uncertainty, not a schedule. It counts PRs after #155, which is in flight. The low end groups constructs into fewer PRs; the high end is one PR per construct, as I proposed on route (c).
RustSemantics, and the twoItemproofs (itemOperands_eq,takes_of_typed) in the same PR, since a PR that growsItemwithout them fails the library compile; the raise construct (oneLitor twoCtorproof cases) with them or on its ownrc_litValue.match_1); theExprconstructs (loop withbreak/return, mutable local and assignment, borrows, field and indexed read, casts) at about 11 proof cases each by the dummy measurement; the item kinds (structs, enums with fields,impl,Clone/Drop,Same/Key); the memory-model refinement statements forList,Str,Bytes.lex.texwith the old text moved totests/as the oracle (core and std together or apart); then removing the oracle and adding the planted-text gateSo 7 to 13 serial landings on the #91 chain, plus slice 1 in parallel. Every one of them moves the language-1.2 ID, so each also costs every other open ID-moving PR a main merge and a regeneration.
Where the range comes from, from most to least uncertain:
cons/uncons, iterative release) are new theorems. They are not cases added to existing ones.impls and traits areItemDefconstructors, and I have not spikedItemDefagainst the library. I will run the same dummy-constructor measurement before stage 3, as I did forItemandExpr.Exprconstructor came from a dummy whoseevalarm isstuck. A real constructor's proofs differ in content, and may need more cases than the dummy did.I will post the
ItemDefnumber here when I have it, so the plan on #15 can take the stage 3 range down.Generated by Claude Code
chore(#91): regenerate on the combined language-1.2 identity (main 24…
afflom commentedon Oct 17, 2026
Slice 1 (package scaffolding as data): first design point, how the renderer obtains the record. Contributor session, branch
train/package-scaffold; I take hh's four refinements as written and #105 step 3b with them.Decision: decode the module's semantic data; do not evaluate it.
semanticmodule,RustPackage, written in the notation atlanguage/package-1.2/RustPackage.lex.tex. It declares aLintRow {lint, group, reason}with a closedClippyGroupinductive, the rustc gate rows (unsafe_codeforbid,warningsdeny), the Clippy base gate, the[package]/[lib]values (edition, publish, library path), and the allowed lints as a list ofrecords.package.rsreads it from the embedded tree: the notation is read to its canonical data (surface::module, asidentity::data_formalready does for the ID), and a small decoder walks the definition bodies, which are closed (record,cons,nil,ctor, string and boolean literals). Anything else in a body is refused, so the decoder cannot silently read less than the module says. The module is read, never rendered intolib.rs, andpackage.rskeeps only a canonical TOML printer over the record.language/, the ID covers it with no registry row:identity_bytesalready contributes the data form of a.lex.texthere. A gate shows it: one lint row changed in a scratch copy of the embedded tree changes the language-1.2 ID, and a comment-only edit of the notation does not.#[no_std]iff the target row oflanguage/production-1.2.tomlhasallocation = false;#[forbid(unsafe_code)]is printed for every rustc gate row at levelforbid, the same row that printsunsafe_code = "forbid"inCargo.toml.group != correctnessis checked twice: by the decoder, which refuses a row in that group (a registered failure), and by a theorem in the module itself over the same list. SPEC §17.16's lint table is compared with the rows by a gate.Alternative rejected: evaluate the module. The shipped crate has no evaluator of semantic terms. The meaning of those terms is decided by the generated Lean and its kernel, so a second interpreter in
lexleanwould be an unverified reading of the language it ships, to learn what a closed list of strings is. Decoding costs a reader of constructor, list and literal shapes, and refuses every other shape. The two cannot disagree on data that shape.Alternative rejected: keep the module in
compiler/srcand commit a generated copy underlanguage/. It adds a regeneration path and a comparison gate for a file that is already the data; the hand-written source underlanguage/is itself what the ID hashes.#105 3b rides in the same PR as agreed on #105 (comment 6203599898):
lexlean/rust-provenance/1gainseffects(the root's declared effects on that target, ascending; same set as PX-13), and therecursionsentence oflanguage/production-1.2.tomlstates the abort. Nothing else of #105 moves.Hand-written work only goes to the branch; every generated tree is left to the train's one regeneration.
Generated by Claude Code
Merge pull request #155 from UOR-Foundation/claude/issue-91-derived-rows
train: regenerate every record the combined tree moves
fix(rust): address review of the package scaffold (#91 slice 1)