Skip to content

fix(backend): lower explicit shared byte literals without widening limits - #68

Merged
13 commits merged into
mainfrom
fix/explicit-shared-byte-regressions
Oct 18, 2026
Merged

13 commits merged into
mainfrom
fix/explicit-shared-byte-regressions

Conversation

@afflom

@afflom afflom commented Oct 11, 2026 •

Copy link
Copy Markdown
Member

Purpose

A form change to the Lean spelling of modeled byte literals, with an independent oracle for it. It is not a performance or capacity fix: measurements are in VERIFICATION.md ("explicit byte-literal lowering can fail") and say so.

What the lowering changes

  • Every octet is written _root_.UInt8.ofNat (nat_lit n) (all 256 values), inside _root_.ByteArray.mk #[...]; the empty array is _root_.ByteArray.mk #[].
  • An octet occurring four or more times in one literal is bound once by a deterministic let __byteN, scoped to that literal; no outer generated local is captured.
  • The canonical document keeps the original decimal numbers and order (ByteArray.mk #[...]).
  • Same IR, input language, limits and generated-Lean ownership. Only crates/lexlean/src/backend/semantic.rs changes in the compiler.

Tests

  • SM-19 (languages 1.1 and 1.2): every generated literal is read back by an independent closed-grammar reader (no Lean, no renderer), the document display is checked, the module verifies, and the generated definitions are evaluated against the original bytes.
  • MD-02: the same reader over every embedded artifact byte.
  • Reader controls refuse foreign, noncanonical and unused syntax and a binding whose name disagrees with its value; three renderer unit tests cover empty/all-256, the four-occurrence boundary and outer-local capture.

Measured (Lean 4.32.1, x86-64 Linux, one run per size, seeded-random bytes with all 256 values)

  • RED: with main's lowerer unchanged, conformance_sm_19 fails at its first fixture (the empty array) because the reader rejects the old spelling. It is not a Lean or size failure; the 1024-byte corpus elaborates under main's rendering at the default depth.
  • Default maxRecDepth: the largest literal that elaborates is 6394 bytes under both renderings (6409 fails under both). Time at 4096 bytes: 2.0 s both; at 6394 bytes: 4.4 s (main) vs 3.3 s (explicit).
  • maxRecDepth 100000: 4096 bytes 3.0 s vs 2.1 s; 16384 bytes 12.7 s vs 11.3 s; 65536 bytes both fail on heartbeats (37.1 s vs 49.2 s).
  • The lowering does not move the largest elaborable literal. No improvement beyond these numbers is claimed.

Status

  • This head is hand-written work on current main only. Generated outputs are not regenerated here; the maintainer's single sweep (with feat(semantic-ir): byte and fixed-width widening conversions in language 1.2 (SM-32, #114) #151) regenerates them, and check-golden is expected to fail until then.
  • lean_backend identities that should advance: language 1.1 (9) and 1.2 (19). Language 1.0 does not move (it has no semantic modules; no committed output contains a byte literal). The bump is part of the sweep.
  • The earlier CI maintenance mode and capture scripts are dropped; the repository's own writers regenerate.
  • Item 6 (fresh PrismPM native/Wasm/installed-SDK qualification) is an adoption gate performed by the owner in the PrismPM repository, recorded here; it is not run by this PR.
  • The full gate (just vv) runs in CI on the integrated tree after the sweep.

🤖 Generated with Claude Code

https://claude.ai/code/session_014jPy3EVAUhMVS5f13tr9Ka

@afflom

afflom commented Oct 11, 2026

Copy link
Copy Markdown
Member Author

Corrected the MD-02 representation regression in 0865df4. The independent, closed-expression oracle now compares every embedded artifact byte, including explicit UInt8 literals and local octet sharing; it rejects opaque, unbound, out-of-range, noncanonical, forged, and duplicate definitions. All original MD-02 digest, manifest, theorem, LaTeX, negative, and resource assertions remain. Independent source review, the new rejection test, complete MD-02, formatting, and strict package Clippy passed locally in the pinned tools container. Fresh full CI is required before acceptance; the previous host-matrix failures remain failures, not evidence for this head.

Alexander Flom and others added 10 commits October 13, 2026 19:37
…tput

The base gained tests/tooling/{commands,core,traversal}, each publishing its
generated tree under its own expected/ directory, after this script was
written. Its scope list and generated-root discovery were constants that
predate them, so the captured patch could not reconstruct the result tree.
Derive the tooling roots the way the example roots are derived.
Rebased onto main and regenerated with the repository own writers rather than
the branch original capture: locks for every example project and the compiler,
the golden oracles, the calculus modules, the negative fixtures locks (the six
protected admission fixtures skipped), the fixtures, and the normalized
verification records.

The tree is the writers output: check-golden reports every published artifact
equal to its oracle, check-calculus reports the embedded modules a fixed point
across the second compiler build, check-fixtures reports every fixture equal to
its expected files, and the branch own capture harness reports a complete,
reconstructible generated tree with no non-generated path changed.

Every difference against main is a digest, a digest-bearing name, a position
that moved because the rendering grew, or a byte-literal respelling; the only
substantive movement is the externals swap the lowering implies
(UInt8.instOfNat out, UInt8.ofNat in, one for one).
@humuhumu33
humuhumu33 marked this pull request as ready for review October 13, 2026 21:22
@humuhumu33
humuhumu33 force-pushed the fix/explicit-shared-byte-regressions branch from 0865df4 to c384e62 Compare October 13, 2026 21:29

afflom commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

Recorded under L0.4 of the completion plan #106 (open pull requests reviewed against R3–R6 and merged or closed).


Generated by Claude Code

afflom commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

L0.4 of #106: where this PR stands against main (26e4dea7), and what I measured. I have not changed the branch.

Not obsolete. main still renders a Bytes literal as ByteArray.mk #[1, 2, …] (crates/lexlean/src/backend/semantic.rs), so the lowering this PR changes is as it was. The hand-written part is small (backend/semantic.rs +136, the SM-19 tests and semantic_ir/bytes.rs, the ci.yml maintenance mode and its two scripts); the other ~2,100 files are outputs regenerated against a base of 11 October, all stale now. Landing it means redoing the sweep on current main for the languages whose lean_backend it bumps, and a 3-hour CI cycle.

What it buys, measured. Before moving language identities for it I timed the two renderings in the pinned Lean 4.32.1 on a scratch file, one def holding a random-byte literal (all 256 values occur, so the shared let __byteN bindings apply), set_option maxRecDepth 100000, one run each on one machine:

bytes ByteArray.mk #[n, …] (main) explicit octets with shared bindings (this PR)
4,096 8.4 s 4.3 s
16,384 19.3 s 18.8 s
65,536 54.5 s, then an error 88.1 s, then an error

At the default maxRecDepth both forms fail at 16,384 with maximum recursion depth has been reached. So in this experiment the new form is about twice as fast at 4 KiB, equal at 16 KiB and slower at 64 KiB, and it unblocks no size the old form cannot do. That is a micro-benchmark, not PrismPM's corpora, and the errors at 64 KiB are ones I did not chase; it shows the benefit is not general, not that there is none for a given corpus.

What I need to decide. The PR says its purpose is to unblock PrismPM corpus generation and claims no speedup. If a PrismPM corpus fails today with the old lowering, the reproduction (the corpus or its size and shape, and the error) is what justifies the identity change; with it I will rebase this onto main, regenerate, and drive CI, preserving the commits and authorship, and open the PR from an issue per the repository's practice. Without it I recommend closing this and opening that issue to hold the requirement, since a change to lean_backend regenerates every language-1.1 and 1.2 identity for no measured gain. It stays open until then.

The scratch files and the timings are not committed; they are reproducible from the table above.


Generated by Claude Code

afflom commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

Maintainer triage at 18:05Z. This PR (the PrismPM byte-literal lowering) is dirty against main and has no CI on a current base; its description still lists the generated outputs and the full qualification as pending, and ~2,100 of its files are outputs regenerated on a base of 11 October. It is not part of the v0.4 release path (no v0.4 issue, no milestone). I am not changing it now. Decision needed from the owner, in order of preference: (1) say whether PrismPM still needs this lowering; if yes, the sweep is redone on current main after the identity-moving v0.4 and v0.5 PRs land, by the maintainer; (2) if not, close it with the reason. Silence is not a decision: I will ask again when #158 and #146 have landed.


Generated by Claude Code

Generated outputs take main's side and are regenerated by the maintainer's sweep;
the CI maintenance mode and capture scripts are dropped.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jPy3EVAUhMVS5f13tr9Ka
The unit test follows Render's lean_imports field; SM-19's evaluation compiles
the shipped runtime the generated module imports. VERIFICATION.md records the
red against main's lowerer and the measured Lean timings.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jPy3EVAUhMVS5f13tr9Ka

afflom commented Oct 17, 2026

Copy link
Copy Markdown
Member Author

Owner decision, 2026-10-17: #68 is completed as part of v0.4, with the other open pull requests. This replaces my triage note of 18:05Z on the 16th (which asked whether PrismPM still needs the lowering); the answer is yes.

What that means, concretely (contributor session session_014jPy3EVAUhMVS5f13tr9Ka is on it):

  1. Hand-written part first. The lowerer (backend/semantic.rs), the independent closed-grammar oracle and its mutation plants (semantic_ir/bytes.rs), SM-19 and MD-02 are ported onto current main by hand. The ~2,100 generated files of this branch are on a base of 13 October and are discarded; they are regenerated, not merged.
  2. The ci.yml maintenance mode and the two capture scripts are dropped. The repository's own writers regenerate everything on the pinned toolchain (every identity move of this wave was done that way), and §9.4/RP-14 admit no extra workflow mode or condition form; ci.yml is also being rewritten for Proposal: the PR gate verifies UOR Atlas by main's signed attestation, not by re-proving it (§9.4) #131 (ci: a plan job decides what a run is — draft PRs run rest, a merge of a verified head runs nothing (#131) #166), so a 221-line addition could only conflict. If PrismPM's custody really needs a generator-custody mode, say so on this PR and it comes back as its own change.
  3. The sweep is one pass, with the other identity-moving PRs. feat(semantic-ir): byte and fixed-width widening conversions in language 1.2 (SM-32, #114) #151 (widening conversions), feat(models): decode IEEE-754 bit patterns as exact dyadic rationals (MD-13, #118) #150 (dyadic decoding), certificate A (feat(preservation): certificate A as kernel-term data (stage 3 of #89) #157) and B (feat(preservation): certificate B as kernel-term data (SP-16) #163) are being integrated with fix(backend): lower explicit shared byte literals without widening limits #68 into one branch: one regeneration, one CI run, then each PR is merged as its commits land. That replaces five sequential regenerations (about two hours of CI each).
  4. Measured before claimed. On 15 October I timed both renderings in the pinned Lean 4.32.1: 4 KiB: 8.4 s on main vs 4.3 s with this lowering; 16 KiB: 19.3 vs 18.8 s; 64 KiB: 54.5 s then an error vs 88.1 s then an error; at the default maxRecDepth both fail at 16 KiB. The session reproduces this on current main and runs SM-19 against the unchanged lowerer to show whether the large-corpus fixtures genuinely fail there; the PR description is made to say only what is measured.
  5. Item 6 of "Required completion" (fresh PrismPM native/Wasm/installed-SDK qualification) is performed by the owner in the PrismPM repository, so it is recorded here as an adoption gate and is not a merge gate.

Generated by Claude Code

@afflom afflom added this to the LexLean v0.4 milestone Oct 17, 2026 — with Claude
@afflom
afflom marked this pull request as draft October 17, 2026 16:44
…both renderings

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jPy3EVAUhMVS5f13tr9Ka

afflom commented Oct 17, 2026

Copy link
Copy Markdown
Member Author

DoD checklist at 22663a7c0 (main 243f0c734 merged in; generated files take main's side, not regenerated):

  • 1 RED, unchanged main lowerer: cargo test -p repo-conformance --test conformance conformance_sm_19 panics at bytes.rs:248 on the empty-array fixture (expected closed byte syntax "_root_.ByteArray.mk #["). Genuine assertion, but the reader rejecting the old spelling, not a Lean/size failure (1024-byte corpus elaborates under main at default depth).
  • 2 Generic literal lowering only (backend/semantic.rs); IR, language, limits unchanged.
  • 3 Identities: lean_backend for 1.1 (9) and 1.2 (19) should advance; 1.0 not. Bump and regeneration belong to the maintainer's sweep.
  • 4 conformance_sm_19 passes with Lean 4.32.1 (82 s), conformance_md_02 passes, the oracle controls (2) and renderer unit tests (3) pass.
  • 5 cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, xtask validate-model (403 ids), validate-spec-links, check-test-inventory pass. check-golden fails as expected (models/Recognizer.coverage.json) until the sweep.
  • Measurements and largest-elaborable bisection (6394 bytes under both renderings at default depth) are in VERIFICATION.md.
  • Item 6 (PrismPM qualification) is the owner's adoption gate.

Generated by Claude Code

afflom pushed a commit that referenced this pull request Oct 17, 2026
…icit byte-literal spelling (#68); lowering-size-limit is a wide match; SPEC cost table

#68 writes a bytes literal's octets as `UInt8.ofNat (nat_lit n)`. The generated
TargetSemantics module therefore compares two byte arrays spelled that way, and
the embedded preservation library, which checks a stored equation against the
elaborator's, refused the stored `fromOrdering.eq_1` (LLV7014). Its statement
now spells the two arrays as the elaborator makes them (twelve nodes inserted
before the statement; later ids shifted by thirteen, `lexlean fmt` canonical).

Certificates A and B are kernel data, so a repeated type is one node and the
chain `g_k<T> = g_{k+1}<(T, T)>` 16 deep, the old `lowering-size-limit`, now
verifies (`COSTS_A.type_node` is 0). The fixture is a match on 400
constructors: LLS8002 in lowering, certificate A at least 10,200,437 bytes,
1,205 program nodes, 20 KB of source. SPEC.md §17.17 *Limits* carries the cost
table `COSTS_A` and `COSTS_B` state (it still had the text-certificate costs)
and says why the chain no longer measures a certificate.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QppGKetUURseXowZun9KY
afflom pushed a commit that referenced this pull request Oct 17, 2026
One regeneration for #68, #150, #151, #157, #163, #91 slice 1 and stage 4 of
#89 on main: the language-1.2 and 1.1 compiler-semantics IDs (lean_backend 10
and 20 by hand; the preservation library statement is the second hand edit),
every lexlean.lock, the golden oracles, the calculus fixtures, the production
export, the conformance fixtures, and the normalized verification records of
every example that moves (all of them verified end to end with the pinned
Lean, certificates A, B and E as data), then `validate-model --write`.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QppGKetUURseXowZun9KY

afflom commented Oct 17, 2026

Copy link
Copy Markdown
Member Author

Maintainer status: this PR's head 22663a7c0 is an ancestor of the v0.4 train, #167, which carries the sweep this description defers to. On the train: lean_backend advanced from 9/19 (main) to 10/20; every golden, fixture, calculus, production-export and verification record is regenerated by the repository's writers; the embedded preservation library's fromOrdering.eq_1 statement was re-stated for this lowering's spelling (the elaborator makes a different type for UInt8.ofNat (nat_lit n), and the library checks its stored equation against it). The description's claims were checked against the integrated tree: SM-19 and MD-02 read every generated literal back, the measurements in VERIFICATION.md stand as written (largest elaborable literal 6394 bytes under both renderings; the lowering moves no limit), and the description says so. Nothing in the description overstates.

This PR merges when #167 does (one gate on the integrated tree). Item 6 remains the owner's adoption gate in PrismPM, as the description says.


Generated by Claude Code

@humuhumu33 humuhumu33 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Content review of #68 at 22663a7 (train member of #167).

The renderer and the independent reader are correct. Every octet is written root.UInt8.ofNat (nat_lit n), the empty array is root.ByteArray.mk #[], and bindings begin at four occurrences in ascending order. The let scope is parenthesized and holds only closed octets, so it can neither capture an outer name nor be captured. Several literals in one definition are covered by SM-19's byteCorpus under Lean. The reader in semantic_ir/bytes.rs shares no code with the renderer and refuses every noncanonical form I tried. Only MD-02's two exact-text byte assertions changed, both into decode-and-compare; nothing else in MD-02 was dropped or weakened.

  1. Blocking, to fix on #167: OR-04's plant "a split limit one too small" (crates/conformance/src/cases/oracle.rs:1757-1760, replace_once "let _x_74 = 4;" on Coverage.Prims.text) is anchored to a local name in the oracle's Rust. That Rust is generated from LexLean's Lean, and Coverage.Prims.text contains byte literals this PR respells. CI run 39051756984 (bdd-verification) fails with "the edit does not apply to Coverage.Prims.text"; the same leg passed on main 243f0c7, and this head is 243f0c7 plus the six hand-written files. #167 leaves the plant unchanged (oracle.rs:1759-1761), and its draft CI does not run this leg. Please re-anchor every OR-04 plant on a root whose Lean contains a byte literal, and run conformance_or_04 before #167 is marked ready.

  2. Should-fix: VERIFICATION.md:274-277 and 292-305 measure a scratch file with Lean's default heartbeats (and, for the bisection, the default depth). Generated modules set maxRecDepth 100000 and maxHeartbeats 1000000000 (backend/semantic.rs:1866). The text also says SM-19's largest corpus is 1024 bytes, but verify_large_byte_declaration(true) (semantic_ir.rs:57-67, 1087-1088) has 8192-byte literals with all 256 values, and it passes under Lean in CI. So the 6394-byte ceiling and "does not move the largest elaborable literal" describe the scratch file, not LexLean's output. Please name the options used, correct 1024 to 8192, and limit or re-measure the conclusion.

  3. Should-fix: VERIFICATION.md:268 and the PR body say "MD-02 applies the same reader to every embedded artifact byte". MD-02 has its own looser decoder (models.rs:541-594): it does not check the four-occurrence rule, binding order, or an octet both shared and inline, and it accepts a binding used twice (models.rs:606). It also decodes only banner and hiddenWeights (models.rs:664, 678) of the six *.bytes definitions in Recognizer.lean. Please reuse bytes::read_expression for every *.bytes definition, or correct the sentence.

  4. Should-fix: SPEC.md:2740-2748 (binder hygiene) says the backend writes ByteArray and UInt8 "all without qualification". Byte literals now write root.ByteArray.mk and root.UInt8.ofNat (nat_lit n), and bind __byteN. One sentence would fix it: the spelling, plus why __byteN captures nothing (source names begin with a letter, and the scope holds only closed octets).

  5. Should-fix, on #167: CHANGELOG Unreleased has no entry for the lowering or for the lean_backend moves (1.1 9 to 10, 1.2 19 to 20, c245450). It mentions #68 only in the fromOrdering.eq_1 entry.

  6. Nit: VERIFICATION.md:286 (4096 bytes: 2.6 s vs 2.3 s) contradicts :296 (2.0 s under both), and :302 then calls the explicit form faster at 4096 on single runs. "By 16-byte steps" cannot produce 6394 and 6409, which are 15 apart: 6394 is the largest size tested that elaborates, and 6395-6408 were not tested.

  7. Nit: the RED is a spelling RED only (VERIFICATION.md:271-277). If the reason for the change is that rooted names and nat_lit keep a literal's meaning independent of user names (language 1.1 has no binder-hygiene check), please say so in one line. A 1.1 fixture with a byte literal under a let named ByteArray or UInt8 would give a behavioral RED.

  8. Nit: features/suites/semantic-ir.feature:136 is hard to parse, and "byte literals" reads wider than the change, since production/certificate.rs:819 and rust_term.rs:90 keep the old spelling. Suggested: "And the generated Lean writes every byte literal as explicit octets, binding an octet that occurs four or more times once, local to that literal, while the document shows the original decimal numbers and every byte in order".

Item 6 (PrismPM qualification) is the owner's adoption gate outside this repository and is not a merge gate.

This is a comment review now; formal approval will be given on #167's final head once OR-04 passes there.

afflom pushed a commit that referenced this pull request Oct 18, 2026
…llow the data certificates; CHANGELOG names #68; no stale allow-list entry

From the integration review on #167 (humuhumu33):

- The SP-02 statement said a match on 420 constructors is not refused by the
  early bound; the bound refuses a 400-constructor match (the new
  `lowering-size-limit` fixture) and `FITTING_CONSTRUCTORS` is 240. The
  register, SPEC §31 and the scenario say 240; CONFORMANCE.md regenerated.
- SPEC §17.17 said types are not shared and the chain is refused from 14 deep;
  with certificates A and B as kernel data the chain verifies 16 deep. It also
  said every cost is half of the greatest; certificate A's are the largest with
  five percent to spare (as `COSTS_A`'s comment states), B's and E's are half.
  VERIFICATION.md's chain figures are marked superseded with a pointer.
- CHANGELOG gains #68's entry (lean_backend 1.1: 10, 1.2: 20, and the IDs that
  move); the widening and scaffolding entries no longer say language 1.1 is
  unchanged in a tree where #68 moves it; stale per-member register counts are
  dropped; the scaffolding entry says the renderer records effects and derives
  nothing (#105), as 0dedb4b made true.
- The allow-list of Lean-looking literals had an entry for the developer
  printout removed in cde36cd; SP-17 requires every entry to match, and this
  one matched nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QppGKetUURseXowZun9KY
afflom pushed a commit that referenced this pull request Oct 18, 2026
…e's current locals

#68's byte-literal spelling moves Lean's LCNF of the text root, so the oracle
crate it is compared with numbers its locals differently: the split limit is
`_x_67` (was `_x_74`) and the equality test `_x_43` (was `_x_46`). Read
from the oracle crate the corpus run builds on this tree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013QppGKetUURseXowZun9KY
@afflom afflom closed this pull request by merging all changes into main in 193cd21 Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants