Class members leave the lexical chain (§13.17, §13.18, §13.22) - #179
Merged
Merged
Conversation
§15's #2 and #4, taken together because they wanted the same check in the same place: a class or trait member colliding with a name that already means something, reported where the collision is written rather than left to surface somewhere else, or not at all. §13.22 shipped a crash. A method body resolves through the class environment before it reaches the file's imports, so a method named `font` hides `import "lumen:font"` from *every* method in the class, and the failure lands as a property error at whatever call site first wanted the module, with nothing at the import or the method to connect it back. import "ghost:math" as math class Theme { load() { return math.floor(3.7) } // was: property error here, math(role) { return role } // function has no method `floor` } // now: syntax error at `math` §13.18 was silent. A field and a method could share a name, a property read answering with the field and a call with the method, because the two lookup paths never meet. class Thing { thing = 7 thing() { return "method" } // now: syntax error, already a field } checkMethodDeclaration (evaluator/function.go) makes both checks on the declaration path. Either declaration can come first, so the field/method collision is also checked from the other side in evaluateAssign - methods being the only members that live in the body's own environment. Neither lookup path changed: both are correct on their own, and it is the pair of declarations that is the mistake. HasField and DeclaredName join object.FieldDeclarer so the checks read the same on a class and a trait. Deciding what counts as a module needed an honest answer rather than type sniffing, since a `scheme:name` import binds a LibraryModule but a Ghost source module binds a plain Map, which is also what a map literal is. object.Map gains a Module flag set only by moduleValue, so shadowing an import is rejected while shadowing a map, a function or a variable stays ordinary lexical shadowing. Environment.GetEnclosing is what asks the question, skipping the class's own members to see what the name meant outside. Global modules are deliberately not covered, and a test pins why: a method named `console` does not hide the global `console` from its siblings, because globals resolve through the library registry rather than the environment chain. There is nothing there to warn about. Tested in evaluator/class_members_test.go, fifteen cases across both declaration orders, class and trait bodies, a Ghost source module written to a temp directory, and - the half that matters for a check like this - the shapes that must not be rejected: overriding an inherited field or method, a field and method merely sitting beside each other, a method named like an outer function, variable or plain map, and the aliased-import form the fix recommends. §8.8 documents both rules. All 41 programs in examples/ are unchanged, and Studio's 132-case suite passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EGsNMRiKwWmvoizxD619zA
Replaces this branch's §13.22 diagnostic, which treated a symptom. The
review question that prompted it: a method named `math` is reached as
`this.math()`, so why should it collide with a top-level `math` import at
all? It should not. Rejecting the declaration made an author work around a
collision that has no reason to exist.
So resolution order changed instead, in the one way §13.17 was already
forcing. A method now closes over the scope its class was *declared* in
rather than the class body's scope, and initializeField does the same for
field initializers - the other path into a class body, which reproduced the
bug exactly and would have survived the diagnostic. Members stay in
class.Environment, which LookupMember reads for `this.x()` and `super.x()`;
that table is simply no longer a lexical scope.
import "ghost:math" as math
class Theme {
load() { return math.floor(3.7) } // 3 - the module
math(role) { return role } // reached as this.math()
}
This closes §13.17 in the same change, since it is the same question: a
bare sibling call is now a plain name error at the call site, and undefined
recognises a name that is a member of the running class and answers
"`describe` is a member of `W`; reach it through `this.describe`". The old
report pointed at the callee's `this`, in correct code, in another method.
Gone with the diagnostic: shadowedModuleError, isImportedModule,
Environment.GetEnclosing and the object.Map.Module flag - along with the
question they existed to answer, which was whether a binding was "really"
an imported module. That had no honest answer for a Ghost source module
bound as a plain Map, and a rule that has to classify what it shadows is
doing the wrong job. §13.18's field/method check is untouched.
A bonus worth recording: §14 decision 9 listed "a method's local named like
a sibling method rebinds that method" as a live cost of walking assignment.
Members are out of the chain now, so that collision is gone; §13.13 and
decision 9 are updated, and a test pins it.
This is a breaking change. A bare sibling call to a method that never
touches `this` used to work - the narrowest version of the feature, which
is why the defect survived to 1.0 - and now raises. One test pinned it. All
41 programs in examples/ and all 132 cases of Studio's suite are unchanged,
neither having used the bare form. §8.8 now states the JavaScript rule the
rest of Ghost's class syntax already follows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGsNMRiKwWmvoizxD619zA
… line The note claimed confining the field/method check to one body was deliberate "where a name appearing twice is overriding rather than colliding". That reasoning holds for field-over-field and method-over-method, not for field-over-method, which is the collision §13.18 is about. A class field against an inherited or trait-supplied method still answers the field for a read and the method for a call, exactly as the callout describes. Records it as a residual with the shape of the fix, rather than as a justified boundary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EGsNMRiKwWmvoizxD619zA
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.
Overview
§15's #2, #3 and #4. It started as #2 and #4 — one diagnostic on the member-declaration path — and a review question reframed it:
It shouldn't. Rejecting the declaration made an author work around a collision that has no reason to exist. So the second commit replaces that diagnostic with a scoping change, in the one direction §13.17 was already forcing — which is why #2 and #3 turned out to be the same question, not two.
A method is a member, not a name in scope. A method body — and a field initializer — now resolves through the scope its class was declared in, rather than the class body's scope. Members still live in
class.Environment, whichLookupMemberreads forthis.x()andsuper.x(); that table is simply no longer a lexical scope.Both work. §13.22 dissolves rather than being guarded.
What each callout gets
§13.22 — no diagnostic. Gone with it:
shadowedModuleError,isImportedModule,Environment.GetEnclosingand theobject.Map.Moduleflag — along with the question they existed to answer, is this binding "really" an imported module? That had no honest answer for a Ghost source module bound as a plainMap, and a rule that has to classify what it shadows is doing the wrong job.§13.17 — closed by the same change. A bare sibling call is now a plain
Namefault at the call site, andundefinedrecognises a name that is a member of the running class:§13.18 — untouched, still a
Syntaxfault at the second declaration. A field and a method sharing a name is a genuine collision; it is unrelated to lexical scoping.Field initializers needed the same treatment — the other path into a class body, evaluated per instance, which reproduced §13.22 exactly (
size = math.floor(2.7)beside a method namedmath) and would have survived the diagnostic entirely.A bonus
§14 decision 9 recorded, as a live cost of walking assignment, that "a local named
scalewould rebind a sibling method namedscale". Members are out of the chain now, so that collision is gone:§13.13 and decision 9 are updated to say so, with a test pinning it.
Breaking change
A bare sibling call to a method that never touches
thisused to work — the narrowest possible version of the feature, and precisely why the defect survived to 1.0 — and now raises. One test pinned it (TestClassMemberResolution, updated tothis.).All 41 programs in
examples/and all 132 cases of Studio's suite are unchanged; neither codebase used the bare form, and Studio's ownCLAUDE.mdalready mandatesthis.method().mud.gsdiffers only in frame count inside its timeout, as in the previous PRs.§8.8 now states the JavaScript rule the rest of Ghost's class syntax already follows —
new,extends,constructor, keyword-less methods — where bare sibling calls don't exist either. The bare form was a Ruby-ism inside a JS-shaped class.Verification
evaluator/class_members_test.go: the field/method collision both ways and in a trait; a method sharing a name with a scheme import, a Ghost source module, a trait, and a field initializer, with the method still reachable; bare sibling calls for a callee that touchesthisand one that doesn't; the help text; the local-vs-sibling-method case; and the eight shapes that must not be rejected.go build ./...,go vet ./...,gofmt -l .,go test ./...all clean.§14 decision 12 records the call and its cost. Next on §15 is #5 (§13.16), the one open design decision (§14 decision 10).
🤖 Generated with Claude Code
https://claude.ai/code/session_01EGsNMRiKwWmvoizxD619zA