diff --git a/tslang/lib/TypeScript/MLIRGenAccessCall.cpp b/tslang/lib/TypeScript/MLIRGenAccessCall.cpp index b3bdac111..6f722380e 100644 --- a/tslang/lib/TypeScript/MLIRGenAccessCall.cpp +++ b/tslang/lib/TypeScript/MLIRGenAccessCall.cpp @@ -537,8 +537,9 @@ namespace mlirgen } } - mlir::Value MLIRGenImpl::ClassAccessorAccess(ClassInfo::TypePtr classInfo, - mlir::Location location, mlir::Value thisValue, int accessorIndex, mlir_ts::AccessLevel accessingFromLevel, const GenContext &genContext) + mlir::Value MLIRGenImpl::ClassAccessorAccess(ClassInfo::TypePtr classInfo, + mlir::Location location, mlir::Value thisValue, int accessorIndex, + bool isSuperClass, mlir_ts::AccessLevel accessingFromLevel, const GenContext &genContext) { auto accessorInfo = classInfo->accessors[accessorIndex]; @@ -589,8 +590,9 @@ namespace mlirgen } else { + auto effectiveThisValue = getThisRefOfClass(location, classInfo->classType, thisValue, isSuperClass, genContext); auto thisAccessorOp = builder.create( - location, accessorResultType, thisValue, + location, accessorResultType, effectiveThisValue, getFunc ? mlir::FlatSymbolRefAttr::get(builder.getContext(), getFunc.name) : mlir::FlatSymbolRefAttr{}, setFunc ? mlir::FlatSymbolRefAttr::get(builder.getContext(), setFunc.name) @@ -736,7 +738,7 @@ namespace mlirgen auto accessorIndex = classInfo->getAccessorIndex(name); if (accessorIndex >= 0) { - return ClassAccessorAccess(classInfo, location, thisValue, accessorIndex, accessingFromLevel, genContext); + return ClassAccessorAccess(classInfo, location, thisValue, accessorIndex, isSuperClass, accessingFromLevel, genContext); } for (auto [index, baseClass] : enumerate(classInfo->baseClasses)) diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index 02e58637f..ec886dd59 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -5522,8 +5522,9 @@ class MLIRGenImpl mlir::Location location, mlir::Value thisValue, int genericMethodIndex, bool isSuperClass, mlir_ts::AccessLevel accessingFromLevel, const GenContext &genContext); - mlir::Value ClassAccessorAccess(ClassInfo::TypePtr classInfo, - mlir::Location location, mlir::Value thisValue, int accessorIndex, mlir_ts::AccessLevel accessingFromLevel, const GenContext &genContext); + mlir::Value ClassAccessorAccess(ClassInfo::TypePtr classInfo, + mlir::Location location, mlir::Value thisValue, int accessorIndex, + bool isSuperClass, mlir_ts::AccessLevel accessingFromLevel, const GenContext &genContext); // TODO: why isSuperClass is not used here? mlir::Value ClassIndexAccess(ClassInfo::TypePtr classInfo, diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 0debe9e6b..e6acb1a35 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -235,6 +235,7 @@ add_test(NAME test-compile-00-class-static-block COMMAND test-runner "${PROJECT_ add_test(NAME test-compile-00-class-discover-types COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_discover_types.ts") add_test(NAME test-compile-00-class-accessor COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor.ts") add_test(NAME test-compile-00-class-accessor-2 COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor2.ts") +add_test(NAME test-compile-00-class-accessor-super COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_super.ts") add_test(NAME test-compile-00-class-super COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super.ts") add_test(NAME test-compile-00-class-super-static COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super_static.ts") add_test(NAME test-compile-00-class-default-constructor COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_def_constr.ts") @@ -596,6 +597,7 @@ add_test(NAME test-jit-00-class-static-block COMMAND test-runner -jit "${PROJECT add_test(NAME test-jit-00-class-discover-types COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_discover_types.ts") add_test(NAME test-jit-00-class-accessor COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor.ts") add_test(NAME test-jit-00-class-accessor-2 COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor2.ts") +add_test(NAME test-jit-00-class-accessor-super COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_accessor_super.ts") add_test(NAME test-jit-00-class-super COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super.ts") add_test(NAME test-jit-00-class-super-static COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_super_static.ts") add_test(NAME test-jit-00-class-default-constructor COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00class_def_constr.ts") @@ -850,6 +852,23 @@ add_test(NAME test-compile-export-import-class-interface COMMAND test-runner "${ add_test(NAME test-compile-export-import-class-extends COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends.ts") add_test(NAME test-compile-export-import-class-extends-multilevel COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends_multilevel.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends_multilevel.ts") add_test(NAME test-compile-export-import-class-extends-implements-diamond COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends_implements_diamond.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends_implements_diamond.ts") +add_test(NAME test-compile-export-import-class-abstract COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_abstract.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_abstract.ts") +add_test(NAME test-compile-export-import-class-static COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_static.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_static.ts") +# Cross-module generic class instantiation: DISABLED, known issue found 2026-07-22. A generic +# class specialization instantiated in the IMPORTING module incorrectly inherits `export` +# (dllexport) linkage from the original `export class` declaration, even though the +# specialization is defined fresh, locally, in this module. For a multi-type-param generic +# (Pair) the resulting mangled name contains a raw comma +# (M.Pair..instanceOf), which is a linker-directive metacharacter - +# lld rejects it with "invalid /export:". Separately, the -shared tiers fail even earlier: +# once a compiled .dll/.lib for the exporting module already exists next to its .ts source, +# import resolution reads declarations back from that binary artifact instead of +# reparsing source, and that path has no representation for an uninstantiated generic +# template at all, so the whole namespace (M) fails to resolve ("can't resolve name: M"). +# Re-enable once both are fixed; test files are kept in the tree (export_class_generic.ts / +# import_class_generic.ts) for that follow-up. +# add_test(NAME test-compile-export-import-class-generic COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_generic.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_generic.ts") +add_test(NAME test-compile-export-import-class-accessor COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_accessor.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_accessor.ts") # shared libs tests (dlls/compile-time) add_test(NAME test-compile-time-shared-decl-emit-type COMMAND test-runner -shared -compile-time "${PROJECT_SOURCE_DIR}/test/tester/tests/emit_compiletime_func.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/decl_type.ts") @@ -878,6 +897,16 @@ add_test(NAME test-compile-shared-export-import-object-literal-with-class-types add_test(NAME test-compile-shared-export-import-class-extends COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends.ts") add_test(NAME test-compile-shared-export-import-class-extends-implements-diamond COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends_implements_diamond.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends_implements_diamond.ts") add_test(NAME test-compile-shared-export-import-class-extends-multilevel COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends_multilevel.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends_multilevel.ts") +add_test(NAME test-compile-shared-export-import-class-abstract COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_abstract.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_abstract.ts") +add_test(NAME test-compile-shared-export-import-class-static COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_static.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_static.ts") +# DISABLED: see the matching test-compile-export-import-class-generic comment above (known issue, 2026-07-22). +# add_test(NAME test-compile-shared-export-import-class-generic COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_generic.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_generic.ts") +# DISABLED: known issue found 2026-07-22 (separate from the super-accessor-call crash fixed +# above). Under the -shared dynamic-import path, a base class's get/set accessors are not +# resolvable at all from a derived class in another module ("Class member 'celsius' can't be +# found") - the same class of gap as the disabled generic-class tests: declaration +# reconstruction for a dynamically-imported class is incomplete for some member kinds. +# add_test(NAME test-compile-shared-export-import-class-accessor COMMAND test-runner -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_accessor.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_accessor.ts") add_test(NAME test-compile-shared-export-import-object-literal-with-interface COMMAND test-runner -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_with_interface.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_with_interface.ts") add_test(NAME test-compile-shared-export-import-object-literal-untyped COMMAND test-runner -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_untyped.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_untyped.ts") add_test(NAME test-compile-shared-export-import-object-literal-untyped-multi-method COMMAND test-runner -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_untyped_multi_method.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_untyped_multi_method.ts") @@ -907,6 +936,12 @@ add_test(NAME test-jit-shared-export-import-object-literal-with-class-types COMM add_test(NAME test-jit-shared-export-import-class-extends COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends.ts") add_test(NAME test-jit-shared-export-import-class-extends-multilevel COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends_multilevel.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends_multilevel.ts") add_test(NAME test-jit-shared-export-import-class-extends-implements-diamond COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_extends_implements_diamond.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_extends_implements_diamond.ts") +add_test(NAME test-jit-shared-export-import-class-abstract COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_abstract.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_abstract.ts") +add_test(NAME test-jit-shared-export-import-class-static COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_static.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_static.ts") +# DISABLED: see the matching test-compile-export-import-class-generic comment above (known issue, 2026-07-22). +# add_test(NAME test-jit-shared-export-import-class-generic COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_generic.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_generic.ts") +# DISABLED: see the matching test-compile-shared-export-import-class-accessor comment above (known issue, 2026-07-22). +# add_test(NAME test-jit-shared-export-import-class-accessor COMMAND test-runner -jit -shared "${PROJECT_SOURCE_DIR}/test/tester/tests/import_class_accessor.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_class_accessor.ts") add_test(NAME test-jit-shared-export-import-object-literal-with-interface COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_with_interface.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_with_interface.ts") add_test(NAME test-jit-shared-export-import-object-literal-untyped COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_untyped.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_untyped.ts") add_test(NAME test-jit-shared-export-import-object-literal-untyped-multi-method COMMAND test-runner -jit -shared -gctors-as-method "${PROJECT_SOURCE_DIR}/test/tester/tests/import_object_literal_untyped_multi_method.ts" "${PROJECT_SOURCE_DIR}/test/tester/tests/export_object_literal_untyped_multi_method.ts") diff --git a/tslang/test/tester/tests/00class_accessor_super.ts b/tslang/test/tester/tests/00class_accessor_super.ts new file mode 100644 index 000000000..4d00cef90 --- /dev/null +++ b/tslang/test/tester/tests/00class_accessor_super.ts @@ -0,0 +1,46 @@ +class Temperature { + protected _celsius: number = 0; + + get celsius(): number { + return this._celsius; + } + + set celsius(v: number) { + this._celsius = v; + } + + get fahrenheit(): number { + return this._celsius * 9 / 5 + 32; + } +} + +// overriding an accessor and calling back into the base implementation via +// `super.` / `super. = value` used to crash MLIR +// verification: the setter call's `this` operand was left as a raw by-value +// ClassStorageType struct instead of being materialized to a pointer (the +// same repair ordinary `super.method()` calls already got via +// getThisRefOfClass), so `'llvm.call' op operand type mismatch` was thrown +// the first time this combination was exercised (found via the cross-module +// accessor test, but the bug itself is general, not cross-module-specific). +class ClampedTemperature extends Temperature { + get celsius(): number { + return super.celsius; + } + + set celsius(v: number) { + super.celsius = v < 0 ? 0 : v; + } +} + +function main() { + const t = new ClampedTemperature(); + t.celsius = -10; + assert(t.celsius == 0); + assert(t.fahrenheit == 32); + + t.celsius = 100; + assert(t.celsius == 100); + assert(t.fahrenheit == 212); + + print("done."); +} diff --git a/tslang/test/tester/tests/export_class_abstract.ts b/tslang/test/tester/tests/export_class_abstract.ts new file mode 100644 index 000000000..f88f6a1e1 --- /dev/null +++ b/tslang/test/tester/tests/export_class_abstract.ts @@ -0,0 +1,30 @@ +namespace M { + + // 2-level abstract chain (abstract extends abstract) defined entirely in + // the exporting module - the importer supplies the first concrete + // override. This is the class analogue of the cross-module optional/vtable + // interface coverage but for abstract methods specifically: an abstract + // method's vtable slot has no implementation in the declaring module at + // all, so the slot must be patched purely from a module that never even + // sees the base class definition being compiled alongside it. + + export abstract class Shape { + color: string = "red"; + + abstract area(): number; + + describe(): string { + return `${this.color} area=${this.area()}`; + } + } + + export abstract class NamedShape extends Shape { + name: string = "shape"; + + abstract area(): number; + + describe(): string { + return `${this.name}:${super.describe()}`; + } + } +} diff --git a/tslang/test/tester/tests/export_class_accessor.ts b/tslang/test/tester/tests/export_class_accessor.ts new file mode 100644 index 000000000..79a69f781 --- /dev/null +++ b/tslang/test/tester/tests/export_class_accessor.ts @@ -0,0 +1,27 @@ +namespace M { + + // A get/set accessor pair defined in the exporting module, overridden in + // an importer subclass which calls back into the base accessor via + // `super.celsius`. `super. = value` across a module boundary + // used to crash MLIR verification (the `this` operand for the setter + // call was left as a raw by-value ClassStorageType struct instead of + // being materialized to a pointer via getThisRefOfClass, unlike ordinary + // super method calls) - fixed by threading isSuperClass through + // ClassAccessorAccess. + + export class Temperature { + protected _celsius: number = 0; + + get celsius(): number { + return this._celsius; + } + + set celsius(v: number) { + this._celsius = v; + } + + get fahrenheit(): number { + return this._celsius * 9 / 5 + 32; + } + } +} diff --git a/tslang/test/tester/tests/export_class_generic.ts b/tslang/test/tester/tests/export_class_generic.ts new file mode 100644 index 000000000..0333c8949 --- /dev/null +++ b/tslang/test/tester/tests/export_class_generic.ts @@ -0,0 +1,38 @@ +namespace M { + + // Generic class exported from one module and instantiated with concrete + // type arguments in another. Each distinct instantiation (Box, + // Box, Pair) needs its own correctly-laid-out + // fields/methods to be materialized across the module boundary, not just + // a single generic-erased shape. + + export class Box { + value: T; + + constructor(v: T) { + this.value = v; + } + + get(): T { + return this.value; + } + + set(v: T): void { + this.value = v; + } + } + + export class Pair { + first: A; + second: B; + + constructor(a: A, b: B) { + this.first = a; + this.second = b; + } + + swapDescribe(): string { + return `${this.second}-${this.first}`; + } + } +} diff --git a/tslang/test/tester/tests/export_class_static.ts b/tslang/test/tester/tests/export_class_static.ts new file mode 100644 index 000000000..ead1c24c9 --- /dev/null +++ b/tslang/test/tester/tests/export_class_static.ts @@ -0,0 +1,22 @@ +namespace M { + + // Static fields/methods defined in the exporting module, inherited (not + // shadowed) by a subclass in the importer. TS static members are shared, + // not per-subclass storage - `Derived.count` and `Base.count` must be the + // SAME global cell across a module boundary, and `super.increment()` + // called from a static method in the importer must reach the base's + // static method and mutate the base's storage in place. + + export class Counter { + static count: number = 0; + static label: string = "counter"; + + static increment(n: number): void { + Counter.count = Counter.count + n; + } + + static describe(): string { + return `${Counter.label}:${Counter.count}`; + } + } +} diff --git a/tslang/test/tester/tests/import_class_abstract.ts b/tslang/test/tester/tests/import_class_abstract.ts new file mode 100644 index 000000000..bc8047e17 --- /dev/null +++ b/tslang/test/tester/tests/import_class_abstract.ts @@ -0,0 +1,34 @@ +import './export_class_abstract' + +class Circle extends M.NamedShape { + radius: number = 2; + + constructor() { + super(); + this.name = "circle"; + } + + area(): number { + return 3 * this.radius * this.radius; // fake pi=3 to keep the check integer-exact + } +} + +function main() { + const c = new Circle(); + assert(c.area() == 12); + assert(c.describe() == "circle:red area=12"); + + // virtual dispatch of both the abstract method (area) and the concrete + // override (describe, which itself calls the abstract method through + // `this`) must still reach Circle's implementation through every + // abstract-typed ancestor reference. + const asNamedShape: M.NamedShape = c; + assert(asNamedShape.area() == 12); + assert(asNamedShape.describe() == "circle:red area=12"); + + const asShape: M.Shape = c; + assert(asShape.area() == 12); + assert(asShape.describe() == "circle:red area=12"); + + print("done."); +} diff --git a/tslang/test/tester/tests/import_class_accessor.ts b/tslang/test/tester/tests/import_class_accessor.ts new file mode 100644 index 000000000..de33365da --- /dev/null +++ b/tslang/test/tester/tests/import_class_accessor.ts @@ -0,0 +1,31 @@ +import './export_class_accessor' + +class ClampedTemperature extends M.Temperature { + get celsius(): number { + return super.celsius; + } + + set celsius(v: number) { + super.celsius = v < 0 ? 0 : v; + } +} + +function main() { + const t = new ClampedTemperature(); + t.celsius = -10; + assert(t.celsius == 0); + assert(t.fahrenheit == 32); + + t.celsius = 100; + assert(t.celsius == 100); + assert(t.fahrenheit == 212); + + // NOTE: accessing an overridden accessor through a base-typed reference + // (`const asBase: M.Temperature = t; asBase.celsius`) is a known, + // pre-existing, non-cross-module-specific gap: get/set accessors are not + // part of the vtable, so such access resolves statically to the base's + // own accessor rather than dispatching virtually. Not exercised here - + // this test only covers the super-accessor-call crash fix. + + print("done."); +} diff --git a/tslang/test/tester/tests/import_class_generic.ts b/tslang/test/tester/tests/import_class_generic.ts new file mode 100644 index 000000000..20307d96b --- /dev/null +++ b/tslang/test/tester/tests/import_class_generic.ts @@ -0,0 +1,18 @@ +import './export_class_generic' + +function main() { + const b = new M.Box(10); + assert(b.get() == 10); + b.set(20); + assert(b.get() == 20); + + const bs = new M.Box("hi"); + assert(bs.get() == "hi"); + bs.set("bye"); + assert(bs.get() == "bye"); + + const p = new M.Pair(1, "one"); + assert(p.swapDescribe() == "one-1"); + + print("done."); +} diff --git a/tslang/test/tester/tests/import_class_static.ts b/tslang/test/tester/tests/import_class_static.ts new file mode 100644 index 000000000..e251554ce --- /dev/null +++ b/tslang/test/tester/tests/import_class_static.ts @@ -0,0 +1,29 @@ +import './export_class_static' + +class NamedCounter extends M.Counter { + static incrementTwice(n: number): void { + super.increment(n); + super.increment(n); + } +} + +function main() { + M.Counter.increment(5); + assert(M.Counter.count == 5); + assert(NamedCounter.count == 5); + + NamedCounter.incrementTwice(10); + assert(M.Counter.count == 25); + assert(NamedCounter.count == 25); + + assert(M.Counter.describe() == "counter:25"); + assert(NamedCounter.describe() == "counter:25"); + + // mutate the inherited static field through the derived class name and + // confirm the base sees it too - proves shared storage, not a copy + NamedCounter.label = "derived"; + assert(M.Counter.label == "derived"); + assert(M.Counter.describe() == "derived:25"); + + print("done."); +}