rokucommunity / rokucommunity/brighterscript

Subclass field re-declared with only a type annotation (no initializer) clobbers the value the base constructor already set

Open
#1,771 0 comments 0 reactions 0 assignees View on GitHub

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: true makes an uninitialized field declaration emit Object.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=false with a minimal rootDir/outDir/files config, no manifest needed to observe the transpiled output.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.