Skip to content

Close the two remaining shadowing holes (§13.25, §13.18) - #180

Merged
kaidesu merged 2 commits into
1.0from
claude/ghost-items-prioritize-f4ghko
Sep 6, 2026
Merged

kaidesu merged 2 commits into
1.0from
claude/ghost-items-prioritize-f4ghko

Conversation

@kaidesu

@kaidesu kaidesu commented Sep 6, 2026

Copy link
Copy Markdown
Member

Overview

The two items left over from the "does this resolve all shadow/scoping issues?" sweep after #179. One is a new callout that report never found; the other closes a residual #179 recorded rather than fixed.

§13.25 — a library global could not be shadowed by anything

evaluateIdentifier consulted the library registries before the environment, so §9.1's two global names beat every binding in the language — a parameter, a loop variable, a name bound in a block, an import ... as. The binding was made and then never readable.

function render(type) { return type }
render("warning")               // before: library function {type}
                                // after:  "warning"

class Cmd { constructor(type) { this.type = type } }
new Cmd("edit").type            // before: library function {type} — written into the instance
                                // after:  "edit"

Nothing raised, which makes it worse than the shadowing §13.22 described: there the failure was at least a fault at some later call. Here a string parameter silently is a function object and gets stored as one — the silent wrong answer §3 sets out to make impossible. And type is an ordinary word for an ordinary parameter: a command type, an event type, a token type.

§9.1 grants these two global standing "the same standing print/type-checking primitives have in other scripting languages" — where a local shadows them without ceremony (Python's type is a builtin any local shadows). The behaviour contradicted the rationale offered for it.

Fix: the two lookups are swapped — environment first, registries as the fallback when nothing is bound. Shadowing becomes the ordinary rule; a global is what a name means where nothing else claims it. The optimizer's LibraryBinding marking is unchanged and still correct: it now saves an undefined name two map lookups, rather than saving every ordinary variable two, which the new order already does.

Cost: none measurable. Allocation is identical on evaluator/benchmark_test.go and wall time is within run-to-run noise — a global now walks the scope chain before reaching the registry, which is the walk every other name already pays.

§13.18 — the cross-body collision

#179's note recorded this as a residual with the shape of the fix; this closes it. The in-body check couldn't see a field here colliding with a method that arrived from a superclass or a used trait:

class P { label() { return "method" } }
class C extends P { label = 7 }
// before: c.label → 7, c.label() → "method"   (the §13.18 duality, silently)
// after:  syntax error: `label` is declared as a field here and as a method on `P`

checkInheritedCollisions runs once the class body is complete — the earliest either declaration path could see the other, since use may sit anywhere in the body. Both directions are caught (a field here against an inherited method, and a method here against an inherited field), through a superclass, a grandparent, or a trait.

Only field-against-method counts. A name arriving twice as two fields, or twice as two methods, is overriding — the point of extends and use — and five cases pin that it stays legal.

The fault is reported at the declaration in this body and names where the other one came from, that being the half a reader can't see from here. object.Field gained the token it was declared at; declaredMethods reads method names and tokens back off the class body.

Verification

  • evaluator/globals_test.go: seven binding forms shadowing a global, an aliased import doing the same, the globals still resolving unshadowed (including inside a function and a method), and a shadow not escaping its scope.
  • evaluator/class_members_test.go: five cross-body collisions and the five overriding shapes that must stay legal, alongside the existing in-body cases.
  • All 41 programs in examples/ unchanged (mud.gs excepted as before — a non-terminating interactive loop, byte-identical for 2.9 MB, differing only in frame count inside the timeout).
  • Studio's 132-case suite passes.
  • go build ./..., go vet ./..., gofmt -l ., go test ./... clean.

Note on §15

§13.25 came from sweeping the scoping surface after §14 decision 12, not from the Chisel/Studio report — and it outranked most of what was still open. §15 now records that, because ranking a report is worth doing and is not the same as knowing what is wrong.

With this, §15's next item is #5 (§13.16) — the export surface, and the only ranked item left that isn't small. It needs §14 decision 10 settled before code.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EGsNMRiKwWmvoizxD619zA


Generated by Claude Code

evaluateIdentifier consulted the library registries before the
environment, so §9.1's two global names beat every binding in the
language - a parameter, a loop variable, a name bound in a block, an
`import ... as`. The binding was made and then never readable.

    function render(type) { return type }
    render("warning")   // was: library function {type}

Nothing raised, which makes this worse than the shadowing §13.22
described: there the failure was at least a fault at some later call.
Here a string parameter silently is a function object and gets stored as
one - `new Cmd("edit").type` wrote the library function into an instance
- which is the silent wrong answer §3 sets out to make impossible. And
`type` is an ordinary word for an ordinary parameter: a command type, an
event type, a token type.

§9.1 grants these two global standing "the same standing print/type-
checking primitives have in other scripting languages", where a local
shadows them without ceremony. The behavior contradicted the rationale
offered for it.

The two lookups are swapped: environment first, registries as the
fallback when nothing is bound. Shadowing becomes the ordinary rule and a
global is what a name means where nothing else claims it. The optimizer's
LibraryBinding marking is unchanged and still correct, now saving an
undefined name two map lookups rather than saving every ordinary variable
two - which the new order already does. Allocation is identical on the
benchmarks and wall time is within noise.

Found by sweeping the scoping surface after §14 decision 12 rather than
by the Chisel/Studio report - the mirror image of §13.22, where a member
hid an outer name and here an outer name hid everything.

The SPEC entry for this lands with the next commit, which touches the
same sections.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGsNMRiKwWmvoizxD619zA
Closes the residual the previous pass recorded rather than fixed: the
in-body check could not see a field here colliding with a method that
arrived from a superclass or a used trait, so `class C extends P { label
= 7 }` against `P.label()` kept the silent duality §13.18 is about -
`c.label` answering 7 and `c.label()` answering the method.

checkInheritedCollisions runs once the class body is complete, which is
the earliest either declaration path could see the other: `use` may sit
anywhere in the body, so when a member is declared the other body may not
have been consulted yet. Both directions are caught - a field here
against an inherited method, and a method here against an inherited
field - through a superclass, a grandparent, or a trait.

Only field-against-method counts. A name arriving twice as two fields, or
twice as two methods, is overriding, which is the point of `extends` and
`use`; five cases pin that it stays legal.

The fault is reported at the declaration in this body and names where the
other one came from, that being the half a reader cannot see from here.
object.Field gained the token it was declared at so a field can be
reported precisely after the fact, and declaredMethods reads method names
and tokens back off the class body for the same reason.

Also carries the SPEC for both this and §13.25 from the previous commit,
since they touch the same sections: §13.25 is new, §13.18 records the
residual as closed, §9.1 states that the two globals are shadowable, and
§15 lists §13.25 among the closed items with a note that it came from a
sweep rather than from the report.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGsNMRiKwWmvoizxD619zA
@kaidesu
kaidesu marked this pull request as ready for review September 6, 2026 06:26
@kaidesu
kaidesu merged commit 32e1296 into 1.0 Sep 6, 2026
2 checks passed
@kaidesu
kaidesu deleted the claude/ghost-items-prioritize-f4ghko branch September 6, 2026 06:26
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