rokucommunity / rokucommunity/brighterscript
Subclass field re-declared with only a type annotation (no initializer) clobbers the value the base constructor already set
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 208
- Forks
- 68
- Avg merge
- 8h 39m
- Merged PRs (30d)
- 39
Description
Description
When a subclass re-declares an inherited field purely to narrow its type - same field name, no initializer, just a type annotation - the transpiled subclass constructor still emits an explicit m.field = invalid assignment. Since that runs after the base class constructor already set the real value, it silently clobbers it back to invalid.
This makes "narrow an inherited field's declared type in a subclass" - a completely ordinary, common OOP pattern - a runtime footgun rather than a no-op.
Reproduction
class Animal
sound as string
sub new(sound as string)
m.sound = sound
end sub
end class
class Dog extends Animal
' Type-only narrowing of the inherited field - no initializer given, just a type
' annotation.
sound as string
sub new()
super("Woof")
end sub
end class
sub main()
d = new Dog()
print d.sound ' prints "" (invalid coerced to empty string by print), not "Woof"
end sub
Transpiled output (bsc 1.0.0-alpha.52):
sub __Dog_method_new()
m.super0_new("Woof")
m.sound = invalid ' <- clobbers what the base constructor just set
end sub
Expected behavior
A field re-declaration with no initializer and no different default value should be purely a compile-time type annotation, with no runtime effect - the same as it already works for the reverse case (a subclass declaring a field the base class doesn't have at all gets a real initializer/assignment, which is correct and expected). Only a re-declaration that supplies its own initializer/default should emit an assignment.
Why this matters
It's easy to hit by accident and very hard to debug: the symptom shows up somewhere the field is later read (often much later, in an unrelated method), not at the point of the clobber, and nothing about the source code looks wrong - sound as string reads like an ordinary, harmless type-narrowing declaration.
We hit this in brighterscript-game-engine: every SceneObject subclass narrows an inherited drawable as Drawable field to its own drawable type (e.g. drawable as Image), which clobbers the value the base SceneObject constructor just stored. It currently goes unnoticed only because a repair step elsewhere in the codebase reassigns the field immediately after construction on every internal code path - constructing the class directly and reading the field before that repair runs exposes it. Filed as markwpearce/brighterscript-game-engine#69 before we traced it back to the compiler.
Comparison to other languages
- TypeScript (target < ES2022, or
useDefineForClassFields: false): a subclass field declaration with a type annotation and no initializer compiles to nothing at runtime - pure compile-time narrowing, no clobber. (TypeScript did effectively reintroduce this same footgun when adopting true ES2022 class-field semantics by default -useDefineForClassFields: truemakes an uninitialized field declaration emitObject.defineProperty(this, field, {value: undefined}), which does stomp a value the base constructor set. It's a well-documented gotcha from that transition, which is presumably an argument for treating this as something worth being deliberate about rather than defaulting into.) - Java/C#/Kotlin/Swift: redeclaring an inherited field in a subclass ("field hiding") creates a genuinely separate storage slot rather than re-initializing the parent's, so there's no shared slot to clobber in the first place.
Environment
brighterscript: 1.0.0-alpha.52- Reproduced via
bsc --project bsconfig.json --create-package=falsewith a minimalrootDir/outDir/filesconfig, no manifest needed to observe the transpiled output.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by running the minimal bsc reproduction described in the issue with its bsconfig.json and inspect the transpiled __Dog_method_new output. Trace the class-field lowering that handles inherited, type-only declarations. Done means the declaration no longer emits an assignment, while a subclass field with its own initializer still does, with regression coverage for both cases.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 70/100