mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
Support declared JavaScript fields and static counters
- Compile JavaScript instance fields with native undefined initialization and ordered resets. - Support prefix and postfix updates to numeric static fields through exact class bindings.
This commit is contained in:
@@ -35,6 +35,7 @@ const results = await Promise.all(jobs); // number[] — compiles
|
||||
```
|
||||
|
||||
- `undefined` and `null` compile both as standalone values and as union arms. Optional record and class fields compile, as do optional/default/rest parameters.
|
||||
- Numeric static fields support prefix/postfix `++` and `--` through the declaring class or an immutable alias. Updates through a subclass name remain unsupported because JavaScript creates a separate property on that subclass.
|
||||
- Symbol-keyed instance fields compile with an initializer or an unconditional constructor assignment when the key is a stable module-level `Symbol()`, `Symbol("literal")`, or `Symbol.for("literal")` binding. Equal registry keys share a field across modules. Reassigned keys, runtime-computed descriptions, uninitialized computed fields, distinct symbols with the same description in one class, copying symbol fields with rest/spread or `Object.assign`, and symbol-keyed access through `unknown` remain unsupported.
|
||||
|
||||
**Standard library**
|
||||
@@ -54,7 +55,7 @@ const results = await Promise.all(jobs); // number[] — compiles
|
||||
**The `any`/`unknown` boundary**
|
||||
|
||||
- `any` without `--dynamic` is a compile error (SC2011) — use `unknown` and a checked cast, or opt into the engine.
|
||||
- `any` and `unknown` ride locals, parameters, and returns — never class fields, array elements, or union arms (record and tuple fields hold `unknown`). Program class instances can pass through `unknown` and recover at their exact static class type without losing identity, as can supported native handles; JSON-shaped records and arrays retain the copy boundary described below.
|
||||
- `any` and `unknown` ride locals, parameters, and returns; record and tuple fields hold `unknown`. JavaScript instance fields can also hold native checked values without a JavaScript engine: bare fields start as `undefined`, and redeclaring a field with the same checked-value type resets it in initialization order. Explicit TypeScript `any`/`unknown` class fields, static checked-value fields, statically typed array elements, and union arms remain unsupported in static builds. Program class instances can pass through `unknown` and recover at their exact static class type without losing identity, as can supported native handles; JSON-shaped records and arrays retain the copy boundary described below.
|
||||
- Operations on `unknown` beyond the supported surface (truthiness, `typeof` narrowing, property access, `+`, `switch`, `throw`) need a checked cast first.
|
||||
|
||||
**Type surface**
|
||||
|
||||
@@ -1706,13 +1706,17 @@ export class CEmitter {
|
||||
* assigns later, a constructor branch skips it, a base constructor's
|
||||
* virtual call reads a derived field before super() returns. Node reads
|
||||
* `undefined` there; a NULL payload pointer would be a segfault (union
|
||||
* fields) or a silent nothing (jsval fields). Undefined-armed unions get
|
||||
* fields), or an invalid dyn/jsval cell. Native checked-dynamic fields
|
||||
* get the immortal dyn undefined. Undefined-armed unions get
|
||||
* the interned immortal unit instance (free; releases skip it); jsval
|
||||
* (`any`) fields get an engine undefined cell — such classes exist only
|
||||
* in --dynamic builds, and the field's release balances it. Empty for
|
||||
* every type that cannot hold undefined (tsc's SPI guards those) and for
|
||||
* record shapes' construction paths, which write every field. */
|
||||
undefFieldInitLineC(name: string, t: IrType): string[] {
|
||||
if (t.kind === "dyn") {
|
||||
return [` o->${mangleField(name)} = scr_dyn_undefined(); /* ${cCommentText(name)} starts undefined */`];
|
||||
}
|
||||
if (t.kind === "jsval") {
|
||||
return [` o->${mangleField(name)} = scr_jsval_undefined(); /* ${cCommentText(name)} starts undefined */`];
|
||||
}
|
||||
|
||||
@@ -199,16 +199,18 @@ export interface ClassHost extends ShapeHost {
|
||||
|
||||
/** The newFn initialization stores for fields whose type ADMITS undefined
|
||||
* (undefFieldInitLineC's LLVM twin): undefined-armed union fields start
|
||||
* at the interned unit instance; jsval fields (an `any` class field under
|
||||
* --dynamic) start at the engine's undefined cell. */
|
||||
* at the interned unit instance; dyn fields start at native undefined;
|
||||
* jsval fields (an `any` class field under --dynamic) start at the engine's
|
||||
* undefined cell. */
|
||||
function undefFieldInits(host: ClassHost, meta: LlClassMeta): string[] {
|
||||
const out: string[] = [];
|
||||
meta.def.fields.forEach((f, i) => {
|
||||
const { index } = classFieldIndex(meta, f.name);
|
||||
if (f.type.kind === "jsval") {
|
||||
host.declare(`declare ptr @scr_jsval_undefined()`);
|
||||
if (f.type.kind === "jsval" || f.type.kind === "dyn") {
|
||||
const undefinedFn = f.type.kind === "dyn" ? "scr_dyn_undefined" : "scr_jsval_undefined";
|
||||
host.declare(`declare ptr @${undefinedFn}()`);
|
||||
out.push(
|
||||
` %ufv${i} = call ptr @scr_jsval_undefined()`,
|
||||
` %ufv${i} = call ptr @${undefinedFn}()`,
|
||||
` %uf${i} = getelementptr inbounds %${mangleClassStruct(meta.def.name)}, ptr %o, i64 0, i32 ${index}`,
|
||||
` store ptr %ufv${i}, ptr %uf${i} ; ${llvmCommentText(f.name)} starts undefined`,
|
||||
);
|
||||
|
||||
@@ -35,7 +35,17 @@ export interface ClassInfo {
|
||||
/** OWN fields only (declaration order) with their initializers: the
|
||||
* class's constructor runs exactly these — inherited fields initialize in
|
||||
* the base constructor, before/via super(). */
|
||||
fieldOrder: { name: string; type: IrType; initializer: ts.Expression | undefined; /** Redeclared INHERITED field: the initializer assigns the base slot at this position; no new slot (def.fields excludes it). */ redeclared?: true }[];
|
||||
fieldOrder: {
|
||||
name: string;
|
||||
type: IrType;
|
||||
initializer: ts.Expression | undefined;
|
||||
/** A bare JS dyn field defines undefined at its declaration position,
|
||||
* even if an earlier initializer or base constructor wrote the slot. */
|
||||
undefinedInitializer?: SrcLoc;
|
||||
/** Redeclared field: initialize the existing slot at this position;
|
||||
* no new slot (def.fields excludes it). */
|
||||
redeclared?: true;
|
||||
}[];
|
||||
/** OWN declared methods only — inherited lookups walk the base chain
|
||||
* (findMethodOn). An `abstract` entry is a signature with no body (and
|
||||
* no module function): it declares the vtable slot; concrete subclasses
|
||||
@@ -1572,11 +1582,15 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
// the ordinary undefined-armed union machinery.
|
||||
const type = lowerer.irTypeOf(member.name);
|
||||
if (type.kind === "void") lowerer.badType(member.name, lowerer.typeOf(member.name));
|
||||
// dyn stays out of class fields (KEEP NARROW; record
|
||||
// fields and array elements are unmappable via mapType already).
|
||||
if (type.kind === "dyn") {
|
||||
// Bundled JS declares fields without annotations before assigning
|
||||
// them in the constructor. Use the same native checked-dynamic
|
||||
// storage as implicit constructor-assigned JS fields.
|
||||
if (type.kind === "dyn" && !isJsSourceFile(member.getSourceFile())) {
|
||||
lowerer.unsupported("SC1090", member.name, "'unknown'-typed class fields");
|
||||
}
|
||||
const undefinedInitializer = type.kind === "dyn" && !member.initializer
|
||||
? { undefinedInitializer: locOf(member) }
|
||||
: {};
|
||||
if (fields.has(member.name.text)) {
|
||||
// REDECLARING an inherited field: Node [[Define]]s the OWN
|
||||
// property again when THIS class's field initializers run
|
||||
@@ -1586,13 +1600,12 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
// slot, no new slot, no layout change (the `class
|
||||
// ConfigError extends Error { name = "ConfigError" }`; the
|
||||
// builtin Error prefix included — reads, toString, and throw
|
||||
// reports all answer the overwritten name like Node). A BARE
|
||||
// redeclare writes undefined in Node (`class B extends A
|
||||
// { x; }` reads undefined!) and a type-changing redeclare has
|
||||
// no single slot type — both keep the fence.
|
||||
// reports all answer the overwritten name like Node). A bare
|
||||
// JS dyn redeclaration resets the existing slot to undefined.
|
||||
// Other bare or type-changing redeclarations keep the fence.
|
||||
const baseType = fields.get(member.name.text)!;
|
||||
if (member.initializer && typeEquals(type, baseType)) {
|
||||
fieldOrder.push({ name: member.name.text, type, initializer: member.initializer, redeclared: true });
|
||||
if ((member.initializer || type.kind === "dyn") && typeEquals(type, baseType)) {
|
||||
fieldOrder.push({ name: member.name.text, type, initializer: member.initializer, ...undefinedInitializer, redeclared: true });
|
||||
continue;
|
||||
}
|
||||
lowerer.unsupported(
|
||||
@@ -1622,7 +1635,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
const admitsUndefined =
|
||||
(type.kind === "union" &&
|
||||
(lowerer.unions.get(type.unionId)?.arms.some((a) => a.kind === "undefinedT") ?? false)) ||
|
||||
type.kind === "jsval";
|
||||
type.kind === "jsval" || type.kind === "dyn";
|
||||
if (!member.initializer && !admitsUndefined) {
|
||||
const opts = lowerer.program.getCompilerOptions();
|
||||
const spi = opts.strictPropertyInitialization ?? opts.strict ?? false;
|
||||
@@ -1641,7 +1654,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
}
|
||||
}
|
||||
fields.set(member.name.text, type);
|
||||
fieldOrder.push({ name: member.name.text, type, initializer: member.initializer });
|
||||
fieldOrder.push({ name: member.name.text, type, initializer: member.initializer, ...undefinedInitializer });
|
||||
} else if (ts.isConstructorDeclaration(member)) {
|
||||
// A body-less constructor is an OVERLOAD SIGNATURE: type-world,
|
||||
// lowers to nothing — tsc resolved each `new` against the
|
||||
@@ -3009,6 +3022,24 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
|
||||
return a.length === b.length && a.every((p, i) => p.mode === b[i]!.mode && typeEquals(p.type, b[i]!.type));
|
||||
}
|
||||
|
||||
/** Writable storage for an own static field named through its exact class.
|
||||
* Inherited writes create a new subclass property in JS, so they must not
|
||||
* update the declaring class's global. */
|
||||
export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAccessExpression): { id: string; type: IrType } | null {
|
||||
if (access.questionDotToken || !ts.isIdentifier(access.expression)) return null;
|
||||
const info = lowerer.exactClassOfReceiver(access.expression);
|
||||
if (!info) return null;
|
||||
const found = lowerer.findStaticOn(info, access.name.text);
|
||||
if (!found?.field) return null;
|
||||
if (found.declarer !== info) {
|
||||
lowerer.unsupported("SC1090", access, `assigning the inherited static '${access.name.text}' through a subclass name (JS creates an OWN property on the subclass — assign through '${found.declarer.def.jsName ?? found.declarer.def.name}' instead)`);
|
||||
}
|
||||
if (found.field.readonly) {
|
||||
lowerer.unsupported("SC1090", access, `assigning the readonly static '${access.name.text}'`);
|
||||
}
|
||||
return { id: found.field.globalId, type: found.field.type };
|
||||
}
|
||||
|
||||
/** `C.x` where C is a class declared in the program and x a static
|
||||
* member of its chain: field reads are the module global, static
|
||||
* methods become interned closures, and `.name` folds to the class's
|
||||
@@ -4354,6 +4385,17 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
|
||||
const out: IrStmt[] = [];
|
||||
const thisType: IrType = { kind: "object", className: info.def.name };
|
||||
for (const f of info.fieldOrder) {
|
||||
if (f.undefinedInitializer) {
|
||||
const loc = f.undefinedInitializer;
|
||||
out.push({
|
||||
kind: "fieldSet",
|
||||
obj: { kind: "varRef", localId: thisLocal.id, type: thisType, loc },
|
||||
className: info.def.name,
|
||||
field: f.name,
|
||||
value: dynUndefinedExpr(loc),
|
||||
loc,
|
||||
});
|
||||
}
|
||||
if (!f.initializer) continue;
|
||||
lowerer.stats.statementsTotal++;
|
||||
lowerer.bumpFileStat(locOf(f.initializer).file, "total");
|
||||
|
||||
@@ -24,7 +24,7 @@ import { fenceEnumObjectValue, lowerEnumAccess } from "./lower-enums.js";
|
||||
import { ambientNsRootOf, ambientUndefReadType, ambientUndefVarRootOf, ambientUndefinedFnSymbolOf, contextualUndefReadType, fenceEarlyAliasUse, fenceEarlyNsMemberRef, lowerNsIdentifierValue, nsMemberIdentOf, nsUndefRead, nsWritableTarget } from "./lower-namespaces.js";
|
||||
import { expandoMemberRead, expandoWritableTarget } from "./lower-expando.js";
|
||||
import { lowerSocketInstanceOf, lowerTlsRootCertificates } from "./lower-server.js";
|
||||
import { findGenericMethodOn, lowerStaticFieldRead } from "./lower-classes.js";
|
||||
import { findGenericMethodOn, lowerStaticFieldRead, staticFieldWriteTarget } from "./lower-classes.js";
|
||||
import { bindingNeverReassigned, funcTypeFromParamShapes, implicitMonoFile, lowerTaggedTemplate, nullishGenericBindingUnitOf, objLitGenericFnInfoOf, objLitGenericFnNodeOf, requireObjLitGenericReceiver } from "./lower-calls.js";
|
||||
import { mixinFnOfCallee } from "./lower-mixins.js";
|
||||
import { isConstAssertionTypeNode, isGenericCallableMemberType, isParseArgsDynTypeName, underConstAssertion, unitOnlyUnion } from "../type-mapper.js";
|
||||
@@ -5911,6 +5911,13 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio
|
||||
const loc = locOf(expr);
|
||||
const op = expr.operator === ts.SyntaxKind.PlusPlusToken ? "+" : "-";
|
||||
if (!ts.isIdentifier(expr.operand)) {
|
||||
if (ts.isPropertyAccessExpression(expr.operand)) {
|
||||
const target = staticFieldWriteTarget(lowerer, expr.operand);
|
||||
if (target) {
|
||||
if (target.type.kind !== "f64") lowerer.unsupported("SC1043", expr);
|
||||
return { kind: "incDec", op, prefix, localId: target.id, type: F64, loc };
|
||||
}
|
||||
}
|
||||
// CLASS-FIELD receivers (`if (--this.limit === 0)` — countdown.js's
|
||||
// dec(); `--this[kLimit]` — its symbol-keyed spelling): a single-
|
||||
// evaluation read-modify-write over the instance, yielding old/new
|
||||
|
||||
@@ -21,7 +21,7 @@ import { ForOfIterProjection, lowerForOfArrayIter, lowerForOfMap, lowerForOfSear
|
||||
import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, bindingGenericFnInfoOf, bindingGenericFnNodeOf, bindingNeverReassigned, deadUnmappableBinding, implicitLocalFnInfoOf, implicitLocalFnNodeOf, implicitMethodCallInfersReturn, nullishExprUnitOf, nullishGenericBindingUnitOf, recordKeysArrayCall, registerOverloadedCallableAlias } from "./lower-calls.js";
|
||||
import { isMixinFnBinding, mixinResultBindingClassOf } from "./lower-mixins.js";
|
||||
import type { ClassInfo, ClassIteratorInfo } from "./lower-classes.js";
|
||||
import { genericIfaceBindingKeepsClass } from "./lower-classes.js";
|
||||
import { genericIfaceBindingKeepsClass, staticFieldWriteTarget } from "./lower-classes.js";
|
||||
import { lowerStreamUnderscoreAssign, streamClassAliasDecl } from "./lower-stream.js";
|
||||
import { lowerHttpResPropertyAssignment, lowerHttpServerTimeoutAssignment, lowerServerCloseOverrideAssignment } from "./lower-server.js";
|
||||
import { builtinMemberRequireDecl, builtinNamespaceDestructureModuleOf, createRequireBindingDecl, createRequireCalleeFileOf, createRequireNamespaceDecl, createRequireProgramModuleDecl, createRequireProgramModuleOf, lowerNodeModuleCall, registerBuiltinCallableAlias } from "./lower-builtins.js";
|
||||
@@ -5263,6 +5263,8 @@ function lowerBranchSwitch(
|
||||
// increment (the member's global IS the variable); expando
|
||||
// function members (`foo.count++`) are module globals too.
|
||||
if (ts.isPropertyAccessExpression(expr.operand) && !expr.operand.questionDotToken) {
|
||||
const staticTarget = staticFieldWriteTarget(lowerer, expr.operand);
|
||||
if (staticTarget) return lowerIncDecToTarget(lowerer, expr, staticTarget);
|
||||
const exT = expandoWritableTarget(lowerer, expr.operand);
|
||||
if (exT) return lowerIncDecToTarget(lowerer, expr, exT);
|
||||
const nsT = nsWritableTarget(lowerer, expr.operand);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { analyze } from "../src/index.js";
|
||||
|
||||
test.each(["Child.count++", "++Child.count", "Child.count--", "--Child.count"])("refuses inherited static updates: %s", (update) => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-static-update-"));
|
||||
try {
|
||||
const entry = join(dir, "main.cjs");
|
||||
writeFileSync(entry, `class Base { static count = 1; } class Child extends Base {} console.log(${update}); ${update};`);
|
||||
const { coverage } = analyze(entry);
|
||||
const diagnostics = [...coverage.diagnostics, ...(coverage.runtimeFences ?? [])];
|
||||
expect(diagnostics.filter((d) => d.code === "SC1090" && d.message.includes("assigning the inherited static")).length).toBe(2);
|
||||
expect(coverage.stats.statementsIsland).toBe(0);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("refuses updates to readonly static fields", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-static-readonly-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
writeFileSync(entry, 'class C { static readonly count = 1; }\n// @ts-expect-error Exercise the compiler backstop too.\nconsole.log(C.count++);\n');
|
||||
const { coverage } = analyze(entry);
|
||||
expect([...coverage.diagnostics, ...(coverage.runtimeFences ?? [])].some((d) => d.code === "SC1090" && d.message.includes("readonly static"))).toBe(true);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -8566,6 +8566,19 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/js-declared-fields.cjs": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-declared-fields.cjs"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/static-field-updates/main.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/static-field-updates/counter.ts",
|
||||
"<repo>/tests/corpus/static-field-updates/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/symbol-class-fields.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/symbol-class-fields.ts"
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
const { EventEmitter } = require("node:events");
|
||||
const brand = Symbol.for("@opentui/core/Renderable");
|
||||
|
||||
class BaseRenderable extends EventEmitter {
|
||||
[brand] = true;
|
||||
static renderableNumber = 1;
|
||||
_id;
|
||||
num;
|
||||
_dirty = false;
|
||||
parent = null;
|
||||
_visible = true;
|
||||
constructor(options) {
|
||||
super();
|
||||
this.num = BaseRenderable.renderableNumber++;
|
||||
this._id = options.id ?? `renderable-${this.num}`;
|
||||
}
|
||||
get id() { return this._id; }
|
||||
set id(value) { this._id = value; }
|
||||
get isDirty() { return this._dirty; }
|
||||
markClean() { this._dirty = false; }
|
||||
markDirty() { this._dirty = true; }
|
||||
get visible() { return this._visible; }
|
||||
set visible(value) { this._visible = value; }
|
||||
}
|
||||
|
||||
const first = new BaseRenderable({ id: "first" });
|
||||
const second = new BaseRenderable({});
|
||||
console.log(first.id, first.num, first[brand], first.visible, first.isDirty);
|
||||
console.log(second.id, second.num);
|
||||
first.id = "renamed";
|
||||
first.visible = false;
|
||||
first.markDirty();
|
||||
console.log(first.id, first.visible, first.isDirty);
|
||||
first.markClean();
|
||||
console.log(first.isDirty);
|
||||
|
||||
// Allocation must make early reads safe; a bare declaration then resets
|
||||
// the slot at its own position, after earlier initializer side effects.
|
||||
class Ordered {
|
||||
// @ts-expect-error Deliberately read before the declaration runs.
|
||||
before = this.later;
|
||||
write = this.seed();
|
||||
later;
|
||||
// @ts-expect-error A bare JS field is initialized to undefined.
|
||||
after = this.later;
|
||||
#private;
|
||||
seed() { this.later = "temporary"; return "temporary"; }
|
||||
readPrivate() { return this.#private; }
|
||||
setPrivate(value) { this.#private = value; }
|
||||
}
|
||||
const ordered = new Ordered();
|
||||
console.log(ordered.before, ordered.write, ordered.after, typeof ordered.later);
|
||||
console.log(ordered.readPrivate());
|
||||
ordered.setPrivate("secret");
|
||||
console.log(ordered.readPrivate());
|
||||
ordered.later = ["live"];
|
||||
console.log(ordered.later[0]);
|
||||
ordered.later = undefined;
|
||||
console.log(ordered.later === undefined);
|
||||
|
||||
// A base constructor can call an override before derived initializers.
|
||||
class EarlyBase {
|
||||
value;
|
||||
constructor(seed) {
|
||||
console.log("early", this.read(undefined));
|
||||
this.value = seed;
|
||||
}
|
||||
read(value) { return value; }
|
||||
}
|
||||
class EarlyDerived extends EarlyBase {
|
||||
// @ts-expect-error Observe the base constructor's write before the reset.
|
||||
sawBase = this.value;
|
||||
// @ts-expect-error Intentionally reset the inherited slot to undefined.
|
||||
value;
|
||||
later;
|
||||
read(value) { return this.later; }
|
||||
}
|
||||
const early = new EarlyDerived("base");
|
||||
console.log(early.sawBase, early.value, early.later);
|
||||
|
||||
// String-keyed views and inspection include uninitialized public fields.
|
||||
const { inspect } = require("node:util");
|
||||
class Empty { value; }
|
||||
const empty = new Empty();
|
||||
function stringView(value) {
|
||||
console.log(Object.keys(value).join(","), JSON.stringify(value));
|
||||
}
|
||||
console.log(inspect(empty));
|
||||
stringView(empty);
|
||||
|
||||
// Reference-valued assignments remain live and release on replacement.
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const item = new Empty();
|
||||
item.value = { name: `item-${i}` };
|
||||
if (i === 49) console.log(item.value.name);
|
||||
item.value = ["replacement"];
|
||||
item.value = undefined;
|
||||
}
|
||||
|
||||
// Prefix/postfix updates share the declaring class's numeric static slot.
|
||||
console.log(BaseRenderable.renderableNumber++, ++BaseRenderable.renderableNumber);
|
||||
console.log(BaseRenderable.renderableNumber--, --BaseRenderable.renderableNumber);
|
||||
BaseRenderable.renderableNumber++;
|
||||
--BaseRenderable.renderableNumber;
|
||||
console.log(BaseRenderable.renderableNumber);
|
||||
@@ -0,0 +1,7 @@
|
||||
export class Counter {
|
||||
static value = 10;
|
||||
static #private = 20;
|
||||
static nextPrivate(): number {
|
||||
return Counter.#private++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Counter } from "./counter.ts";
|
||||
|
||||
console.log(Counter.value++, ++Counter.value, Counter.value);
|
||||
console.log(Counter.value--, --Counter.value, Counter.value);
|
||||
Counter.value++;
|
||||
--Counter.value;
|
||||
console.log(Counter.value, Counter.nextPrivate(), Counter.nextPrivate());
|
||||
|
||||
const Alias = Counter;
|
||||
console.log(Alias.value++, Counter.value);
|
||||
const Local = class {
|
||||
static count = 3;
|
||||
};
|
||||
console.log(--Local.count, Local.count--, Local.count);
|
||||
|
||||
class Independent extends Counter {
|
||||
static override value = 100;
|
||||
}
|
||||
console.log(Independent.value++, Independent.value, Counter.value);
|
||||
Reference in New Issue
Block a user