Skip to content

class static post-increment through imported base yields NaN and corrupts perform-ecs #8654

Description

@proggeramlug

Summary

An imported base class's public static field is not the same initialized cell when it is read and post-incremented from a function in the defining module. Component._id++ reads as undefined, assigns NaN to both derived constructors, and leaves the externally visible Component._id at 0.

This is a correctness bug with a catastrophic performance consequence in perform-ecs: component bitmasks collide, destroyed entities remain in a component view, and the nominally linear destroy benchmark becomes a growing-array workload.

Reproduced on current main (c2da03439e6e848cfed73163c131acb601ae96dc, Perry 0.5.1519) and on PR #8645 (0ab2fb3909d8ad503b526acb23b2695f2d825bcb). PR #8645 fixes the separate Array-subclass constructor failure but does not fix this case.

Self-contained repro

base.js:

export class Component {
  static _id = 0;
}

export function makeComponent(constructor) {
  constructor.id = Component._id++;
}

main.js:

import { Component, makeComponent } from "./base.js";

class Position extends Component {}
class Velocity extends Component {}

makeComponent(Position);
makeComponent(Velocity);

console.log(JSON.stringify({
  nextId: String(Component._id),
  positionId: String(Position.id),
  velocityId: String(Velocity.id),
}));

Build and run from a Perry checkout:

cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static
node main.js
PERRY_NO_AUTO_OPTIMIZE=1 \
PERRY_RUNTIME_DIR=target/release \
target/release/perry compile main.js -o main-perry
./main-perry

Observed:

Node:  {"nextId":"2","positionId":"0","velocityId":"1"}
Perry: {"nextId":"0","positionId":"NaN","velocityId":"NaN"}

The perform-ecs integration below also reproduces through the default auto-optimize path.

Public-package impact

perform-ecs/src/Component.ts uses the same pattern:

export abstract class Component {
  static readonly _id: number = 0;
  static readonly id: number;
}

export function makeComponent(constructor: any) {
  constructor.id = (<any>Component)._id++;
}

In ddmills/js-ecs-benchmarks' perform-ecs / Destroy case:

State after destroy Node Perry
component IDs [0, 1] [NaN, NaN]
retained view entries after 100 destroys 0 200
retained view entries after 100,000 destroys 0 200,000

At full scale on an M1 Mac mini, Perry took 169.70 s and peaked at 256,832 KiB RSS. Block time increased from 1.95 s for the first 10,000 destroys to 31.07 s for the last 50,000, consistent with the ever-growing retained array. This timing must not be treated as a valid performance comparison until the semantic bug is fixed.

Expected behavior

  • A public static field has one canonical initialized storage cell across its defining module and compiled consumers.
  • Component._id++ returns the old numeric value and stores the incremented numeric value.
  • The repro prints the same values as Node.
  • perform-ecs assigns distinct component IDs and retains zero view entries after destruction.

Acceptance criteria

  • Add a two-module native regression based on the repro above and run it in both prebuilt-runtime and auto-optimize modes.
  • Cover post-increment from the defining module, post-increment from an importing module, and reads from both modules.
  • Include a forced-GC variant so moving/registration does not split or lose the static cell.
  • Add an ecosystem regression using the perform-ecs component-registration shape: IDs [0, 1], distinct masks, and zero retained entities after add/destroy.
  • Preserve the class-semantics coverage in test262 language/class tail — 175 (self-contained worklist) #5893/fix(runtime): finish class semantics follow-up #8645.

Likely area

The symptom points to static-field metadata/storage identity across module lowering: the defining-module function appears not to resolve the same initialized static cell that main.js observes. Relevant areas include static_field_meta, static initialization lowering, shared mutable capture, and class registration/parent-static storage.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regressionparityCompatibility gap with Node.js, ECMAScript, or the supported ecosystemperformanceRuntime, compile-time, build-size, or memory performance

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions