Repository navigation
Array#join keeps the separator boundary around an empty nested array (fixes #102) - #150
Open
jiro-by-nori[bot] wants to merge 1 commit into
Open
jiro-by-nori[bot] wants to merge 1 commit into
jiro-by-nori[bot] wants to merge 1 commit into
Conversation
) `ary_join_1` appends the separator **before** every element after the first, at every level. The model flattened nested arrays into one list of rendered parts and then put the separator between parts, so an empty nested array — which renders nothing and therefore pushed no part — lost the boundary its position earned: `[1, [], 2].join("-")` answered `"1-2"` where CRuby answers `"1--2"`. The prelude's recursive walk now renders each element as a unit and recurses with the same separator, so every level keeps its own boundaries, and it keeps the depth gate the flattening walk had (a self-referential array is refused rather than run out of steps). The Lean `joinImpl` answers a receiver with no nested Array; one with a nested Array defers to that walk, the way an element answering `to_ary` already did. Pinned by `difftest/corpus/regressions/array-join-empty-nested.rb`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #102.
Written by Jiro, an autonomous coding agent (Nori), on behalf of @sam-xif. The commit and this description are the agent's work; nothing here was written by a human.
What was wrong
ary_join_1emits the separator before every element after the first, at every level. The model flattened nested arrays into one list of rendered parts and then put the separator between parts, so an empty nested array — which renders nothing and therefore pushed no part — lost the boundary its position earned.[1, [], 2].join("-")"1-2""1--2"[[], 1].join("-")"1""-1"[[], []].join("-")"""-"[1, [[]], 2].join("-")"1-2""1--2"[[1], []].join("-")"1""1-"[1, [2, [3, nil]], []].join("-")"1-2-3""1-2-3-"A wrong answer, not a gate — the failure mode the project treats as a bug.
The change
ruby-lean/prelude/prelude.rb— the recursive walk renders each element as a unit and recurses with the same separator (__join_collectpushing leaves into a flat list becomes__join_partreturning one element's contribution), so every level keeps its own boundaries. It keeps the depth limit the flattening walk had, so a self-referential array is still refused rather than running the machine out of steps.ruby-lean/RubyCore/Builtins/Support.lean—joinImplis exactly CRuby's rule over a flat array. A receiver containing a nested Array now defers to the prelude twin intoAryDefer?, the mechanism that already routes an element answeringto_arythere. Deferral is one level deep, which is enough: the prelude walk owns everything below.ruby-lean/RubyCore/Generated/{PreludeJson,Prelude}.lean— regenerated (make gen), not edited.difftest/corpus/regressions/array-join-empty-nested.rb— pins the boundary at the start, middle and end, with an empty separator (where the boundary is invisible),nilelements, deeper nesting, and both the pure and the dispatching (to_s/to_ary) paths.Test plan
Toolchain: elan with
leanprover/lean4:v4.32.2, CRuby 4.0.5, Sorbet 0.6.13405,uv.bin/ruby-lean --compare: model and CRuby agree on every case in the table above, and on 150 randomly generated nested-joinprograms (depth ≤ 4, mixedInteger/nil/String, separators",","-",""," | ").difftest run --tier regressions: 224 cases — 223 agree/held, 1 gated (sorbet-hash-gate, declaredopen), 0 regressed, 0 disagree. The new case reportsheld.conformancetier 0 (difftest run --tier 0+scripts/check-conformance.sh): 1287 programs ran, 1087 agree, 194 unsupported, 0 disagree, 0 weakened — holdsdifftest/coverage-baseline.jsonexactly, so no recorded number moved.difftest replay corpus/tier3: 70 cases, 57 agree, 13 unsupported, 0 disagree.make books(cd books && lake build): 1405 jobs, success — includingBooks.Metatheory.Framing.RootFramePrimitives, which is wherejoinImpl_framelives.make metatheory: OK — theorems rest onpropext,Classical.choice,Quot.soundonly; the booted heap satisfies the proofs' assumptions.make soundness:SOUNDNESS CHECK PASSED.make comparator: accepted — statements matchChallenge.lean, axioms are Lean's three, the kernel replays the closure.make-equivalentgen-check:PreludeJson,CRubyNames,Unicode,Preludeall fresh;books/scripts/generate_audited_checker.py --checkclean.make difftest-test: 189 passed withDIFFTEST_NO_SKIPS=1; coverage holds its baseline (72.77 % line / 68.18 % branch).make book-checks:FastPower72/72 inputs agree;LoopSumagrees.feature-loading: agrees.ci-sync: CI runs everymake checktarget.check-not-weakened --base origin/main: CHECKS NOT WEAKENED.make lint(no file it checks changed —.rubocop.ymlexcludesruby-lean/prelude/**anddifftest/corpus/**, and no Python, shell or workflow file is touched) andmake docs(no documentation changed).Adjacent gap, not touched here
[1, 2].join(nil)gates (unsupported: join) where CRuby answers"12"— the builtin's separator conversion refuses anilargument before the prelude twin sees it. Pre-existing, orthogonal to the boundary rule, and left alone to keep this change scoped.