Repository navigation
Add lexlean migrate --to 1.2 for language-1.1 data-form projects (#257) - #261
Conversation
) Language 1.2 refuses the data form of a module with LLP2003 (#149), so the SPEC 17.12 recipe (change the schema tag and the glossary versions, relock) produced a project that cannot build, and lexlean::sdk::to_notation had no command. The new command migrates a whole project in one step: - every module of every source root is read, its tag moved from lexlean/semantic-module/1 to /2, written in the notation through sdk::to_notation, and read back; a notation that does not read back to the module is refused; - builtin glossary references (`\useglossary`, and the imports of each path lexicon manifest) move from 1.1.0 to 1.2.0; a path manifest's language becomes 1.2 and its entries and version are untouched; - the language row becomes 1.2 in canonical form and the lock is regenerated as lexlean/lock/2. The result is written into a scratch copy first and locked and checked as language 1.2 there. Only then are the files staged beside their targets and renamed together, with a rollback of the renamed files if one rename fails. The project mutation lock is taken after the proof, so a refusal creates no build root. A project that is not language 1.1 is refused with LLC0103, a module that is not a 1.1 data-form semantic module or has no notation with LLP2003; `--check` runs the same proof and writes nothing. The function is lexlean::api::migrate::migrate; Engine keeps its seven stable entry points (CL-13). SPEC 17.12 now describes the command and 23.4 documents it. CL-24 is registered with its scenario and conformance test, the negative fixture migrate-module-without-notation holds a module the notation cannot express, and the register count moves to 437 (RP-07 constant, SPEC total, README, CHANGELOG, generated CONFORMANCE.md). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wRsmyNivwbjJp2QZuGVdY
…257) The notation printer writes a node a readable form cannot carry exactly in the generic form `kind{...}` (SPEC 17.21.1: printing and parsing are inverse on every value). A `match` whose branch holds a variable named like a keyword (`var{name: first}`) is such a node, and as the right operand of an infix form it is printed `(a && (match{...}))`. The reader's parenthesised-term dispatch took the leading word `match` (and `not`, `first`, `second`, `cons`, `nil`, ...) for the keyword's own form and then failed on `{` with "expected a term, found `{`". The dispatch now treats a keyword followed by `{` as no keyword, so the term falls through to the generic reader, as it already did for a plain call name. The printer was right; the reader was wrong. The regression is a unit test of the printer and reader with a minimal reproduction: a `match` with a keyword-named variable, and `not` and `first` nodes with an extra member, each as the operand of `and`. It found the failure on stdlib Custody and Effects of the PrismPM SDK under `lexlean migrate --to 1.2`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wRsmyNivwbjJp2QZuGVdY
|
Pushed the reader fix for the nested-match case reported from the PrismPM SDK stdlib (keyword followed by { in a parenthesised term). Regression test is the unit test a_keyword_kind_written_generically_reads_back_in_parentheses. Generated by Claude Code |
…07 (#257) conformance_ex_07 compares the directories of tests/negative with the prescribed rejection classes of SPEC 28.5 held in cases/examples.rs, and failed in the production part of CI with the new fixture migrate-module-without-notation as the one directory the list did not name. The fixture is now listed with the diagnostic it expects, LLP2003, beside the other 17.12 classes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wRsmyNivwbjJp2QZuGVdY
Registering CL-24 for `lexlean migrate` grew the §31 table and the register to 437 rows; RP-07 still asserted 436 and failed in `just vv (rest)` (job 119468915118). The bijection checks that follow are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wRsmyNivwbjJp2QZuGVdY
The prescribed rejection classes are a fixed-length array; adding migrate-module-without-notation made it 286 entries while the type still said 285, so repo-conformance did not compile (E0308 in the honesty job). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wRsmyNivwbjJp2QZuGVdY
|
CI is green on 0d1351d. Three follow-up fixes went in after the first run:
This is ready for review by the LexLean maintainers. One note from using it downstream (PrismPM #224): Generated by Claude Code |
|
Review of The main path is sound. The round-trip proof compares the whole module value ( Two items before merge, three test gaps, and a rebase. Before merge
Test gaps
Rebase. The PR conflicts with
Release is now at register 443 and EX-07 at 297, so after the rebase CL-24 makes them 444 and 298, in all four count places. |
Merge origin/release/v0.4 (9796067) into the PR branch rather than rebasing it, since the branch is shared. Conflicts resolved, each a count that moved on both sides: - README.md: registered ID count 437 and 443 -> 444. - SPEC.md: section 31 total 437 and 443 -> 444. - xtask/src/spec_links.rs: RP-07 row count 437 and 443 -> 444. - crates/conformance/src/cases/repository.rs: RP-07 table length assertion 437 and 443 -> 444. - crates/conformance/src/cases/examples.rs: EX-07 prescribed array length 286 and 297 -> 298 (the release's 297 fixtures plus the one migrate-module-without-notation entry this branch adds). CHANGELOG.md states the register as 444 IDs. CONFORMANCE.md, model/ids.toml and the CL-24 scenario merged without conflict; `cargo xtask validate-model` reports 444 ids and `validate-spec-links` reports 444 table rows bijective with the register. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wRsmyNivwbjJp2QZuGVdY
The register statement of CL-24 claimed that a module the notation cannot express is refused with LLP2003, and cited tests/negative/migrate-module-without-notation as the evidence. That fixture's module carries a top-level `note` member, which schemas/semantic-module.schema.json forbids (the top level is additionalProperties: false with only `declarations` and `spec`), so it is not a language-1.1 module at all and the claim was tested only on malformed input. A schema-valid module the notation cannot express was searched for and none was found: the semantic printer is total over schema-valid values, strings with control characters, line separators, NUL, quotes, braces and non-BMP code points all print and read back equal, and the serde_json recursion limit (128) refuses a deeply nested module as JSON before the notation's own depth limit (256) can be reached. The statement is therefore reworded to what is tested. - model/ids.toml, SPEC.md section 31, features/suites/cli-api.feature: CL-24 now says a module that is not a language-1.1 data-form semantic module is refused with LLP2003. CONFORMANCE.md regenerated with `cargo xtask validate-model --write`. - SPEC.md 17.12 and 23.4: the read-back guard in `migrated_source` stays and is described as a guard no schema-valid module is known to fail. - crates/conformance: the fixture comments and the case context name what the fixture is. The fixture is kept: it is the payload the notation cannot spell that AU-04 requires of a refused data-form fixture. - CHANGELOG.md: the fixture description says what it holds. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wRsmyNivwbjJp2QZuGVdY
The test a_keyword_kind_written_generically_reads_back_in_parentheses
printed every generic keyword node as the right operand of an `and`, which
terms::read reads and which already accepted `kw{`, so it passed without the
guard added in paren() and did not cover it.
- a_generic_keyword_node_first_after_a_parenthesis_reads_back puts a
generic `not{...}` and `first{...}` (each with an extra member, which no
readable form carries) as the left operand, printed `(not{...} && true)`,
the position paren() reads first. Verified to fail on the base reader
(the guard reverted) and to pass with it.
- paren(): the guard treated every keyword followed by `{` as generic,
which also turned a hand-written `(let {name: x, type: T} := v; b)` into
an error, although that form read before and is the one `let` form
whose own syntax continues with `{`. The guard now keeps `let` when
the first member after the brace is `name`, which no `let` node has, so
the form reads as before.
- a_let_with_a_generic_binder_still_reads covers that form.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wRsmyNivwbjJp2QZuGVdY
conformance_cl_24 did not test four claims of SPEC 23.4, and its "--check creates no build root" assertion was vacuous: add_path_lexicon relocks, which creates .lexlean/.lock, before the --check snapshot was taken, so the build root was in both snapshots. - add_path_lexicon removes the build root its relock created, and has_build_root is asserted before and after --check and after every refusal, so a command that created one fails the case. - the successful migration asserts the files are replaced together: the set of changed files is exactly the modules, the path manifest, lexlean.toml and lexlean.lock, with no other file touched or created. - LLC0103 on a path lexicon manifest that is not language 1.1, in the migration and in --check. - LLP2003 on a module that is not a semantic module (a tooling module). - a scratch copy that fails its 1.2 check (a path manifest importing lexlean.std.nat@9.9.9, which the scratch lock cannot resolve, LLR3001) is refused with nothing written, in the migration and in --check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wRsmyNivwbjJp2QZuGVdY
…ale proof Two defects in crates/lexlean/src/api/migrate.rs. Rollback stopped at the first failed restore. publish() restored the files already published in reverse, but the `?` on each restore returned at the first one that failed, so every file not yet restored stayed migrated and the error named one path, contradicting "never left half migrated" (on Windows an editor or scanner holding a module open is enough to trigger it). - rollback() attempts every restore, last published first, and returns every failure; publish() reports the persist error together with all files that could not be restored, and says the project is partly migrated when any could not. The proof read the project unlocked. stage() re-read each original under the lock but did not compare it with the bytes the proof had read, so a `fmt` or an editor save between the proof and the lock was overwritten by text derived from the stale file. - rewrites() now returns a Plan whose `inputs` holds the SHA-256 of the raw bytes of every module, path manifest and the configuration it derived the new files from (read_input replaces read_text). - stage() takes the proven fingerprint and, under the lock, refuses with LLV7010 when the file is absent or its bytes differ; nothing is published because the staged temporaries are dropped. The lockfile is regenerated, not derived from its old bytes, and has no input. - migrate_inner() is split so the lock-and-publish step, write(), is callable on a plan. Tests (migrate/tests.rs): - a_failed_publish_restores_every_file_already_published forces a persist failure on the fourth of four staged files and checks the two replaced files and the created one are back to what they were. - every_restore_is_attempted_and_every_failure_reported gives rollback() two unrestorable files around a restorable one and checks both failures are reported and the one between them was still restored. - a_file_changed_after_the_proof_is_refused_and_nothing_is_written edits a module between rewrites() and write() and checks the LLV7010, that no file changed, and that the edit survives. - a_project_unchanged_since_the_proof_is_written is its control, and a_staged_file_whose_original_vanished_is_refused covers the absent case. SPEC.md 23.4 and CHANGELOG.md state both behaviors. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wRsmyNivwbjJp2QZuGVdY
|
Addressed in
Checks run: Not changed: the configuration file's fingerprint is taken when Generated by Claude Code |
humuhumu33
left a comment
There was a problem hiding this comment.
Review of 4bf7b3a39, built and run. Not approving yet: the head does not contain release/v0.4, and two documented claims do not match the code. The migration itself did everything #257 asks on three real projects, verify included.
Host. WSL Ubuntu 26.04 x86-64, 12 cores, 15 GB. Rust 1.97.1, Lean 4.32.1 through elan. Debug build of lexlean at 4bf7b3a39. PrismPM projects from PrismPM main @ c1432003, copied, never edited in place.
Evidence: three language-1.1 data-form projects
| Project | Modules, path lexicons | 1.1 check, lock --check, fmt --check |
migrate --check |
migrate |
1.2 check --all, lock --check, fmt --check --all |
fmt --all after |
migrate again (and --check) |
1.2 build --all |
verify --all 1.1 → 1.2 |
|---|---|---|---|---|---|---|---|---|---|
LexLean examples/semantic-1.1 (the only 1.1 example) |
11, 0 | pass | exit 0, tree byte-identical, no .lexlean |
exit 0, 13 files changed | pass | byte-identical | exit 2 LLC0103, tree unchanged |
pass | pass → pass (11 modules) |
PrismPM examples/Calculator |
1, 0 | pass after lexlean lock (see note) |
exit 0, unchanged, no .lexlean |
exit 0, 3 files changed | pass | byte-identical | exit 2 LLC0103, unchanged |
pass | pass → pass (1 module) |
PrismPM tests/fixtures/holo/ho-11-text-application/project |
4, 1 (prism.arch) |
pass after lexlean lock |
exit 0, unchanged, no .lexlean |
exit 0, 7 files changed | pass | byte-identical | exit 2 LLC0103, unchanged |
pass | pass → pass (4 modules) |
After migration no \semanticdata and no @1.1.0 remain. lexlean.toml changes only its language row. The prism.arch manifest changes only language and its three imports (lexlean.core, lexlean.std.bool, lexlean.std.nat to 1.2.0).
Note on the baseline: both PrismPM locks are stale for this build (LLC0102, compiler semantics moved, because PrismPM main pins an older LexLean). The 1.1 baseline is therefore after a 1.1 lexlean lock. Migrating the stale-lock copy directly gives byte-identical output to migrating the relocked copy, for all three projects.
Evidence: refusals and edge cases
| Case | 1.1 check |
migrate --to 1.2 |
Files unchanged | Build root created |
|---|---|---|---|---|
Already 1.2 (examples/language-1.2) |
pass | exit 2 LLC0103 |
yes | no |
Language 1.0 (examples/nat-add-zero) |
pass | exit 2 LLC0103 |
yes | no |
| Hand-broken data form (JSON truncated) | LLP2003 |
exit 1 LLP2003 |
yes | no |
Data form carrying the /2 tag in a 1.1 project |
LLT4001 |
exit 1 LLP2003 |
yes | no |
| A notation module inside a 1.1 project | LLR3001 |
exit 2 LLC0103 |
yes | no |
| Hand-edited data form, valid JSON, pretty-printed and keys reordered | LLT4001 "semantic-module JSON is not canonical" |
exit 0, migrated | no, migrated | n/a (it migrated) |
--to 1.1 |
n/a | exit 2 LLC0001 |
yes | no |
No panic in any run. The reformatted module migrates to output byte-identical to the canonical example's migration, so no meaning changes. The issue is the documentation, item 3 below.
Before merge
- The head does not contain
release/v0.4. It is 20 commits behind (f057b8308, #254). A local merge conflicts inCHANGELOG.md,README.md,SPEC.md,crates/conformance/src/cases/repository.rsandxtask/src/spec_links.rs, so the register count moves again in all four places. - A stale-proof refusal creates the build root (
crates/lexlean/src/api/migrate.rs:467).writecallsacquire_lock, which runscreate_dir_allon the build root, beforestage(:470) refuses withLLV7010. SPEC §23.4 (SPEC.md:8965) says "a refusal writes nothing and creates no build root". Found by reading.a_file_changed_after_the_proof_is_refused_and_nothing_is_writtenchecks file bytes but not the build root. Either remove a build root the call created on that refusal, or narrow the sentence to the refusals before the lock. - A non-canonical data form is migrated, not refused (
migrate.rs:142).sdk::read_moduleaccepts any JSON value, so a module that 1.1checkrefuses as non-canonical (LLT4001) migrates with exit 0. That is a reasonable behaviour, because the read-back check still compares the whole value. But §17.12 (SPEC.md:4127), §23.4 and theCL-24statement say a module that is not a language-1.1 data-form semantic module is refused withLLP2003, and nothing tests this case. Either refuse it withLLP2003, or state thatmigratecanonicalizes the data form and add the case tomigration.rs.
Test coverage
- Covered by
conformance_cl_24and the unit tests:--checkwrites nothing; a whole migration with a path lexicon; read-back equality per module;check,lock --checkandfmt --checkafter; second run refused; a 1.2 project refused; a non-semantic module; a 1.2 manifest; a scratch-check failure;--to 1.1; the rollback and the stale proof. - Not covered: the notation-module branch (
migrate.rs:133), the non-canonical data form (item 3), andbuild/verifyof a migrated project. The table above gives theverifyevidence that #257's definition of done asks for.
Docs
§17.12 and §23.4 state the command and its refusals. The README capability row names migrate and CL-24. CHANGELOG has the entry. Only items 2 and 3 above differ from the code.
…t-thread-y7idcx-migrate-1-2 Bring the migration branch up to release/v0.4 (20 commits, through #254). Conflicts in CHANGELOG.md, README.md, SPEC.md, crates/conformance/src/cases/repository.rs and xtask/src/spec_links.rs are resolved by keeping both sides: the release's runtime entry and the migrate entry both stay in CHANGELOG.md, and the register count becomes 446 everywhere it is stated (model/ids.toml and the SPEC.md section 31 table were at 444 and 445 on the two sides). CONFORMANCE.md and ERRORS.md are regenerated with `cargo xtask validate-model --write`; the SPEC.md section 17.12 and 23.4 prose already states that a non-canonical data form is refused, which the following commit implements. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wRsmyNivwbjJp2QZuGVdY
… on refusal Two review findings on the 1.1 to 1.2 migration: - A data form that language 1.1 `check` refuses as not canonical (LLT4001) was migrated. `migrated_source` now refuses it with LLP2003 before anything else is derived. The test is the one 1.1 makes: `sdk::data_is_canonical` drops the space and line-feed atoms outside JSON string literals, as the structural grammar does, and compares the rest with its canonical JSON byte for byte, so the laid-out examples still migrate and an escaped or reordered member is refused. - A stale-proof refusal created the build root: `write` took the mutation lock, which creates the build root, before staging refused with LLV7010. `write` now notes whether the build root existed, and on any refusal or failure after taking the lock removes the lock file and the directory it created. A build root that was already there is kept. Tests: `a_file_changed_after_the_proof_is_refused_and_nothing_is_written` asserts the build root is absent after the refusal, and `a_refusal_keeps_a_build_root_that_was_already_there` covers the other side. CL-24 gains a case that pretty-prints a module and escapes one member name: 1.1 `check` refuses it with LLT4001, and `migrate` and `migrate --check` refuse it with LLP2003 and leave the tree and build root untouched. The CL-24 statement and scenario say so. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wRsmyNivwbjJp2QZuGVdY
|
Addressed at a9db189 (merge c709f46):
Local: fmt, clippy Generated by Claude Code |
|
Maintainer review of Should fix
Nit: What checked out: the round trip compares the whole module value with the tag flipped to Landing order: after the #91 heap half (#255 now, then #259, #262, #264, #265, #293) and the bootstrap trio; this PR needs no regeneration, so it goes in the first identity-neutral slot after those. Generated by Claude Code |
…t-thread-y7idcx-migrate-1-2 Conflicts were the register count only (README.md, SPEC.md section 31 total, cases/repository.rs, xtask/src/spec_links.rs) and the CHANGELOG entries; both sides kept. The register is 452: the line's 451 plus CL-24. The fixture lock of migrate-module-without-notation pins the compiler-semantics ID of the other language-1.1 fixtures. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…gister 453 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QppGKetUURseXowZun9KY
|
Maintainer: landing. Pushed one merge commit on this branch (no rewrite): Generated by Claude Code |
… register 454 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QppGKetUURseXowZun9KY
…30-slice-4 The line gained #261 (CL-24, register 453); conflicts were the register count (README.md, SPEC.md section 31 total, cases/repository.rs, xtask/src/spec_links.rs) and the CHANGELOG entries, both sides kept. The register is 456: the line plus this branch's three rows; CHANGELOG states it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Closes #257 (milestone v0.4, so this targets
release/v0.4per AGENTS.md "Release lines").What
lexlean migrate --to 1.2 [--check]migrates a language-1.1 data-form project, path lexicon packages included, to language 1.2 in one command:\semanticdata{...}to the authoring notation throughlexlean::sdk::to_notation(tag/1to/2first), and the notation must read back to exactly the module, so declaration names, theorem statements and axiom policies are unchanged;1.1.0to1.2.0in every\useglossaryand in each path lexicon manifest'simports; the manifestlanguagebecomes1.2(entries and package version untouched);languagerow is rewritten (canonicallexlean.toml) and the lock regenerated aslexlean/lock/2.The migrated project is first written into a scratch copy, locked and checked as language 1.2. Only then are files staged and renamed together (rollback if a rename fails). The mutation lock is taken after the proof, so a refusal writes nothing and creates no
.lexlean. Refusals:LLC0103(project not language 1.1),LLP2003(not a 1.1 data-form semantic module, or no notation), or the scratch check's own diagnostics.--checkruns the same proof and writes nothing.Enginekeeps its seven entry points (CL-13); the function islexlean::api::migrate::migrate. No new diagnostic codes.Register / spec (R3)
CL-24row inmodel/ids.toml+ SPEC §31 table (§17.12, §23.4), scenario infeatures/suites/cli-api.feature, testconformance_cl_24(cases/migration.rs):--checkwrites nothing; full migration ofexamples/semantic-1.1plus a language-1.1 path lexicon package (modules read back equal under the 1.2 tag,check,lock --check,fmt --checkpass); a migrated project andexamples/language-1.2refused withLLC0103;--to 1.1refused; every refusal leaves all files byte-identical.tests/negative/migrate-module-without-notation(LLP2003).migrate. Register count 436 to 437 (RP-07 constant, SPEC total, README, CHANGELOG, regenerated CONFORMANCE.md).Verification
Locally (no Lean toolchain on this host):
cargo fmt --check,cargo clippy --all-targets --all-features -D warnings(lexlean, repo-conformance, xtask),xtask validate-model,validate-spec-links,check-test-inventory,conformance_cl_24, thebddmeta-gate. Not run here: the Lean-backed cases andjust vv; CI owns those.Real project: a copy of the prism-code model (9 data-form modules +
prism.archpath lexicon, language 1.1) migrates withlexlean migrate --to 1.2; the copy then passeslock --check,fmt --check --all,check --allandbuild --all.verifyneeds the pinned Lean toolchain, which is not installed on this host.Generated by Claude Code