Add Lean 4 port of voxgig/struct - #81
Conversation
Add a complete Lean 4 port of voxgig/struct (lean/), following the OCaml port's structure. Lean keeps undefined (noval) and JSON null distinct, so the port mirrors the canonical TypeScript logic directly. Design notes: - Nodes are mutable, reference-stable handles into a per-context heap (Lean's strict positivity rules forbid IO.Ref fields inside the Value inductive; same heap design as the Elixir port's ETS nodes). Function values live in a registry; the mutable Injection state in an arena. - The whole API runs in SIO := ReaderT Ctx IO. The state is deliberately NOT in module-initialize globals: the Lean runtime marks values stored in persistent global IO.Refs as shared, which forces a full heap-array copy on every write (measured: 16s vs 1ms for 50k writes). - JS-style number formatting (shortest round-trip) is hand-rolled with exact Nat arithmetic, since Float.toString prints "1.100000". - Zero third-party dependencies: regex is the in-tree Vregex engine (RE2 subset), and the test runner has an in-tree insertion-order JSON reader (Lean core's Json stores objects in a sorted tree). Integration: lean is wired into the top-level Makefile (test/lint/publish), tools/check_parity.py (full 48-function parity, reusing the Scala def pattern), CI build/lint workflows (leanprover/lean-action), README.md, AGENTS.md, design/REPORT.md and cspell.json. Tests: make test-lean passes the full shared corpus 1329/1329 (the same count as the other full-parity ports); make lint-lean (warnings-clean type-check) passes; python3 tools/check_parity.py reports every port ok including lean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELX8fGrxo5YaJZC3QWVJeH
The spell-check CI job flagged iinj/idef/inone, the InjArg constructor names referenced in lean/AGENTS.md and lean/DOCS.md. Add them to the cspell words list (alongside the existing injdef/noval terms). Verified with cspell over **/*.md: clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELX8fGrxo5YaJZC3QWVJeH
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7d83540284
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Address the canonical-fidelity issues raised in the PR #81 review, each verified against typescript/src/StructUtility.ts: - jsonify: pre-check cycles and return __JSONIFY_FAILED__ (canonical JSON.stringify throws on circular structures; a recursion-based encoder cannot recover from the stack overflow, so the check runs up front) and use the same sentinel in the catch path (was "null"). - jsonEncode: serialize non-finite numbers as null and omit map entries whose value is noval/function/sentinel, matching JSON.stringify's treatment of undefined/function/symbol properties (list slots still encode as null). - MODENAME: keyed by the mode flag values (canonical {4:'val',...}), not list positions. - setprop/delprop: coerce list keys with JS Number() then Math.floor, so "1.9" and "2e0" address elements 1 and 2 (canonical `+key` coercion); NaN keys remain no-ops. - getpath: expand $GET:/$REF: path parts only when an injection is present, like $META: (canonical guards all three with `injdef &&`), so literal "$REF:x"-style properties resolve in plain getpath calls. - join: filter to non-empty strings first and index over survivors while keeping the original size for the trailing-strip bound, so joinurl([null, "/a", "/b"]) preserves the leading slash. - $FORMAT number/integer: JS Number() coercion (true -> 1, "" -> 0) with NaN -> 0, and integer applies the 32-bit `n | 0` wrap. - Vregex: support \B (non-word-boundary) and RE2 named groups (?P<name>...), both documented in design/REGEX.md's subset. The re_find_all/re_replace stubs, whole-match-only re_find and non-validating re_compile are left as is: they exactly match the Haskell, OCaml, Scala and Elixir ports' in-tree-engine convention and are documented as not corpus-tested; upgrading them is cross-port work. Tests: full corpus still 1329/1329; warnings-clean build; parity green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELX8fGrxo5YaJZC3QWVJeH
|
Triaged all 14 Codex review findings against the canonical
The remaining four (implement Generated by Claude Code |
…port DECISION: the Go port's stdlib regexp (RE2) behaviour is the minimum regexp functionality every port must provide for the six re_* functions — re_find returns [whole, capture1, ...], re_find_all returns every non-overlapping match in that shape, re_replace expands $1..$9 capture references, re_test/re_escape as before. Documented in design/REGEX_API.md (new "Minimum functionality" section) and the repo AGENTS.md. Corpus enforcement: new build/test/regex.jsonic group (31 cases across test/find/find_all/replace/escape), compiled into test.json. Expected values were generated with Go and cross-checked against the canonical TS; cases stay inside the intersection of Go RE2, ECMAScript, PCRE, java.util.regex, .NET, ICU and the in-tree engines (no named groups, no empty-width matches, no unmatched groups, $1..$9-only replacements — $& is not portable since Go spells it $0). The group is wired into all 24 port runners. Ports raised to the floor: - ocaml, haskell, lean: the three sibling in-tree Vregex engines now track capturing groups (index assignment at parse time incl. RE2 (?P<name> groups; spans threaded functionally through the backtracking CPS), and gain find (with captures), find_all and replace (with $&/$0..$9/$$ expansion). The re_find/re_find_all/re_replace stubs are replaced by real implementations. - scala, elixir: re_replace was an identity stub — now implemented over java.util.regex / Regex with JS-style $-reference translation. - dart: string-template branch of re_replace was a stub — now expands $&/$1..$9/$$ via replaceAllMapped. - perl: string replacements now expand $&/$1..$9 (were interpolated literally). - zig: re_replace now expands $&/$0..$9/$$ from the capture slots (was documented as literal-only); the stale zig KNOWN_GAPS entry in tools/check_parity.py is removed. - python: re_* exported from the package __init__. - typescript, javascript: re_* exposed on the StructUtility class so the SDK utility carries the full canonical API. Verified locally (regex group green in each): typescript 95/0, javascript 95/0, python 100 OK, go PASS, ruby 93/0, perl 151 PASS, java 144/0, kotlin BUILD SUCCESSFUL, rust ok, c 1303/1303, cpp 1331/1331, elixir 1362/0, ocaml 1360/0, haskell 1360/0, lean 1360/0. Remaining ports (lua, zig, csharp, php, swift, clojure, scala, dart, aql) are wired identically and verified by CI. check_parity.py green with zero known gaps; cspell clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELX8fGrxo5YaJZC3QWVJeH
- c, cpp: clang-format the new corpus-test subjects (single-line ifs and doubled void casts split). - zig: the test wrappers built std.json.Array values, but the port's JsonValue.array holds *ListRef — construct ListRef nodes instead. Verified with zig 0.13: 72/72 tests pass, zig fmt clean. - kotlin: split the regex add(...) wirings per ktlint argument rules. - swift: line-break the regex.replace subject at the 100-column limit. - ruby: fix indentation of the @regex_spec assignment. - python: drop the unused re_compile import and ruff-format the test. All corresponding local test suites re-run green (c 1303/1303, cpp 1331/1331, zig 72/72, ruby 93/0, python 100 OK, kotlin lint+test, go/rust/ts/js lint clean). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELX8fGrxo5YaJZC3QWVJeH
Summary
This PR adds a complete Lean 4 port of
voxgig/struct, bringing the library to 17 total language implementations. The port passes the full shared corpus (1329/1329 tests) and maintains full parity with the canonical TypeScript implementation.Key Changes
lean/src/VoxgigStruct.lean(2838 lines): Core library implementationValuetype and heap-based storage for maps and listsget,set,merge,transform,validate,walk,select, etc.undefinedvsnulldistinctionlean/src/Vregex.lean(301 lines): Minimal regex engine$LIKEvalidation operatorlean/test/Runner.lean(677 lines): Self-contained test runnerbuild/test/test.jsonDocumentation and build files:
lean/README.md: User-facing overview and requirementslean/DOCS.md: Comprehensive Lean-specific guidelean/AGENTS.md: Implementation notes and design decisionslean/Makefile: Build and test targetslean/lakefile.toml: Lake package configurationlean/lean-toolchain: Pinned Lean 4 version (v4.32.1)CI/CD integration:
.github/workflows/build.ymlwithtest-leanjob.github/workflows/lint.ymlwithlint-leanjobMakefileandAGENTS.mdto include Lean in port matrixtools/check_parity.pyto track Lean as complete portdesign/REPORT.mdwith Lean metricsNotable Implementation Details
undefinedandnull: Like TypeScript, OCaml, and Haskell, the port maintains the semantic distinction viaValue.novalvsValue.nullhttps://claude.ai/code/session_01ELX8fGrxo5YaJZC3QWVJeH