Skip to content

fix(check): purity check now descends into js/export wrappers#4

Merged
tompassarelli merged 1 commit into
mainfrom
fix/purity-checks-exports
Jun 16, 2026
Merged

fix(check): purity check now descends into js/export wrappers#4
tompassarelli merged 1 commit into
mainfrom
fix/purity-checks-exports

Conversation

@tompassarelli

Copy link
Copy Markdown
Owner

check-purity! skipped jst-export/jst-export-default structs (only list-shaped wrappers were descended), so exported functions — the public API — escaped the !-effect check entirely. Now descends into export + with-meta wrappers.

Active suite 1377/1377. Surfaces genuine leaks downstream (gjoa: 27 exported make-* factory fns that mutate without a ! marker) — the check doing its job.

check-purity! only descended into list-shaped wrapper forms, but
js/export and js/export-default parse to jst-export(-default) STRUCTS,
not lists. So every EXPORTED defn — the public API, exactly what you most
want the purity guarantee on — silently escaped the !-effect check.

Descend into jst-export / jst-export-default / with-meta wrappers. Active
suite 1377/1377. Surfaces real leaks in downstream corpora (gjoa: 27
exported factory fns that mutate without a ! marker) — those are the
check working as intended.
@tompassarelli tompassarelli merged commit 2341387 into main Jun 16, 2026
@tompassarelli tompassarelli deleted the fix/purity-checks-exports branch June 16, 2026 07:55
tompassarelli added a commit that referenced this pull request Jun 17, 2026
The concrete crown jewel, tying moves 1-3: a scope-correct rename performed as a
CLAIM EDIT on the canonical Fram store, exported byte-stable, recompiled.

- bin/test/code-as-claims/rename-in-store.clj: ports chartroom's Turtle #4
  (self-contained in beagle's repair toolchain). Loads files' lossless claims into
  ONE Fram store; renames a symbol in the target module(s) by SUPERSEDING its `v`
  claims (claim-native, recoverable); the same-named symbol in other modules is
  untouched by construction (scope is structural, not lexical). Collision invariant:
  refuses a rename onto an existing binding (exit 3, no claims mutated).
- bin/test/code-as-claims/rename.sh (gate): rename mod_a's `helper` -> safe-add;
  proves mod_a renamed (def + caller), mod_b's identical `helper` UNTOUCHED, renamed
  tree RECOMPILES clean, collision refused. Verified the killer property directly:
  `helper` inside a STRING is left intact (a text sed corrupts it; the graph edit,
  operating on symbol leaves, does not). Wired into CI.

Honest scope: module-local rename (def + callers in the module); cross-module
collision is correct; intra-module local shadowing + cross-file rename-CASCADE are
documented follow-ups. Uses the move-3 fixes (number? node-ref test in the loader).
tompassarelli added a commit that referenced this pull request Jun 18, 2026
…ariants)

rename.sh §10: an unquote nested inside a quote inside a template still escapes+renames;
a nullary (bare-symbol) defunion variant is renameable.
delete.sh §6-7: deleting a defunion with a live variant-ctor ref is refused (orphan scan
keyed on the deleted subtree); an unused parameterized union head is deletable.
tompassarelli added a commit that referenced this pull request Jun 21, 2026
…HAMT

Closes the last soundness hole-class (#4 runtime dedup) from beagle-2's boundary
map. The runtime collection builders no longer emit native ARRAYS for sets (a
pre-existing set-builder bug) and route to value-dedup HAMT when the element type
is compound:

  coll-kind helper: 'set | 'vec | 'map | 'unknown, seeing through conj/into/disj
  to the underlying collection (so a native Set/conj-result isn't mistaken for
  an array).
  conj  : value-set -> hamtSetAdd fold ; native set -> new Set([...c, x]) (a Set,
          not [...c,x]) ; vec -> [...c,x].
  into  : value-set -> xs.reduce(hamtSetAdd, target) ; native set -> new Set([...
          target, ...xs]) ; vec -> spread.
  count : native branch now dispatches on coll-kind -> set:.size / map:Object.keys
          ().length / else:.length, so (count (conj #{..} x)) is .size not .length
          (was undefined).
  frequencies : compound elements -> value-keyed hamtMap (was native object ->
          "[object Object]" collisions). group-by left native (its key type comes
          from f's return, not reliably inferable — documented residual).

Verified vs Clojure shape: conj/into on scalar+compound sets (3/1/4/2),
frequencies compound/scalar (2/2). rep-soundness +4 = 24/24; active 1484/1484,
gated JS (emit-js 147, exec-oracle 7, ...) + behavioral 78 green.

ALL 6 hole-classes now closed: #1 records, #2 unions, #3 escape-across-fn,
#4 runtime dedup, #5 heterogeneous, #6 cross-rep equality.
tompassarelli added a commit that referenced this pull request Jun 21, 2026
beagle-2's corpus caught the residual: my first #4 routed conj/into on the
TARGET set's rep, so an EMPTY `#{}`, a native scalar set, or a compound set
LITERAL target (all classify native) left `(into #{} compound)` / `(conj #{}
compound)` as a native `new Set` -> ref-dedup -> NO value-dedup. `(set X)` worked
but `(into #{} X)` — the idiomatic dedup-to-set — did not.

Fix: route conj/into on the ELEMENT type (conj-hset? / into-hset?), coercing the
target to a hamtSet (emit-set-as-hamt) when an element is not provably-scalar:
  - conj-hset?: target is a set AND (target already hset OR some conjed elem not
    provably-scalar) -> hamtSetAdd fold over hamtSet(target).
  - into-hset?: target is a set AND (target hset OR xs's element type not provably-
    scalar) -> xs.reduce(hamtSetAdd, hamtSet(target)).
  - into a VECTOR stays a native array (not over-promoted); scalar set -> native Set.
classify-rep + count now see conj/into results as hset.

Verified vs Clojure: (into #{} [[1 2][1 2][3 4]])=2, (conj (conj #{}{:a 1}){:a 1})
=1, (into #{[1 2]} [[1 2][3 4]])=2, into-vector stays 3, scalar-set into=4.
rep-soundness +4 element-driven gates (empty/literal-target + vec-not-promoted) =
28/28; active 1488/1488, gated JS + behavioral green. #4 now fully closed.
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.

1 participant