Skip to content

Add Lean 4 backend [AI-assisted] - #40

Open
septract wants to merge 166 commits into
rems-project:masterfrom
OathTech:mdd/lean-backend
Open

Add Lean 4 backend [AI-assisted]#40
septract wants to merge 166 commits into
rems-project:masterfrom
OathTech:mdd/lean-backend

Conversation

@septract

@septract septract commented Mar 7, 2026

Copy link
Copy Markdown

Summary

Add a new backend targeting Lean 4, allowing Lem definitions to be exported as Lean 4 code. This brings the set of supported targets to: OCaml, Coq, HOL4, Isabelle/HOL, Lean 4, LaTeX, and HTML.

The backend is structurally modelled on the Coq backend, adapted for Lean 4 syntax and semantics. Tested against Lean 4.28.0 via the Lake build system.

What's included

  • src/lean_backend.ml (~2400 lines) — main backend translating Lem AST to Lean 4 syntax
  • lean-lib/LemLib Lean 4 runtime library (~720 lines): sets, maps, comparisons, numeric utilities, BitVec operations for machine words
  • library/lean_constants — Lean 4 reserved words and typeclass names
  • 368 Lean target reps across 28 library files (vs 309 for Coq) — every Coq target rep has a Lean equivalent, plus extras for Lean-specific features
  • Supporting changes to 21 shared source files (all guarded behind Target_lean checks — see "Shared code safety" below)
  • Documentation in doc/manual/backend_lean.md and updates to README.md
  • make lean-libs target generating Lean library files from Lem's standard library
  • scripts/lean_coverage.sh — bisect_ppx coverage script used to identify and close untested codepaths (~87% line coverage on lean_backend.ml)

Test suite

  • 12 backend tests: Types, Pats, Pats3, Classes2, Classes3, Exps, Coq_test, Coq_exps_test, Record_test, Op, Let_rec, Indreln2 — all generate valid Lean and compile via Lake (57 jobs)
  • 44 comprehensive tests with 424 runtime assertions covering: arithmetic, strings, lists, sets, maps, pattern matching, type definitions, type classes, mutual recursion, vectors, machine words (BitVec), cross-module imports, and more
  • 2 real-world examples:
    • examples/cpp/ — C++ concurrency model (Cmm.lean, ~1930 generated lines, 34 Lake jobs, 0 errors)
    • examples/ppcmem-model/ — PowerPC memory model (10 .lem files, 10/10 compile, 43 Lake jobs, 0 errors)

Notable design decisions

  • Whitespace-sensitive output: Lem's block formatting disabled for Lean; explicit spaces used instead
  • UTF-8 output: Meta_utf8 variant in output.ml for correct encoding of , ×, ,
  • Constructor scoping: export TypeName (Ctor1 Ctor2 ...) after each inductive — Lean's open is file-local, so export is needed for importers to see constructors
  • Termination: recursive functions default to partial def unless a declare {lean} termination_argument = automatic annotation is present; 10 library functions were annotated total, and 3 more redirect to total LemLib wrappers. Only 2 genuinely partial defs remain (unfoldr, leastFixedPointUnbounded)
  • deriving BEq, Ord: auto-derived for simple types (non-mutual, no function-typed constructor args); sorry-based instances as fallback for mutual types (Lean's deriving doesn't support mutual inductives)
  • Machine words: mword maps to Lean's BitVec with 36 operations implemented in LemLib; int32/int64 use distinct newtype wrappers (LemInt32/LemInt64)
  • Type/value namespace unification: Lean shares a single namespace (unlike Lem/Coq/OCaml). rename_top_level.ml seeds constant renaming with type names to handle collisions. Cross-module names included via full env.t_env scan
  • Typeclass name avoidance: lean_constants includes Add, Sub, Mul, etc. to prevent clashes with Lean stdlib
  • Propositional equality in indreln: == (BEq) converted to = (Prop) in inductive relation antecedents, handling both Lem AST decomposition paths
  • n+k pattern rejection: is_lean_pattern_match in patterns.ml triggers guard-based desugaring instead of unsupported n+k patterns
  • Tab sanitization: Lean 4 forbids tabs; all whitespace tokens sanitized automatically

Shared code safety

Changes to shared source files are guarded to only affect the Lean backend:

  • typed_ast_syntax.ml: class path collection in used_types — only consumed by Lean-specific code in rename_top_level.ml
  • rename_top_level.ml: Tc_class renaming returns early for non-Lean targets
  • target_trans.ml: class names added to avoid set only when target is Target_lean
  • output.ml: block token type kept as Kwd (preserves Coq/HOL/Isabelle spacing); UTF-8 encoding fix (of_string) is universal but correct
  • target_binding.ml: e_env fallback only fires when primary lookup fails (low risk, all targets)

Verified: all 6 non-Lean backends (OCaml, Coq, HOL, Isabelle, HTML, LaTeX) produce byte-for-byte identical output on all test files compared to master. Zero regressions.

Known limitations (won't fix)

  • 2 genuinely partial def in generated library: unfoldr (user-supplied termination), leastFixedPointUnbounded (iterates until fixpoint). Correctly partial.
  • sorry-based instances for mutual types: Lean deriving can't handle mutual inductives. Documented with /- mutual type -/ comments.
  • Duplicate Nat/Int instances in Num.lean: nat/natural both map to Nat, int/integer both to Int. Inherent to Lem's design, same in all backends.
  • Overlapping BEq instances: from Eq0, SetType, MapKeyType — three paths to BEq a. Lem typeclass design tradeoff.
  • O(n²) set operations: list-based representation, same as Coq backend.
  • coq_backend_skips.lem: non-positive inductive — fundamental Lean restriction.
  • Generated output spacing: double spaces, extra parens — inherited from Lem's output pipeline, affects all backends.

Test plan

  • make -C src — compiler builds cleanly
  • 12/12 backend tests generate valid Lean and lake build succeeds (57 Lake jobs)
  • 44/44 comprehensive tests pass with 424 runtime assertions verified
  • lake build in lean-lib/ — LemLib compiles (33 Lake jobs)
  • make lean-libs — all library files generated successfully
  • examples/cpp/Cmm.lean generates and compiles (34 Lake jobs)
  • examples/ppcmem-model/ — 10/10 files generate and compile (43 Lake jobs)
  • All 6 non-Lean backends produce byte-for-byte identical output vs master (60 file comparisons across OCaml, Coq, HOL, Isabelle, HTML, LaTeX — 0 differences)

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

septract and others added 17 commits March 6, 2026 10:06
Add a new backend targeting Lean 4, enabling lem to generate .lean files
from semantic definitions. This follows the same architecture as the
existing Coq backend (custom LeanBackend module via functor).

New files:
- src/lean_backend.ml: Main backend (~1600 lines)
- lean-lib/LemLib.lean: Runtime support library
- library/lean_constants: Lean 4 reserved words

Target registration across: ast.ml, target.ml, target.mli, parser.mly,
main.ml (-lean flag), target_trans.ml, backend.ml, backend.mli,
process_file.ml (.lean output with /- -/ comments and import LemLib).

Library declarations (declare lean target_rep) added to all standard
library files: basic_classes, bool, maybe, num, list, set, map, string,
either, relation, sorting, word, machine_word, tuple, function,
set_extra, map_extra, set_helpers, assert_extra.

Key Lean 4 adaptations:
- Bool/List/Nat capitalized types via target_rep type declarations
- Constructor patterns use dot notation (.Red, .some, etc.)
- Comments converted from (* *) to /- -/
- Records use structure/where syntax
- Match arms use | pat => expr (no end keyword)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Generate Lean 4 'instance : Inhabited T' for each type definition,
mirroring Coq's 'Definition T_default' generation. This ensures
default values are available for all user-defined types.

Also raise proper errors for Typ_with_sort in pat_typ and typ,
matching Coq's behavior instead of silently passing through.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Lean 4 to all documentation: README, manual (introduction,
invocation, backends, language grammar, backend linking, typeclasses),
and the Ott grammar definition. Create new backend_lean.md manual page.

Add missing declare lean target_rep entries for nth (list_extra.lem),
ord and chr (string_extra.lem) to complete library parity with Coq.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add lean to target lists for wordFromInteger, wordFromNumeral,
wordToHex in machine_word.lem. Add lean target_rep for choose and
exclude lean from choose lemmas/asserts in set_extra.lem. Exclude
lean from THE_spec lemma in function_extra.lem. Add lean-libs target
to library/Makefile and leantests target to tests/backends/Makefile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…n backend

- Add Meta_utf8 variant to output.ml to preserve UTF-8 bytes (×, →, etc.)
  instead of double-encoding through of_latin1
- Fix to_rope_help_block to use of_string for Format output, preventing
  double-encoding of Unicode characters in block-formatted output
- Add flatten_newlines utility to collapse newlines in output trees
- Disable block formatting for Lean backend (Lean 4 is whitespace-sensitive)
- Replace break_hint_space with explicit spaces in App, Infix, If, Fun, Case
- Add 'open TypeName' after inductive types for constructor scoping
- Add 'open ClassName' after class definitions for method scoping
- Remove dot-prefix on constructors in expression/pattern position
- Fix pattern constructor argument spacing (concat emp -> concat space)
- Fix 'let' keyword spacing (letx -> let x)
- Expand LemLib with set/map operations, ordering, and utility functions
- Add Lake project setup for lean-lib (lakefile.lean, lean-toolchain)
- Add lean-test Lake project for end-to-end compilation testing
- Expand lean_constants with ~30 missing Lean 4 reserved words
- Add lean-libs target and Lean paths to Makefile install/distrib/clean

5 of 7 test files now compile: Types, Classes2, Classes3, Pats, Pats3.
Remaining issues: Exps (set BEq instances), Coq_test (mutual inductives
with varying parameters), Classes3 (target-specific code leaking).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Filter target-specific class methods from Lean output: class bodies
  now skip methods annotated for other backends ({hol}, {coq}, etc.)
- Filter corresponding instance methods when class method is not
  target-visible for Lean
- Add 'deriving BEq' to inductive types and structures when all
  constructor/field types support it (no function-typed args)
- Skip 'deriving BEq' for mutual blocks to avoid cross-reference issues
- Handle mutual inductives with heterogeneous parameter counts by
  converting parameters to indices (Type 1 universe), with implicit
  bindings in constructors
- Use sorry for Inhabited defaults of mutual recursive types
- Export SetType.setElemCompare in Pervasives_extra for bare usage

All 7 test files now compile: Types, Classes2, Classes3, Pats, Pats3,
Exps, Coq_test (previously 5/7).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace all assert false in lean_backend.ml with descriptive error messages
- Add Typ_backend handling in typ and indreln_typ (previously unreachable crash)
- Fix sort_by_ordering bug: .EQ => false (mergeSort expects strict <, not <=)
- Add @[inline] to 19 trivial wrapper functions in LemLib.lean
- Add module-level documentation to LemLib.lean, Pervasives_extra.lean, lakefile.lean
- Add 13 missing Lean 4 keywords to lean_constants
- Fix flatten_newlines to recurse into Core nodes in output.ml
- Add setEqualBy doc comment noting sorted-input precondition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix lean_backend.ml assert/lemma/theorem emission:
  - assert -> #eval with Bool check (runtime verification)
  - lemma/theorem -> by decide (proof-time verification)
- Create tests/comprehensive/ directory structure
- Add Makefile, run_tests.sh, lakefile.lean, expected_failures.txt
- Add Pervasives_extra.lean stub for test compilation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tions

Test suite covers: let bindings, function patterns, pattern matching edge
cases, type features, constructors, expression edge cases, higher-order
functions, either/maybe types, set/map operations, comprehensions, modules,
type classes, inductive relations, mutual recursion, do notation, target-
specific declarations, infix operators, scope/shadowing, strings/chars,
numeric formats, assertions/lemmas, records, reserved words, comments, and
stress testing.

Backend changes:
- lean_backend.ml: assert -> #eval Bool check (runtime verification),
  lemma/theorem -> by decide (proof-time verification)
- process_file.ml: auxiliary .lean files now import their main module

Results: 21/25 test files compile and pass Lake build, with 103 #eval
assertions verifying runtime correctness. 4 files are expected failures
(comprehensions, inductive relations, sets/maps, stress - all due to
missing BEq instances or syntax issues).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix indreln Prop ascription: remove ': Prop' from constructor antecedents
  that confused Lean's elaborator for subsequent type references
- Add 'export SetType (setElemCompare)' to comprehensive Pervasives_extra
- Enable all 4 formerly-expected-failure tests in lakefile (comprehensions,
  indreln, sets_maps, stress_large) — all now compile and pass
- Clear expected_failures.txt (no remaining failures)
- Track lake-manifest.json for comprehensive test project
- Expand .gitignore: Lean build artifacts, generated .lean files in
  tests/backends/ and tests/comprehensive/, .claude/, _opam/, lean-lib/.lake/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ation

- Fix typ dropping Typ_app type arguments (e.g. List Nat → List)
- Fix setFromList/setFromListBy reversed output order (foldl → foldr)
- Change Comp_binding/Setcomp silent comments to proper errors
- Change pattern catch-alls from silent comments to proper errors
- Add lean-libs to Makefile libs_phase_2 target
- Add Pats3 to backends leantests target with build rule
- Add fmapUnion and fmapElements to LemLib
- Add test_typ_args.lem regression test (4 assertions, all pass)

All tests pass: 57/57 comprehensive jobs, 19/19 backend jobs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…c type vars

- Native do-notation: remove pipeline desugaring, emit Lean 4 do blocks
  with proper indentation and whitespace handling
- Vector literals: render L_vector as prefix+bits (e.g. 0b1010)
- Vector patterns: P_vector renders as list patterns with .toList on
  match expression; P_vectorC raises clear error (no backend supports it)
- Numeric type variables: fix sorry/errors in class definitions, instance
  declarations, and type class constraints — all now emit (n : Nat)
- Default values: use 'default' instead of 'sorry' for Typ_wild/Typ_var
- LemLib: add lowercase 'vector' type alias for Lean's Vector
- New test: test_vectors.lem (vector expressions + pattern matching)

All tests pass: 27/27 comprehensive (59 lake jobs), 19/19 backends.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…build system

Phase 1 — Backend bugs (lean_backend.ml):
- Fix string literal escaping: escape \, \n, \t, \0, \r (not just quotes)
- Fix let_type_variables: Nvar gets {n : Nat} not {n : Type}
- Fix Indreln: emit removal comment when not targeted for Lean
- Fix VectorSub: correct skips'' whitespace typo
- Fix indreln_typ: space for multi-arg types (ts <> [] not ts = 1)
- Fix theorem: explicit space after keyword
- Fix assert names: escape through lean_string_escape
- Fix Do handler: wrap in (do ...) parens for indentation isolation
- Fix Typ_app/Typ_backend: conditional space for zero-arg types
- Fix P_cons: parenthesize in fun_pattern context
- Fix default_value for Typ_var: use sorry (avoids missing Inhabited)

Phase 2 — LemLib fixes (LemLib.lean):
- Fix setEqualBy: order-independent mutual subset check
- Fix setCompareBy: sort both lists before comparing
- Fix setCase: 4th arg is plain value, not function (matches Lem sig)
- Fix chooseAndSplit: partition by comparison, not just head/tail
- Fix fmapEqualBy: key param from LemOrdering to Bool
- Add apply, integerSqrt, rationalNumerator/Denominator, realSqrt/Floor/Ceiling, intAbs, listGet?/listGet\!
- Add DecidableEq to LemOrdering
- Fix gen_pow_aux: total with termination_by/decreasing_by
- Fix sort_by_ordering: stable (.EQ => true)

Phase 3 — Build system:
- Add Classes2, Classes3, Coq_test to leantests Makefile
- Add nomatch, nofun, infix/infixl/infixr, prefix, postfix to lean_constants
- Fix README: Lean 4.28.0 (not 4.x)

New regression test: test_audit_regressions.lem (string escaping, cons
patterns, set equality — 6 assertions).

All 28 comprehensive tests pass, 19 backend jobs compile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ces, add lean-libs

Fix Bool.<-> resolution error that blocked `make lean-libs`:
- Add e_env fallback to search_module_suffix in target_binding.ml
  When typeclass resolution macros synthesize definitions with narrow
  local environments (missing imported modules in m_env), fall back to
  looking up module paths directly in the global e_env registry
- Fix orderingEqual target_rep: `decide` is wrong (expects Prop),
  use infix `==` since LemOrdering derives BEq
- Revert Comp_binding/Setcomp to comment output (matches Coq backend)

Improve Inhabited instance generation (lean_backend.ml):
- Use `default` for type variables in Inhabited context (not sorry)
- For mutual types, find safe constructors whose args don't reference
  other mutual types, reducing sorry usage
- Collect type/class namespace opens for auxiliary file generation

Add lean-lib generated library files (58 files from make lean-libs)
Add pairEqual and maybeEqualBy to LemLib.lean
Add runtime assertions to 6 existing test files
Add test_cross_module.lem regression test (9 assertions)

29/29 comprehensive tests pass, 19/19 backend jobs pass

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… tests

- Add is_lean_pattern_match in patterns.ml that rejects P_num_add,
  triggering guard-based desugaring instead of invalid Lean 4 syntax
- Add 14 int32/int64 bitwise functions to LemLib with two's complement
  conversion (int32Lnot/Lor/Lxor/Land/Lsl/Lsr/Asr, same for int64)
- Add missing library functions: naturalOfString, integerDiv_t,
  integerRem_t, integerRem_f, THE with target_reps in .lem files
- Fix type/value namespace collision: rename_top_level.ml seeds constant
  renaming with type names for Lean so functions avoid type names
- Fix self-referential Inhabited: generate_default_values detects
  recursive types without base cases and uses sorry
- Add Add/Sub/Mul/Div/Mod/Neg/Pow/Min/Max/Abs/Append to lean_constants
  to avoid ambiguity with Lean stdlib type classes
- Expand backend tests: Record_test, Op, Let_rec, Indreln2 (11 total)
- Fix test .lem files: add type annotations for Num.Numeral resolution,
  convert tabs to spaces in let_rec.lem

All 11 backend tests and 29 comprehensive tests pass (90 Lake jobs).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Expand backend_lean.md: add auxiliary files, recursive definitions,
  inductive relations, BEq derivation, automatic renaming sections
- Add Lake project example to compilation instructions
- Fix incorrect claim about constructor dot notation (uses open TypeName)
- Document Inhabited sorry behavior for recursive types without base cases
- Add -auxiliary_level auto mention, matching HOL4/Isabelle docs
- Fix introduction.md Lean version: 4.x -> 4.28.0
- Fix README.md Lean library entry to match other backends format

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove duplicate unreachable P_record match arms in fun_pattern and
def_pattern. Replace silent 'Internal Lem error' comment strings with
proper exceptions that surface errors to users. Simplify
generate_inhabited_instance by removing dead None branch (single types
now always pass through mutual-aware path). Standardize error message
format to 'Lean backend: ...' prefix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@septract
septract marked this pull request as draft March 7, 2026 07:14
septract and others added 12 commits March 6, 2026 23:48
Move generated library files under LemLib/ namespace so imports become
`import LemLib.Pervasives` instead of bare `import Pervasives`, avoiding
conflicts with Lean stdlib modules (Bool, List, String, etc.).

Fix class name collisions (Eq, Ord) with Lean stdlib by making the
renaming pipeline handle class types. Previously, class definitions were
skipped in add_def_aux_entities (TODO comment), so names in lean_constants
like Eq never triggered renaming. Now Eq -> Eq0, Ord -> Ord0 at all output
sites: class defs, constraints, and instance declarations.

Key changes:
- backend_common.ml: LemLib. prefix for library modules; class_path_to_name
- process_file.ml: dot-to-path conversion for Lean output files
- lean_backend.ml: strip LemLib. prefix from open stmts; use class_path_to_name
- types.ml/mli: type_defs_lookup_tc, type_defs_update_class
- typed_ast_syntax.ml: collect class paths and methods in add_def_aux_entities
- rename_top_level.ml: rename_type handles both Tc_type and Tc_class
- target_trans.ml: add_used_entities_to_avoid_names handles Tc_class
- lean_constants: add Ord
- Pervasives_extra stub moved to lean-lib/LemLib/; test stubs removed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…reps

Key changes to make generated library files compile:

- Import ordering: collect imports in a ref, emit all at file top before
  any other content (Lean requires imports before non-import statements)
- Import-open: suppress 'open' for LemLib.* modules (generated files have
  no namespaces; import alone brings definitions into scope)
- Class exports: use 'export ClassName (methods)' instead of 'open' after
  class definitions so methods are visible to importing files. Filter out
  names that clash with Lean globals (max, min, compare).
- Instance constraints: use inst_constraints from type system (fully
  qualified paths) instead of parsing unqualified Idents from Cs_list AST
- BEq bridges: emit 'instance [Eq0 a] : BEq a' after Eq class def, and
  'instance [SetType a] : BEq a' after SetType class def, so == works
  wherever these classes are in scope
- Target reps: Ord0.compare for compare method, intAbs for integer abs
  functions, \!= for unsafe_structural_inequality
- Remove pairEqual/maybeEqualBy from LemLib.lean (now in generated code)
- Pervasives_extra stub: remove namespace wrapper (matches generated style)
- lakefile: use submodules glob for full library discovery

28 of 61 library modules now build successfully (up from 0).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…g, cpp support

- Add explicit `: Type` annotation on all non-indexed inductives to prevent
  Lean auto-inferring Prop (Sort 0) for single-constructor mutual types
- Fix multi-clause mutual function naming to use const_ref_to_name (avoids
  definition/reference name mismatch e.g. test44 vs test440)
- Generate SetType/Eq0/Ord0 instances for all inductive types; skip for
  Type 1 (heterogeneous mutual blocks) since those classes require Type
- Auto-import LemLib.Pervasives_extra when Pervasives is imported, for
  bridge instances (NumAdd -> Add, etc.)
- Include transitive namespace opens in auxiliary files (Lean open is
  file-local, not exported to importers)
- Add MapKeyType compare method to BEq bridge derivation
- Add isInequal target_rep (\!=) for basic_classes
- Add One/Zero to lean_constants to avoid stdlib collisions
- Replace removed List.get?/List.get\! with listGetOpt/listGetBang wrappers
- Add Ord instance for Prod, set_tc, boolListFromNatural to LemLib runtime
- Update Pervasives_extra stub with Lem numeric class bridges

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ;lean to 13 target group annotations in cmm.lem so the Lean backend
can generate output from the same source file used by other backends.
Changes are purely additive and don't affect Coq/OCaml/Isabelle output.

Also add Lake project files (lakefile.lean, lean-toolchain, lake-manifest)
and .gitignore .lake/ globally instead of per-directory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…pes, constants

Backend fixes for real-world Lem projects (ppcmem-model, cpp):
- Sanitize tab characters in all generated output (Lean 4 forbids tabs)
- Use 'export Type (Ctor1 Ctor2)' instead of 'open Type' for inductives,
  so constructors are visible in importing files
- Parenthesize match/if/let/fun via shared needs_parens helper, applied
  consistently in function args, if-conditions, and case arm bodies
- Fix indreln type signatures to apply target reps (e.g. set -> List)
- Resolve wildcards in fun_pattern P_typ to concrete types
- Handle unit literal in fun_pattern as (_ : Unit)
- Extract is_library_module predicate for OpenImportTarget
- Expand lean_constants from 129 to 262 entries covering all Init types,
  typeclasses, and common functions (id, flip, cast, guard, etc.)
- Add gen_lean_constants.lean script for regenerating the list

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- natLnot: panic instead of returning 0 (NOT undefined for Nat)
- naturalOfString: panic on invalid input instead of returning 0
- THE: panic instead of returning none (Hilbert choice not computable)
- rationalNumerator/Denominator: panic (rationals not supported)
- realSqrt/Floor/Ceiling: panic (reals not supported)
- Add Nat bitwise ops (natLand, natLor, natLxor, natLsl, natLsr, natAsr)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…compilation

- Class method constants: emit @method (Type) _ for bare class methods
  so Lean can resolve implicit type parameters (fixes Machine_word.lean)
- Standalone BEq instances without [Inhabited] constraint, separate from
  Ord which requires Inhabited for sorry bodies
- Termination annotations: use try_termination_proof (like Coq/Isabelle)
  to emit def instead of partial def when termination is provable
- Multi-discriminant match: decompose tuple scrutinees for termination
  checker visibility (match l1, l2 with instead of match (l1, l2) with)
- Library namespace qualification: push namespace before processing so
  auxiliary file opens get qualified names (Lem_Basic_classes.Eq0)
- Bridge instances moved to LemLib/Bridges.lean (survives make lean-libs)
- Auto-import LemLib.Bridges for non-library modules
- Makefile cleanup: remove auxiliary files after lean-libs generation
- Target reps: genlist, last, nat bitwise ops, int32 bitwise ops

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Cross-module name collision: rename_top_level.ml includes ALL env type
  names in Lean constant-avoid set, not just local ones (fixes thread_trans
  type/indreln collision across modules)
- Record literal type ascription: add (({ ... } : Type)) annotation using
  exp_to_typ so Lean can resolve record types without context
- setChoose replaces sorry target rep: Set_extra.choose now maps to a real
  function in LemLib instead of bare sorry (which can't be applied as fn)
- Propositional equality in indreln: lean_prop_equality flag makes isEqual
  output = (Eq) instead of == (BEq) in antecedents; functions lack BEq
- Indreln renamed name output: uses constant_descr_to_name instead of raw
  AST name, so renames like thread_trans -> thread_trans0 are reflected
- deriving BEq, Ord: simple types (non-mutual, no fn-typed args) use Lean's
  deriving instead of sorry-based instances; adds [BEq a] [Ord a] constraints
  on downstream SetType/Eq0/Ord0 instances for parameterized types
- Dynamic library namespace list: replaces hardcoded core_lib_ns with
  computation from module environment (e_env), detecting library modules by
  Coq rename presence
- String.mk -> String.ofList: fixes deprecation warning in string.lem

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix bug in type_def_indexed: types with 0 params in a heterogeneous
mutual block were emitted as Type instead of Type 1, causing a Lean
universe mismatch error. All types in such blocks now consistently
use Type 1.

New test files:
- test_case_arm_nesting.lem: match/if/let/fun in case arms, as function
  args, in if-conditions, in list/tuple constructors (42 assertions)
- test_termination.lem: declare termination_argument, multi-discriminant
  match with 2 and 3 scrutinees, partial def fallback (15 assertions)

Enhanced existing tests:
- test_pattern_edge_cases.lem: n+k patterns (fib, pred, classify),
  unit in tuple/let patterns (13 new assertions)
- test_indreln.lem: inequality, nested fn application, ordering, and
  multi-rule relations in antecedents (4 new relations)
- test_mutual_recursion.lem: heterogeneous param counts (caught the
  Type 1 bug), 3-way mutual recursion
- test_audit_regressions.lem: tabs in comments, type/record defs

Total: 31 tests, 231 assertions, all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Honest accounting of every gap: 942 sorry stubs in Machine_word,
wrong floating-point types, missing overflow semantics, 18 partial
defs, incomplete target rep coverage, indreln \!= edge case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…annotations

Propositional equality in indreln antecedents now handles both the Infix
AST path (direct = / <> syntax) and the App AST path (Lem's <>
decomposition to not(isEqual x y)). Extracted check_beq_target_rep
helper to share logic between both cases. Added regression tests using
(nat -> nat) types which lack BEq and would fail without the fix.

Added {lean}-scoped termination annotations for 10 structurally recursive
library functions (map_tr, count_map, splitAtAcc, mapMaybe, mapiAux,
catMaybes, init, stringFromListAux, concat, integerOfStringHelper),
reducing partial def count from 18 to 8.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…FixedPoint

Convert 5 more partial defs to total:
- LemLib.lean: boolListFromNatural (n/2 division), bitSeqBinopAux (dual-list recursion)
- LemLib.lean: lemStringFromNatHelper, lemStringFromNaturalHelper (n/10 division)
- LemLib.lean: lemLeastFixedPoint (bounded countdown)

Add Lean-only target reps in string_extra.lem and set.lem to route
generated code through the total LemLib implementations. All changes
are inherently Lean-scoped (declare lean target_rep / hand-written Lean).

Add TODO rems-project#7: audit all pre-existing unscoped termination annotations
from upstream to verify they don't affect other backends.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
septract and others added 30 commits August 22, 2026 14:12
…ion (be:G5 half-fix)

Backend_common.lean_module_is_library is now THE library test for the
Lean target (exported via the .mli): both former inline copies — the
name transformation in get_module_name_from_descr and the
transitive-opens scan in lean_backend.ml:5236-5245 — call it. The
"implemented twice" half of be:G5 is closed.

REGISTERED RESIDUAL (the other half, price M, design in-code): the test
is still the {coq}-module-rename PROXY (every library .lem declares
one; a user module legally could, silently reclassifying it). The
principled marker is a source-path test (md.mod_filename vs lem's
library paths — the Isabelle mod_filename branch is the precedent), but
lib_paths_ref lives in main.ml which backends cannot depend on;
threading it cleanly needs a Backend.Make/def_ctxt parameter, NOT
another mutable side channel (the be:G3 lesson). The proxy now has one
normative statement, at the one implementation.

Verified: lem make green; tests/comprehensive make lean exit 0;
BYTE-NEUTRAL at cerberus scale (193 generated files sha256-identical
to the B4/B6 generation).

Pin-dance class: BACKEND-ONLY.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-tie, reserved-name contract (R1 R2 R3, be:S1 S2 S12)

R1 (the be:G4 class, professor B): lem `transitiveClosure`'s lean inline
now routes through transitiveClosureByCmp setElemCompare (mirroring the
ocaml inline) onto the NEW comparator-keyed LemLib.set_tcByCmp — the
ByEq route joined/deduped by BEq and could MISS comparator hops (pinned:
the SetCoherence closure leg shows set_tc missing the (0,·)→(2,·) hop
that set_tcByCmp takes over comparator-EQ/BEq-distinct middles).
be:S12 RIDES ALONG: both closures now share the fuel-totalized set_tc_go
((2|r|)²+1 bound, loud fuelExhaustedWith arm) — the `partial` is gone.
SetCoherence gains the closure legs (divergence pin + no-dup/idempotence
over adversarial pair pools).

R3 (the third default tie): the class emitter's comparator-derived BEq
bridges ([SetType a]/[MapKeyType a] : BEq a) drop to (priority := 500) —
a comparator can be COARSER than a type's own equality, so the isEqual
bridge/derived BEq (1000) now win by PRIORITY, not declaration order.
Lattice note updated (tie de-tied; two justified ties remain).

R2: the St header's claim narrowed to "every cell OF LEAN_BACKEND.ML"
(the two out-of-file cells named: on_cr_simple_applied + the
process_file pre-call write — the registered be:S15 residual).

be:S2 (probe-first, the backend A- gate): PROBE MEASURED FIRST —
a fuel'd def with a parameter named lemFuel generated
`(lemFuel : Nat) (lemFuel : Nat)`: the worker matched the USER's
binder, so shadow_probe 0 3 returned the 999 SENTINEL instead of 0
(verbatim in neg_fuel_shadow.lem). Fix: the RESERVED-NAME CONTRACT
(doc/notes/2026-08-22_arc14-reserved-names.md) + a located
generation-time reserved_binder_check ('lemFuel' + the '_lemReader_'
prefix, over every fuel'd/seed/lifted clause's parameters). Negative
probes: neg_fuel_shadow.lem + neg_reader_shadow.lem (both fire with
the declared message; clean corpora unaffected). Registered residual:
body-level match binders (contract note carries it).

be:S1 (probe-first): the none-binder capture class is DEFENDED by lem's
avoid machinery — probe-measured (binder `none` renames to none1 in
pattern AND body, semantics correct); pinned as a build-failing
regression guard (test_name_capture.lem + TestNameCaptureCheck.lean)
rather than "fixed". The keyword-list classification remainder stays
registered (be:S1 residual).

Verified: lem make green; make lean-libs (Basic_classes/Map bridges at
500; Relation closures comparator-spliced); lean-lib lake build 35/35
incl. the new closure guards; tests/comprehensive make lean exit 0
(41 generated, negative probes 11/11 incl. the two new, panic pin
green). Cerberus-scale validation follows with the pin bump (output-
CHANGING batch: zero standing-corpus movement is the bar).

Pin-dance class: BACKEND + LEAN-LIB (Lake bump required).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The probe-first scratch (probe_capture/probe_fuel_shadow sources + their
generated outputs) was accidentally swept into the basket commit 3cb656f
by its git add -A; the probes' evidence lives where it belongs (the
negative suite + the reserved-names note + the commit message). Plain
deletion commit — no history surgery (D-series preference).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ie probe; lattice-note fixes

RG1 (professor A's hole — THE item withholding the backend A-): the
reserved-binder check now scans COMPILED CLAUSE BODIES, not just
parameters: new exp_bound_names walker (every match/let/fun/do/
quantifier binder in the body, conservative over-collection) feeds the
same located fail-closed check. This simultaneously DISCHARGES the
registered body-level residual from the previous basket (the contract
note updated; the in-code coverage comment corrected from
"Residual: not yet scanned" to the RG1 coverage statement). A's two
witnesses are now negative probes, verbatim shapes:
  neg_fuel_shadow_body.lem  — body tuple binder lemFuel in a fuel'd
    def: was silently wrong-fuel; now "Error: Lean backend: binder
    'lemFuel' collides with a reserved synthesized binder ..." at the
    clause location.
  neg_reader_shadow_body.lem — the witness that COMPILED AND RAN
    SILENTLY WRONG pre-RG1 (use2 100 (1,2) = 5, not 103): body tuple
    binder _lemReader_amb in a reader-lifted def; now the same located
    error.

RG2: the de-tie probe leg — prio_coarse (coarse model SetType beside
derived structural BEq) added to test_instance_priority.lem +
TestInstancePriorityCheck (a separately-importing module): `==` must
resolve to the derived BEq. PLANT (measured, honest): reverting the
bridge to default priority did NOT flip the guard — at the restored
tie the derived instance still wins by newest-declaration order (the
Basic_classes bridge predates every derived instance), so the de-tie
converts that order argument into a priority argument rather than
changing today's winner; recorded in the lattice note. Rebuild-after-
revert green (plant discipline).

RG3: the lattice note self-contradiction fixed — the SetType->BEq
bridge now appears at exactly ONE priority row (500); the 1000 row
keeps only the isEqual bridge. RG4: the probe filename cite corrected
(test_instance_priority.lem).

Verified: lem make green; make lean-libs; lean-lib lake build green;
tests/comprehensive make lean exit 0 (13 negative probes incl. the two
new body-level ones, all rejected with the declared message; the RG2
guards green de-tied AND at the planted tie).

Pin-dance class: BACKEND + LEAN-LIB (Lake bump required).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-note headers

Front door README (what the backend is, provenance, verified
quickstart, properties-of-the-generated-code trust story, how to
check it, honest status) + DESIGN.md (pipeline, load-bearing choices
each verified against src/lean_backend.ml and lean-lib, declare
vocabulary, proof-amenability paragraph) under doc/lean-backend/;
small lean-lib/README.md; minimal top-level pointer + provenance.
Three April-era doc/notes/ records gain HISTORIC/superseded headers
(originals verbatim below; front docs cite only current mechanisms).
Verified: quickstart run end-to-end (demo.lem -> Demo.lean), root
make green, tests/comprehensive make lean green (generation +
compile + panic pin + negatives), lean-lib lake build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Finding classes addressed (docs-only; no code changes):
- INACCURATE: fuel-declare backtick payload documented as the default
  fuel N — it is the exhaustion SENTINEL expression; the wrapper applies
  lemDefaultFuel (10^6). Fixed in README bullet, DESIGN example/prose,
  and the declare table; loudness now correctly attributed to the
  fuelExhausted convention.
- INACCURATE: namespace claim (user modules are top-level; Lem_ prefix
  applies to library modules, LemLib.Set -> Lem_Set; the Std -> Lem_Std
  example did not exist).
- INACCURATE: set comprehensions described as rejected only over
  function-carrying types — ALL live comprehensions are a
  generation-time error; now stated as its own limitation in both
  front docs.
- MISSING: declare table lacked effectful val / reader val /
  reader_seed val (the effect boundary's own control surface); added.
- MISSING: no self-check line for the one-axiom claim; added the
  verified grep.
- CONFUSING: 'share semantics by construction' overstated — reworded
  to same-source + divergence-is-a-backend-bug with the differential
  evidence pointer intact.
- PM-LEAK/JARGON: 'registered residuals/refactor', consumer
  'content-hash pin', 'gate-enforced'/'build gates', 'absence gates'
  rephrased in plain terms.
- HISTORY-LEAK/header accuracy: effectful_target_reps historic header
  wrongly said the note's design 'did not survive'; the shipped design
  refines its Option 3 — header now says so.

Verified: quickstart runs as written (make; ./lem -wl ign -i
library/pervasives.lem -lean demo.lem; cd lean-lib && lake build);
repo-root make green; tests/comprehensive make lean green (panic legs
+ 13 negative probes OK).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The re-mark's adversarial pass caught the reviewer's own rewrite: both
front docs claimed ALL live set comprehensions are a generation-time
error. Wrong — comprehensions with IN-bounded binders are macro-expanded
by Lem's front end into comparator-keyed folds and compile fine
(verified: user-code '{ e | forall (e IN s) | e < 3 }' generates a
setFold body, exit 0; the library's filter/bigunion/map are exactly
this). Only the forms that survive expansion — unbounded
'{ x | condition }' and unexpandable binder shapes (e.g. set.lem sigma's
dependent bound) — are rejected; comment-rendering applies to inline- or
target_rep-mapped library defs (sigma is inline{lean}-mapped, not
target_rep'd as previously stated). Both passages rewritten to say
exactly that; negative/neg_setcomp.lem's own commentary is the spec.

Validation: root make green tail; tests/comprehensive make lean exit 0
(panic legs + all negative probes OK).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…limination path; [USER 2026-08-24] ruling)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…berus consumer

Customer contract (universal axiom census), ruled decisions Q1a-Q4 with
provenance, survivor allowlist classification, arc structure incl. the L0
fix-first slice, and the four review questions for the consumer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rus, two asks (entry shape, adoption-pin manifest), three non-goals

Appended in-place by the refined-cerberus orchestrator per operator
instruction; committed verbatim with attribution preserved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erDiv mapping, char escapes, setChoose), 10 minor, arc attach-point advice

Reviewer report reproduced verbatim; committed by orchestrator as the record.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The seven Lean-backend annotation words (fuel, reader, effectful,
ground_rep, reader_seed, skip_instances, extra_import) were hard lexer
keywords (src/lexer.mll:134-140), breaking them as identifiers for ALL
lem users on ALL targets (2026-08-31 backend quality review, M1).

Fix: parser.mly's identifier nonterminal `x` gains one production per
word, reducing each token back to an ordinary identifier. The tokens
act as keywords only in the `declare` productions — the sole grammar
positions expecting them, and positions where `x` can never occur, so
ocamlyacc conflict counts are unchanged (verified: 2 shift/reduce,
2 reduce/reduce, 5 never-reduced rules, before and after). The lexer
is untouched; any word this arc adds (`supply`) must ride the same
mechanism, per the effect-retirement charter §3.2.

Verified:
- All seven words as let-bound names generate correctly for BOTH
  -ocaml and -lean (14/14 probes; the review's reproducer
  `let fuel = (1:nat)` included).
- New standing acceptance test tests/comprehensive/
  test_contextual_keywords.lem (words as value names, function name,
  parameters, match-pattern variables, record fields + a live
  `declare {lean} fuel` alongside); added to the lean-test lakefile
  roots (the roots list is explicit — without this the asserts
  silently do not run); 6/6 asserts PASS.
- tests/comprehensive `make lean` green: 41 passed / 0 failed / 0
  skipped generation, Build completed successfully (118 jobs), panic
  pin both legs OK, 13/13 negative probes still rejected with their
  declared errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New golden-hash net guarding the 9 non-Lean emitters (ocaml hol isa
coq html tex lem ident tex_all) against fork drift (2026-08-31 backend
quality review m2; charter L0 item ii). tests/nonlean-regress/run.sh
generates the library corpus (the library/Makefile LIBS list) and every
tests/backends/*.lem source per emitter into a scratch tree, hashes
every artifact (generated files + captured stdout/stderr) and records
every exit code, and compares against the committed goldens
(893 artifact rows, 216 exit rows). Any drift — changed bytes,
missing/new artifact, changed exit code — exits 1 naming the target
and file. Vacuity-guarded (minimum row counts; empty corpus fails).
Absolute repo paths in outputs (tex_all headers, error messages) are
normalized to LEMROOT before hashing so the manifest is
worktree-portable. Rebaseline is explicit-only
(NONLEAN_REGRESS_REBASELINE=1) and documented in the script header.

Baselined AFTER the M1 contextual-keyword fix, with the mandated
invariance check: the same net script run against a scratch build of
pre-M1 lem (e591865) produced BYTE-IDENTICAL manifests (893/216 rows)
— the keyword fix changed no non-Lean output.

Plant-tested both ways:
- emitter perturbation (extra space in the OCaml backend's
  pat_wildcard, src/backend.ml:850): net trips, naming exactly the
  ocaml rows (backends/ocaml/{coq_exps_test,exps,pats,pats3}.ml,
  lib/ocaml/lem_*.ml, ...); reverted, net green.
- golden perturbation (one flipped manifest hex digit): exit 1 naming
  the row; restored, net green (exit 0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clause grouping was implemented twice with different keys: the
failwith-thread pre-pass grouped Fun_def clauses by cref
(lean_backend.ml:1129-1138) while the emission path grouped by name
STRING (:2140-2148) — a divergence trap flagged by the 2026-08-31
backend quality review (notes) and mandated as charter L0 item iii
before the supply pre-pass adds a third traversal.

Both now call one shared lean_group_funcls (cref-keyed,
first-appearance order preserved); the coming supply pre-pass is its
intended third consumer.

Verified:
- tests/comprehensive generated Lean tree BYTE-IDENTICAL before/after
  (sha256 over all 82 generated .lean files, diff empty; generation
  41/41 both runs).
- make nonlean-regress green (893 artifact rows byte-identical — the
  change is Lean-path-only, asserted not assumed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The multi-name destructuring Let_def emitter duplicated the RHS per
bound name (lean_backend.ml:2078-2126) — an effectful RHS ran its
effect once per binding where OCaml runs it once, and a threaded
supply draw there would be a silent numbering fork (2026-08-31 backend
quality review m7; charter L0 item iv, mandated before any supply
threading touches Let_def).

Multi-name lets now emit ONE private def (lemLetRhs_<names>, typed
from the RHS expression) plus per-name projection defs that
destructure it; single-name lets keep the historical emission
byte-for-byte. Reader-lifted multi-name lets re-inject the reader
parameters at the projections' reference to the RHS def; [Inhabited]
threading binders ride both (the pre-pass records one demand for the
whole Let_def). A user val colliding with a synthesized lemLetRhs_*
name fails loudly at Lean compile time (duplicate def).

Generated-tree byte-diff over tests/comprehensive (82 files): ONLY
Test_let_bindings.lean changes, at exactly its 3 multi-name sites —
(pair_a, pair_b), (tri_x, tri_y, tri_z), (nest_a, (nest_b, nest_c)) —
each gaining one private RHS def, projections otherwise identical.

New compiled-binary single-evaluation pin (suite phase
lean-tuple-once): test_tuple_let_once.lem binds
`let (first_draw, second_draw) = tick_pair ()` over an effectful
counter (hand-written TupleLetTick.lean); TestTupleLetOnce asserts
the bound values are exactly (1, 2). The RHS is deliberately ONE
opaque effectful call — a literal-tuple RHS is projection-simplified
by the Lean compiler, which erases sibling draws and masks the
duplication (measured: the old emitter's literal-tuple output also
prints (1, 2); its IR evaluates one component per def).
Red-green: old emitter (pre-fix lem @ e591865) FAILS the pin with
"draws: first=1 second=4 ... got (1, 4), want (1, 2)" (exit 1);
fixed emitter passes "draws: first=1 second=2" (exit 0).

Gate: tests/comprehensive make lean green — Generation 42/42, Build
completed successfully (121 jobs), panic pin both legs, the new
single-evaluation pin, 13/13 negative probes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 2026-08-31 backend quality review (M2) and the effect-retirement
charter (§8.3 rider) claim library/num.lem's Lean mapping of
integerDiv to `/` (Int.ediv) diverges from an OCaml oracle using
zarith Z.div (truncation toward zero). VERIFIED FALSE at the actual
call target ([AGENT], evidence below): lem's OCaml integerDiv is
`Nat_big_num.div` (num.lem:1405) = `BI.div_big_int`
(ocaml-lib/nat_big_num.ml:31) = `Big_int_Z.div_big_int` — zarith's
num-compatibility layer, which is EUCLIDEAN, not Z.div. Measured
(compiled probes, verbatim):

  Nat_big_num.div:     (-7,2)->-4 (7,-2)->-3 (-7,-2)->4 (7,2)->3 (-1,3)->-1 (1,-3)->0
  Nat_big_num.modulus: (-7,2)->1  (7,-2)->1  (-7,-2)->1 (7,2)->1 (-1,3)->2  (1,-3)->1
  Lean / (4.28.0 AND 4.32.2): [-4, -3, 4, 3, -1, 0]   (rfl-proved = Int.ediv)
  Lean % (4.28.0 AND 4.32.2): [1, 1, 1, 1, 2, 1]      (rfl-proved = Int.emod)

Perfect agreement at every signed corner. Changing integerDiv to
Int.tdiv (the review's remedy) would INTRODUCE the divergence it
warns about; integerMod already matches (the review's own emod≡erem
observation, independently confirmed). Per mirror-OCaml doctrine and
the slice instruction to change only what mismatches: NOTHING is
changed in library/num.lem or lean-lib/LemLib/Num.lean.

What this commit adds is the agreement PIN the review's concern
deserves: test_integer_div.lem (12 elaborator asserts over the six
signed corners + the //mod operator routes) and the compiled-binary
leg TestIntegerDivParity (suite phase lean-div-parity — runtime Int
arithmetic is GMP-backed, so the compiled agreement is asserted, not
assumed). The OCaml leg of the SAME .lem was compiled against
ocaml-lib extract.cmxa and printed (verbatim):
  div: [-4, -3, 4, 3, -1, 0]
  mod: [1, 1, 1, 1, 2, 1]
  op: -4 1
byte-identical to the Lean binary's values.

Downstream note (charter C1): since the mapping is unchanged, NO
generated-Lean semantics change reaches cerberus-lean at the next pin
bump from this item. The charter's §8.3 rider text and the review's
M2 entry need an operator-visible correction — escalated in the L0
slice record.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
L_char rendering used OCaml Char.escaped, whose decimal escapes
('\200') are invalid Lean — loud wrong output on legal lem input
(2026-08-31 backend quality review M3; reproducer #'\200'). New
lean_char_escape mirrors Char.escaped for the cases it got right
(ASCII printables, named controls, quote/backslash) and emits \xHH
for non-printable/non-ASCII bytes — the latin1 embedding ('\xc8' =
U+00C8), the same convention as Ulib.Text.of_latin1 on the backend's
OCaml side.

The paired lean_string_escape is hardened per the review's adjacency
note: control bytes < 0x20 are now hex-escaped ('\000' keeps its
exact old "\x00" spelling); bytes 0x80-0xFF deliberately continue to
pass through RAW, now documented in-code — lem's lexer admits only
UTF-8 source, so those bytes arrive exclusively inside multi-byte
UTF-8 sequences, and per-byte \xHH escaping would decode-shift the
text (Lean's \xHH is a Unicode scalar, not a byte).

Test: new char-escape section in test_strings_chars.lem — the
review's #'\200' reproducer, NUL/SOH/DEL/0xFF corners, and
decimal-vs-hex cross-notation equalities (#'\200' = #'\xC8',
#'\255' = #'\xFF'); 4/4 asserts PASS. Old emitter on the same
section produces '\200'/'\000' (verified invalid-Lean shapes).

Generated-tree byte-diff: only Test_strings_chars(.aux).lean change,
and pre-existing content is byte-identical (the diff is a pure append
of the new section — no output churn from the escaping change on the
existing corpus). Gate: make lean green (Generation 43/43, 123 jobs,
all pins, 13/13 negative); make nonlean-regress green (Lean-only
change, asserted).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LemLib's setChoose returned the newest-inserted head; OCaml
Pset.choose is min_elt (ocaml-lib/pset.ml:297,358) — the comparator
MINIMUM. The order is observable downstream (cerberus
Core_linking.topo_order emits linked definitions in choose order),
and the divergence carried no in-code note (2026-08-31 backend
quality review M4).

Decision [AGENT], per the review's preferred remedy and mirror-OCaml
doctrine: mirror comparator-minimum. The list representation makes
this trivial (a fold; comparator-EQ ties cannot arise under the
no-EQ-duplicates representation invariant), so the
document-the-divergence fallback is not needed. setChoose now takes
the comparator, spliced at call sites exactly like setAddBy:
set_extra.lem's Lean rep becomes
  declare lean target_rep function choose s = `setChoose` `setElemCompare` s
(the insert/`setAddBy` splicing pattern, library/set.lem:352-355).

Tests: new pin in test_collections.lem — choose {6;1;2} = 1
(comparator-minimum), choose {5} = 5; both PASS. Gate: make lean
green (43/43 generation, 123 jobs, all pins, 13/13 negative);
lean-lib lake build green (35 jobs).

nonlean-regress: TRIPPED as designed on the source-echoing targets —
the drifted rows are exclusively tex/tex_all/html/-lem/ident
renderings of set_extra.lem (and files embedding it), whose content
change is the edited `declare lean` line itself (verified verbatim in
the -lem echo: line 26 only). The semantic code emitters
(ocaml/hol/isa/coq) are byte-identical — zero drifted rows.
Goldens rebaselined with this justifying change per the net's
protocol.

Downstream note (charter C1): at the next lem pin bump,
generated-Lean call sites of `choose` change text
(setChoose setElemCompare ...) and cerberus topo_order's Lean-side
choose order changes from head to comparator-minimum — expected to
MATCH the oracle where it previously diverged; the C1 differential
battery is the gate, and any linked-emission-order baseline movement
there is this item's expected signature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gains lemDefaultFuel + lemLetRhs_ (with negative probes)

m1 (2026-08-31 backend quality review): backend.ml's
Decl_extra_import human-target echo used the quote-string form
(`declare {lean} extra_import "Foo"`), which the fork's own parser
rejects — the historical declare-echo regression class. Now the
backtick form, matching the Decl_fuel/Decl_ground_rep echoes.
Verified: `declare {lean} extra_import \`Foo\`` -lem-echoes verbatim
and the echoed output re-parses (round-trip rc=0; pre-fix the re-parse
was a syntax error at the quote).

Reserved-name contract (review notes + m7 follow-through): new
generated-DEF-NAME leg lean_check_reserved_def_name, enforced at both
Fun_def (funcl_aux) and Let_def emission — 'lemDefaultFuel' (a user
def of that name silently rebinds every fuel wrapper's budget: the
wrappers are point-free `worker lemDefaultFuel`) and the 'lemLetRhs_'
prefix (the m7 synthesized RHS family; the collision moves from a
Lean-compile-time duplicate-def error to a located generation-time
error). Two new negative probes
(neg_default_fuel_name, neg_let_rhs_name) assert the declared error
fragments — the negative suite is now 15/15.

Gate: make lean green (43/43 generation, 123 jobs, all pins, 15/15
negative probes incl. the two new ones); make nonlean-regress green
(893 rows byte-identical — no extra_import in the net corpora, and
the def-name check fires on no library/backends input).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Slice record for the effect-retirement L0 (fix-first) slice: 7 items,
per-item commits and dispositions, plant evidence verbatim (net
emitter/golden plants, m7 red-green), the item-3/4 generated-tree
byte-diff summaries, the M2 VERIFIED-NO-DEFECT escalation (the
review/charter premise about Z.div is factually wrong — lem's OCaml
integerDiv is Euclidean via Big_int_Z; mapping unchanged, parity
pinned), the M4 mirror decision [AGENT], and the C1 downstream notes.
Close-out battery at cc05225 quoted verbatim (43/43 generation, all
pins, 15/15 negative, nonlean-regress 893/216 rows, lean-lib green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eading (charter §3.2)

The state analog of the reader lifting, on its pipeline: contextual-
keyword grammar row (Decl_supply, no new hard keyword), typecheck
Targetset field on const_descr, lean_supply_prepass fixpoint at
Val_def granularity (arity-recording; reader_seed defs liftable;
Lean-target_rep'd defs excluded — dead bodies), and a supply-threading
body transform (supply_thread/supply_block) that A-normalizes draws
and lifted calls in left-to-right depth-first order. Lifted defs take
one (_lemSupply_<name> : Nat) binder per declared supply after the
reader binders and return the value×supply pair; a draw emits
LemLib.supplySplit s = (s, s+1) (new plain def in lean-lib —
kernel-transparent, no axiom/IO). Deterministic state-passing only
(charter O7): the transform emits lets, tuples, supplySplit, and the
source's own control forms — no ND constructor exists in its emission.

Fail-closed guards, each generation-time with a named message and a
negative probe: G-λ (draw under lambda), G-bare (bare lifted/supply
reference — no partial-application repair exists), G-inst (instance
methods), G-rel + the general net (supply constants reached by any
non-threaded emission: indreln, asserts, infix), G-arity + exact
threading arity at call sites, G-type (supply val must be
unit -> nat), annotation-mix (supply × effectful/reader/reader_seed),
truly-mutual and >1-clause groups (v1, extend-on-need; multi-clause
SOURCE defs are pattern-compiled to one match and thread fine —
positive-tested). Reserved-binder contract gains the _lemSupply
prefix. Composition: fuel (worker threads supply through decremented
self-calls; zero-fuel arm returns sentinel with supply unconsumed;
wrapper type gains supply arrows), reader and reader_seed (binder
order [Inhabited] readers supply), multi-name destructuring lets ride
the L0 single-RHS emitter (one threaded RHS call per projection),
multi-supply (sorted-name binder/state order, independent streams).

Verified: tests/comprehensive 45/45 generation + build green;
24/24 negative probes (9 new neg_supply_*); compiled draw-sequence
binary green (new phase lean-supply-draws; TestSupplyCheck.lean rfl
pins = kernel-checked O1 draw-order evidence); new lean-invariance
phase green (ocaml/hol/isa/coq byte-identical with/without the
declares; both plants fired red); make nonlean-regress byte-identical
(893/216 rows); pre-existing comprehensive corpus regenerated
byte-identical vs a 4fd4d50 scratch lem (86 files); lean-lib lake
build green; ocamlyacc conflict counts unchanged (2 s/r, 2 r/r).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n at extern boundaries (charter §4.2)

A target_rep'd val declared a reader CONSUMER gets all declared reader
parameters passed as extra leading arguments at every generated call
site (global sorted reader order, before its own arguments); callers
are lifted by the ordinary reader fixpoint (a consumer use counts like
a reader use); bare/HOF references repair by type-preserving partial
application over the reader parameters, exactly like lifted-def
references. Injection routes through reader_inject_name — the
lifted-def resolver — so inside a reader_seed def the SEED's first
argument is picked up instead of the binder, with no new seed
machinery (probe-verified: seed defs emit '(Impl.f seedarg) x').

Fail-closed guards per the charter: RC-rep (identifier-form Lean
target_rep required — missing, parameter-binding, and non-simple rep
forms each rejected with named messages, swept for every
consumer-marked constant even if unused), RC-mix (consumer ×
reader/reader_seed/supply/effectful), RC-inst (instance methods hit
the existing instance guard via the extended exp_needs_reader),
RC-rel/scope (a consumer call anywhere without a reader value in
scope — indreln rules, lemmas/asserts, non-lifted contexts — is a
generation-time error), infix-position rejection (both in ordinary
emission and inside the supply transform), and a supply-transform
head case so consumer calls with supply-drawing arguments inject
readers correctly.

Verified: tests/comprehensive 46/46 generation + build green;
29/29 negative probes (5 new neg_rc_*); compiled injection binary
green (new phase lean-reader-consumer; TestReaderConsumerCheck.lean
rfl pins for lifted-caller/HOF/seed paths; .lem assert covers the
seed-rooted entry); lean-invariance green incl. new
inv_reader_consumer.lem (ocaml/hol/isa/coq byte-identical);
make nonlean-regress byte-identical (893/216 rows); pre-existing
corpus regenerated byte-identical vs the 4fd4d50 scratch lem (84
files; test_contextual_keywords source deliberately extended with the
new word). reader_consumer rides the L0 contextual-keyword mechanism
(usable as an ordinary identifier everywhere).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…§8.3)

The numeric fuel declare form sets the WRAPPER's budget literal for
exactly that declaration — one more Targetmap field (fuel_budget)
beside fuel_sentinel, flowing the existing path (parser Num row →
typecheck Decl_fuel_budget case → fuel_budget_for at the wrapper
emission), replacing ' lemDefaultFuel' only when a budget is declared.
HARD CONSTRAINT honored structurally (consumer-ratified, charter
§8.3): OPT-IN ONLY — an unannotated declaration emits the identical
' lemDefaultFuel' literal, byte-for-byte (pre-existing corpus
regenerated byte-identical vs the 4fd4d50 scratch lem, 84 files; all
existing fuel tests untouched).

Fail-closed guards (swept for every budget-marked constant even if
unused): budget without the sentinel declare (would silently do
nothing) and non-positive budget literals are generation-time errors
with named messages; negative probes neg_fuel_budget_nosentinel /
neg_fuel_budget_zero. The human-target echo emits the plain numeric
literal (round-trips through lem's parser). The budget composes with
reader lifting (wrapper keeps the reader-prefixed type) and with
supply lifting (test_supply.lem fuel_draws_b: exhaustion mid-stream
returns the partial draw list with the supply at the cut, budget 3).

Verified: tests/comprehensive 47/47 generation + build green; 31/31
negative probes; compiled test green (new phase lean-fuel-budget,
TestFuelBudgetExec.lean): budgeted bspin cuts at exactly 5 while the
unannotated sibling dspin completes depth 999,999 and cuts at
1,000,000 — the exact lemDefaultFuel boundary witnessed in a real
binary; lean-invariance green incl. new inv_fuel_budget.lem (7
artifacts byte-identical across ocaml/hol/isa/coq); make
nonlean-regress byte-identical (893/216 rows); lean-lib lake build
green (untouched by this feature).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ompiled evidence, invariants (+ late G-infix probe)

Adds the slice record (per-feature charter-§ conformance statements,
guard-to-probe mapping, verbatim compiled-test and invariant
close-out evidence, deviations flagged) and the late-found G-infix
negative probe (an operator-named drawing def used infix IS
lem-expressible and fires the guard; suite now 32/32 negative).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…+ minors 1-5 + guard-leg probes

MAJOR-1 FIXED: supply_thread hoisted right-operand draws above && / ||
(Lean's macro_inline and/or short-circuit; the pre-transform code and
the OCaml oracle draw nothing on the short-circuit path — an O1
violation, maskable by id-canonicalized differentials). Fix:
lean_shortcircuit_kind classifies CR_infix &&/|| heads; a drawing
RIGHT operand now threads as a branch arm of the equivalent if
(a && b ≡ if a then b else false; a || b ≡ if a then true else b) via
supply_shortcircuit — draws fire only when the operand evaluates; the
left operand stays strict; pure right operands keep the old path
byte-for-byte; the application-spine leg gets the same treatment when
fully applied and fails closed otherwise. --> (imp) inlines to || and
rides the same path. Tests: sc_and/sc_or/sc_both/sc_nested/sc_imp in
test_supply.lem with 15 kernel-pinned rfl results in
TestSupplyCheck.lean (incl. sc_and 10 false = (false, 10) and the ||
duals) and 4 new compiled checks in the draw-sequence binary.

minor-1: lean_param_dup_check on both reader and supply param lists —
duplicate unqualified constant names (conflating binders) are a loud
error naming both paths (probes neg_supply_dupname, neg_reader_dupname).
minor-2: a supply val with a live lem definition and no Lean
target_rep (one constant, two semantics) is rejected in the pre-pass
(probe neg_supply_defbody).
minor-3: fuel budgets on target_rep'd vals (rep leg) and on constants
no Fun_def defines (invocation-wide completeness leg via
lean_analysis_prepass_all, which sees every module pre-emission) are
loud errors (probes neg_fuel_budget_rep [p8], neg_fuel_budget_speconly
[p3]).
minor-4/5: record corrected in place (probe-breakdown arithmetic; the
non-reproducing ND grep replaced by the sharper
grep -nE '\bND\.[a-z]|msum' — no matches, quoted verbatim).
notes(a): probes for the previously unprobed guard legs —
neg_supply_mix_reader, neg_supply_mix_seed, neg_rc_mix_supply,
neg_rc_infixrep.

Invariants re-verified (addendum in the L1 record, verbatim): full
battery green — 47/47 generation, build (134 jobs), all compiled pins
(supply-draws now 12 checks), 41/41 negative probes; nonlean-regress
byte-identical (893/216 rows); the FULL a51615e corpus (94 generated
files incl. the supply tests at their a51615e state) regenerated with
an a51615e-scratch lem vs this tree's lem: diff -r exit 0 — ZERO
emission changes (no base-corpus file has a draw under a
short-circuit; the sc_* shapes are new source in this commit);
lean-lib build green (untouched); ocamlyacc counts unchanged (2 s/r,
2 r/r). C1-brief obligation recorded (audit deviation-4): explicit
cone check that no supply-lifted Let_def-bound top-level VALUE sits
in the adopted lifted cone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in-repo

Closes the as-relayed provenance gap the charter R3 flagged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Effect-retirement arc L2 (charter section 7.1, cerberus-lean
lean_frontend/docs/2026-08-31_effect-retirement-design.md @64dd6efeb),
executed after C1 (the cerberus consumer @ 1b40098ed carries zero
effectful declares and zero wrap occurrences in generated code).

Deleted:
- lean-lib/LemLib.lean: the runEffectful axiom, runEffectful_impl,
  their implemented_by/never_extract/noinline attributes and the
  scaffold commentary; a HISTORY note remains (DAEMON precedent).
  LemLib now declares ZERO axioms. supplySplit stays.
- src/lean_backend.ml: the call-site wrap emission (is_effectful +
  the thunk wrap in the App-Constant branch), the whole
  exp_contains_effectful attribute machinery (effectful_attr on
  Let_def emissions, attr_for on Fun_def groups and fuel wrappers),
  and the now-unreachable transitional guards (supply x effectful
  mix, RC-mix effectful leg, effectful-head-with-drawing-args).

Retained + refused [AGENT decision, orchestrator-prepared]: the
effectful ANNOTATION (lexer/grammar word, Decl_effectful, the
const_descr.effectful field) stays for other targets' potential use;
its Lean-target handling is a FAIL-CLOSED generation-time refusal
(lean_effectful_retired_check, run at every pre-pass entry, fires
even for unused vals) naming supply lifting as the migration path
with the charter cite. This diverges from the charter's parse-error
plant P1 deliberately: grammar stability for other targets +
fail-closed doctrine + the refusal documents the migration; it also
keeps the non-Lean emitters byte-identical.

Tests converted:
- test_target_reps.lem section 7 (the positive effectful wrap test)
  replaced by negative/neg_effectful_retired.lem (EXPECT the refusal).
- negative/neg_supply_mix.lem deleted (the transitional mix guard is
  gone; superseded by the refusal probe). Negative count 41 -> 41.
- test_tuple_let_once.lem (the m7 single-evaluation pin) converted
  off the retired mechanism: TupleLetTick.lean now hand-writes the
  pure-typed impure extern (opaque + implemented_by unsafe impl +
  never_extract armour in the test scaffold); the pin's observable
  is unchanged ((1,2) vs (1,4)).

Docs: doc/lean-backend/{DESIGN,README}.md and lean-lib/README.md
rewritten from "one axiom at the effect boundary" to the zero-axiom
story as it now IS.

Verified: root make green; lean-lib lake build green (35 jobs);
comprehensive suite exit 0 (generation 47/47, negatives 41/41 incl.
neg_effectful_retired rejected-as-declared, m7 pin OK, panic pins
OK, compiled phases OK, invariance OK); nonlean-regress OK (893
artifact rows, 216 exit rows, 9 emitters, byte-identical to golden);
grep: zero runEffectful in src, lean-lib clean except the mandated
HISTORY note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The three registered L2 riders (charter §3.2 @64dd6efeb; audit log
2026-08-31_arc-audit-log.md NOTE-1):

(a) Paren-split spine strictness pin (L1 delta audit NOTE-1):
    test_supply.lem prefsc_paren/chk — `((&&) a) (chk 4)` renders
    through the supply transform's general-head branch (strip_app_exp
    does not unwrap Paren) and threads STRICTLY; four kernel rfl pins
    in TestSupplyCheck.lean (incl. the strict signature
    prefsc_paren 10 false = (false, 11), vs sc_and 10 false =
    (false, 10) flat-form short-circuit), plant-tested red-green.
    In-code notes at the general-head branch (lean_backend.ml) and at
    strip_app_exp (typed_ast_syntax.ml) documenting the
    oracle-faithful coincidence and the re-adjudication duty on any
    future spine normalization.

(b) M2 erratum appended to 2026-08-31_backend-quality-review.md
    (dated section, per charter §8.5): VERIFIED-NO-DEFECT — the OCaml
    chain (num.lem:1403 -> Nat_big_num.div -> Big_int_Z.div_big_int)
    is Euclidean, agreeing with Int.ediv; M2's remedy would have
    introduced the divergence. Residue kept: the four hand-written
    Z.div seams (vip impl_mem.ml:1021/:718, concrete
    impl_mem.ml:1393/:1967) with the CerbMem.lean:985 Int.tdiv
    parity note.

(c) Q1b-rescope notice appended to
    2026-08-31_effect-retirement-external-review.md (R3.1 rider,
    addressed to the consumer): the §2 "one order-moved site" /
    escalation-clause / behavior-preserved statements are superseded
    per charter §3.6.1/O2/O6 and the C1 adjudication
    [USER 2026-09-01].

Verified: root make green; comprehensive suite exit 0 (47/47
generation, 41/41 negatives, all compiled phases); nonlean-regress
byte-identical (893/216/9); TestSupplyCheck pin plant red-green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ider dispositions, battery verbatim

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nt re-run red-green)

L2 fresh audit F1 (MAJOR): the converted m7 pin was vacuous — closed-
term extraction cached `unsafeBaseIO tickPairIO` at module init
(tickPairImpl___closed__0), so the reverted-emitter plant stayed
green. Fix per the auditor's demonstration: @[never_extract, noinline]
on tickIO AND tickPairIO (extraction reaches through outer attributes;
the closed term mentions only the inner names); header comment
corrected to state the real armour placement and why.

Plant re-executed by this worker post-fix: emitter reverted -> RED
(draws: first=1 second=4, exit 1, the (1,4) signature); restored ->
GREEN (first=1 second=2, exit 0). Both runs verbatim in the record
addendum, along with F2 (prefsc2 -> prefsc_paren rider name drift,
substance identical) and the accepted C2 inputs (implemented_by/unsafe
survivor-pair ratchet leg; log-location hygiene).

Verified: full comprehensive suite exit 0 (47/47 generation, 41/41
negatives, m7 OK); nonlean-regress byte-identical (893/216/9).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants