Fix 4 emit-bug classes: async-await, let-swallow, %-modulo, camelCase-export mangle#2
Merged
Merged
Conversation
A control-flow form (try/do/let/loop/when/match/doseq-body) containing js/await
compiles to an async IIFE `(async () => {...})()` which RETURNS A PROMISE. In
value position (let/loop binding) and statement position (doseq body, non-final
body stmt) that promise was used WITHOUT await -> the binding held a pending
promise and code after it ran before it settled. This 'fire-and-forget' bug bit
6 unrelated files in one gjoa session (marionette client, two wait_for loops,
sidebar loop, branding.bjs copy loop -> 'copied 0 files').
Fix: await-async-iife helper prepends `await` when an emitted value/statement is
exactly the iife/loop output prefix `(async () => `. Applied at all six binding-
value sites + emit-expr-stmt. The exact-prefix match means it never over-awaits
inline-await calls (`f(await g())`) and never double-awaits explicit js/await
(`await (async...`). Active suite 1377/1377; gated js tests identical to baseline;
gjoa corpus (46 modules) recompiles clean.
Thread: @2026-06-15-230101
…non-final stmt A let binding whose value form is paren-short silently absorbs the next binding; the swallowed name emits as a bare `name;` JS statement -> runtime ReferenceError. beagle syntax reports ok (net parens balance), so it was a silent miscompile (hit gjoa drag.bjs setup_drag, vim.bjs setup_vim_keys). A bare symbol in non-final statement position has no effect; when it also resolves to nothing (not local/param/let/extern/builtin/top-level def), it is the swallow signature. last-expr-type now raises E020 with a pointed message + source loc. Tail-position symbols (the return value) and bound symbols are not flagged. gjoa corpus (46 modules) + beagle active suite (1377/1377): no false-positives. Thread: @2026-06-15-230102
…rror -> rem/mod `(% a b)` written intending modulo silently emitted `_pct(a, b)` (undefined) — `%` is the anonymous-fn argument shorthand, only valid inside #(...). The reader rewrites `%`->`%1` inside #(), so a bare `%` at parse time can only be outside a lambda = always an error. Hit gjoa vim.bjs cycle-space (broke gs/gS space chord). Reject at parse with a pointed message naming rem/mod. Legit `%` inside #() (e.g. #(= (.-id %) 5) -> (_pct1)=>...) is unaffected; gjoa corpus + beagle suite clean. Thread: @2026-06-15-230103
kebab names mangle to snake_case; camelCase emits as-is. An exported camelCase name that a consumer references in kebab mangles to a different JS identifier and is undefined in the concatenated bundle. Warn (not hard-error, since declare-extern is the intentional escape hatch) and name the kebab fix. Fires at build and plain check; counted by count-lint-warnings so the agent/repair-loop surfaces it too.
tompassarelli
added a commit
that referenced
this pull request
Jun 16, 2026
emit-claims now emits a uniform 'child' containment edge for every minted node (never for inlined literals), so structural containment is one traversable predicate and node-refs are unambiguous vs literal integers — this is what makes the call graph cleanly derivable in Datalog (chartroom turtle #2). Adds bin/beagle-claims + claims-cli.rkt: a cross-cutting analysis command that claims-emits any file (.bjs/.bclj/.bnix) regardless of its #lang, bypassing the per-file target dispatch (claims are an analysis projection, not a compile target).
tompassarelli
added a commit
that referenced
this pull request
Jun 17, 2026
…ingle-colon) Gates the 9 type-cluster fixes from adversarial sweep #2 (chartroom resolve.clj): constructor heads rename with the type, defunion variants, cross-module t/Type annotations cascade, single-colon ':' annotations cascade, and a rename matching no binding is refused (no silent 0-edit success).
tompassarelli
added a commit
that referenced
this pull request
Jun 17, 2026
Gates the 5 sequential/destructuring fixes from sweep #2: :or default refs cascade, for/let sequential-sibling capture is refused, and a legitimate sequential rename still succeeds (no over-refusal).
tompassarelli
added a commit
that referenced
this pull request
Jun 17, 2026
Gates cluster C+D from sweep #2: quasiquote template refs to module defs rename (template locals stay, hygiene), bare (require m :refer [x]) refs rename cross-module, and renaming into a name a consumer already binds is refused (no duplicate import).
tompassarelli
added a commit
that referenced
this pull request
Jun 17, 2026
…oles Quantifies the lesson: a graph engine is scope-correct only where adversarially proven, not because it is a graph. Two sweeps surfaced 2 + 17 concealed recompiled-but-wrong bugs in an engine whose pitch is "correct unlike sed."
tompassarelli
added a commit
that referenced
this pull request
Jun 20, 2026
* docs: codegen/check findings from gjoa dogfooding (8 items, minimal repros) A companion ledger to text-as-source-latent-bugs.md, capturing codegen/emit/lint findings surfaced by authoring gjoa with beagle. #2 (unary minus) is already fixed; #1 (statement-position lowering) is flagged as an invasive, deliberate change — NOT a dogfooding tangent. The rest are filed with minimal repros while fresh. * docs(findings): add #9 — keyword map-key underscore doubled vs property read A keyword map-literal key with '_' lowers to a doubled underscore ({:a_b 1} -> {a__b: 1}) while the matching property read (.-a_b) stays single (m.a_b), so writes and reads disagree and the round-trip silently returns undefined. Surfaced building the gjoa audit ledger; worked around with camelCase keys. * docs(findings): add #10 — emitted import specifier vs snake_cased filename mismatch A hyphenated module (drag-overlay.bjs -> drag_overlay.js) is imported by a sibling with the original hyphen (from './drag-overlay.js'), which doesn't match the emitted filename — broken under plain ESM. Same root family as #9 (divergent name normalization across positions). Surfaced resolving the gjoa audit-ledger dependency graph.
tompassarelli
added a commit
that referenced
this pull request
Jun 21, 2026
…scape-across-fn) Closes soundness hole-class #3 (escape across the fn boundary) from beagle-2's adversarial boundary map: a fn RETURNING a compound-keyed map (or a `(set var)` over a compound-typed param) was read NATIVE by the caller because rep-selection was intraprocedural + per-node-table only (bare var-refs and call results carry no rep). Two additions to classify-rep: - TYPE-DIRECTED dispatch: an expression whose node-type is a concrete compound-keyed Map / compound-elem Set classifies hmap/hset directly. This catches annotated fn returns (`build-index :- (Map (Vec Int) Keyword)`) and any typed expr — the caller's get/contains/count now route to HAMT ops. - BINDING-TYPE ENV (current-type-env): a var-ref resolves to its declared type (param `:-` annotation / let value type) via arg-type, so `(set paths)` with paths :- (Vec (Vec Int)) sees the compound element and routes to hamtSet. Seeded at defn/fn/arity params (with-param-envs) + composes with rep-env. - rep-env now also seeded from param types (a param :- (Map compound ..) reads as hmap inside the body). Real-program evidence: bjs-bench/programs/structdiff.bjs (pure-value diff/merge = Eddy skip-if-unchanged at lib scale) goes 5/7 -> 7/7 — distinct-paths (value dedup over a compound param) and cross-fn compound-map lookup both flip GREEN. No regression: active 1474/1474, gated JS (emit-js 147, fixtures, jst, exec-oracle) + behavioral 78 all green. Still provably-compound predicate; the Any/records/unions flip (#1,#2,#5,#6) is the next commit.
tompassarelli
added a commit
that referenced
this pull request
Jun 21, 2026
…phic reads Closes soundness hole-classes #1 (records-as-keys), #2 (union keys), #5 (heterogeneous-key literals), #6 (Any-typed compound keys) from beagle-2's adversarial boundary map. The classifier predicate flips from "provably-COMPOUND → HAMT" to "provably-SCALAR → native, else → HAMT": key-class(t): scalar-eq-safe -> 'native ; Map/Set/Vec/List ctor OR a record type -> 'hamt (object-emitting, ; no scalar subtype: a value of this type is ALWAYS a HAMT) ; Any | type-var | union | Float | Nil | unknown -> 'poly PRODUCTION: a map/set whose key/elem is not provably-scalar produces a HAMT (incl records, unions, Any, heterogeneous K=Any). Map LITERALS classify by their actual KEY DATA per-pair (robust to an uncaptured node-type — e.g. a map built inside a `.map` arrow); scalar literal keys (incl quoted `':k`) resolve to their scalar type via arg-type, so they stay native (no over-promotion). READS — 3-way, because Any is a BIDIRECTIONAL wildcard (a native scalar map <: (Map Any V)), so a native value can flow into an Any/union-typed read: - provably-compound/record coll -> hamtMapGet/Has/Count (monomorphic, O(log n)) - scalar coll -> native - poly (Any/union) coll -> $$bc.get/contains/count/keys/vals (polymorphic over native + HAMT; beagle-2's @26df650). count/keys/vals/get gain a 'poly branch. Adversarial-verified (vs the Clojure shape): record keys (distinct lookup + absent-miss), Any-keyed assoc-chain (put2 -> "x" not "y"), heterogeneous-key count (3 not 2) all flip RED->GREEN. rep-soundness gate +4 flip cases (records / Any-key / heterogeneous / Any-typed poly read) = 18/18. Active 1478/1478, gated JS (emit-js 147, exec-oracle 7, ...) + behavioral 78 all green. DEFERRED (rarer, documented): poly-PRODUCTION ops (assoc/conj/dissoc on an Any-typed COLL) need a $$bc COW producer; the Any->concrete-typed-param coercion boundary (gradual-typing blame point).
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 22, 2026
… build) P0 follow-up (fram-2 #18). parse.rkt has its OWN beagle-readtable — it drives read-beagle-syntax (check --agent, build, the repair loop + PostToolUse hook) — SEPARATE from reader-impl.rkt's readtable (#lang module loading). bc53027 added the `^` metadata reader macro only to reader-impl's, so read-beagle-syntax read `^:dynamic` as a bare symbol → "malformed def" on the first dynamic var, and the agent path STOPPED there (real type-errors after it skipped) — the repair loop + hook went blind on every dynamic-var file (resolve.clj + the daemon are dynamic-var-heavy). One missing macro caused both reported issues: #1 check --agent / build / repair-loop blind on dynamic-var files #2 "typed dynamic def rejected" — never a grammar gap; the read path just never produced #%meta, so (def ^:dynamic NAME :- TYPE VAL) misparsed too. Fix: mirror the `^` macro into parse.rkt's readtable (meta-reader-local), with a comment flagging the two-readtable duplication so future reader features stay in sync. Regression test drives read-beagle-syntax directly with a typed ^:dynamic def. Active 1500/1500 + gated 437/437, all targets.
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.
Four classes of incorrect JS emit. Each passes
beagle syntaxbut produces output that only fails at runtime. This PR fixes or guards each.1. async-IIFE-await — emit correctness (
678bbd1)A
try/loop/doseqcontaining(js/await …)compiles to an async IIFE(async () => {…})(). When bound in aletwithout an enclosingjs/await, it was emitted withoutawait, so the binding held a pending Promise that downstream code read synchronously.Fix:
emit-jsawaits async IIFEs in value/statement position (await-async-iife, applied at binding-value and expr-stmt sites). Tail position is left un-awaited; the helper matches only the exact(async () =>prefix, so it never double-awaits an explicitjs/awaitor an inlinef(await g()).2. let-swallow → bare
name;— guard (004d291)A
letbinding whose value form is one paren short absorbs the followingname valuepair as body forms; the swallowed binding name emits as a barename;statement, causing aReferenceErrorat load. Net parens balance, sobeagle syntaxreports ok.Fix:
check.rktraisesswallowed-binding(E020) when a non-final body statement is a bare symbol that resolves to nothing, with a message pointing at the enclosinglet. Tail-position bare symbols (legitimate returns) are not flagged.3.
%as call head → undefined_pct— guard (2f7a441)(% a b)emits_pct(a, b)because%is the#()anonymous-arg shorthand. Outside a lambda this is an undefined call.Fix:
parse.rktrejects%as a call head with an error namingrem/mod.%inside#(…)is unaffected.4. camelCase export name mangle — lint (
7b5b6d5)kebab names mangle to
snake_case; camelCase emits as-is. An exported camelCase name referenced cross-module in kebab mangles to a different identifier and is undefined in the bundle.Fix:
lint.rktlint-export-namingwarns on camelCase JS exports and names the kebab form. It runs inlint-program!, so it fires atbuildand plaincheck, and is counted bycount-lint-warningsso the--agentpath reports it.Verification