Construct builtins through stored constructors and fix function-local class names and dispatch

This commit is contained in:
Chris Tate
2026-10-22 06:44:59 -05:00
parent 5d3dced191
commit 41ef12e463
43 changed files with 758 additions and 32 deletions
@@ -3,4 +3,6 @@
Plain objects, arrays, functions, class instances, and Date, RegExp, Map, Set, WeakMap, WeakSet, and Promise values inherit `constructor`, `valueOf`, `toString`, `hasOwnProperty`, and `propertyIsEnumerable` from `Object.prototype` as in Node, so `value.constructor === Object` and `value.valueOf === Object.prototype.valueOf` compare identities exactly.
Named reads such as `value.valueOf` answer these inherited members; computed-key reads such as `value[key]` do not.
`Object.prototype`'s own members read as values; its mutation and other reflection, such as `isPrototypeOf` and `__proto__`, are unsupported.
The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called, and `instanceof` and `new` against them remain unsupported.
The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called.
`new` through a stored `Object`, `Date`, `Map`, `Set`, `WeakMap`, or `WeakSet` builds the same value as the `new` expression.
`new` through a stored `RegExp` or `Promise`, `new` of a stored `Object` with a primitive argument, and `instanceof` against these constructors are unsupported.
@@ -1,5 +1,6 @@
- **Local class expressions:** Static fields and static blocks in TypeScript files, decorators, private brands, generic members, and references through the class's internal name are unsupported.
- **Local class expressions:** Static fields and static blocks in TypeScript files, decorators, private brands, and generic members are unsupported.
Static methods read the enclosing function's bindings and are inherited by subclasses.
A named class expression's methods and static members can refer to the class by its name.
Supported expressions preserve fresh constructor identity and shared captures across constructors, methods, accessors, and arrow fields.
Local inheritance requires a compiled base class.
Constructors retain identity, `typeof`, `name`, and `length` through `unknown`; cast back to their exact type before construction.
@@ -1,6 +1,6 @@
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
import * as ts from "../ts7/adapter.js";
import { BOOL, DYN, STRING, type IrExpr, type IrStmt } from "../../ir/ir.js";
import { BOOL, DYN, STRING, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js";
import { locOf } from "../program.js";
import type { Lowerer } from "./lowerer.js";
import { classValueRef, type ClassInfo } from "./lower-classes.js";
@@ -15,9 +15,29 @@ export function hasRuntimeStatics(info: ClassInfo): boolean {
/** Constructor descriptors preserve inherited getter receivers and static
* initialization order. Factory evaluations additionally own independent
* fields and closures when the same factory is called repeatedly. */
export function initializeRuntimeStatics(lowerer: Lowerer, info: ClassInfo, value: IrExpr): IrExpr {
if (!info.runtimeStatics && !info.runtimePrototypeMembers) return value;
* fields and closures when the same factory is called repeatedly.
*
* `binding` is the local that holds the class under its own name (a
* function-local class declaration's binding, or the inner name of a
* function-local class expression whose body names itself), declared
* without a value. It is assigned before any static member is defined, so
* static initializers and methods read the class through it. */
export function initializeRuntimeStatics(
lowerer: Lowerer,
info: ClassInfo,
value: IrExpr,
binding?: IrLocal,
): IrExpr {
if (!info.runtimeStatics && !info.runtimePrototypeMembers) {
if (!binding) return value;
return {
kind: "seqExpr",
stmts: [{ kind: "assign", localId: binding.id, value, loc: value.loc }],
result: varRef(binding.id, binding.type, value.loc),
type: binding.type,
loc: value.loc,
};
}
const loc = value.loc;
const local = lowerer.declareHiddenLocal("%staticClass", value.type);
const receiver = varRef(local.id, value.type, loc);
@@ -28,6 +48,16 @@ export function initializeRuntimeStatics(lowerer: Lowerer, info: ClassInfo, valu
: dynUndefinedExpr(loc);
const statements: IrStmt[] = [
{ kind: "varDecl", localId: local.id, init: value, loc },
...(binding
? [
{
kind: "assign" as const,
localId: binding.id,
value: lowerer.coerceToExpected(receiver, binding.type),
loc,
},
]
: []),
{
kind: "exprStmt",
expr: { kind: "libCall", fn: "dyn.classInherit", args: [boxed, base], type: DYN, loc },
@@ -69,6 +69,7 @@ import {
builtinModuleConstOf,
builtinModuleFnOf,
builtinModulesArrayLit,
identifierReadSymbol,
stdlibGlobalNameOf,
} from "../surfaces.js";
import { builtinConstantsObject } from "../lower-builtin-values.js";
@@ -753,7 +754,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
return lowerGlobalValue(lowerer, expr);
}
{
const sym = lowerer.checker.getSymbolAtLocation(expr);
const sym = identifierReadSymbol(lowerer, expr);
// The temporal global is typed (TS 7's lib slice) but Node 24 does
// not define it: the read throws Node's ReferenceError.
if (isTemporalGlobalRoot(lowerer, expr, sym)) {
@@ -3262,6 +3262,9 @@ export function lowerShorthandValue(
}
return lowerer.classValueRef(classInfo, propName);
}
// A standard-library global (`{ Date }`): the value the long form
// `{ Date: Date }` reads.
if (lowerer.isStdlibSymbol(resolved)) return lowerer.lowerExpr(propName);
}
lowerer.rejectUnresolvedSymbol(
symbol
@@ -6034,6 +6034,31 @@ export function lowerClassExpressionInfo(
return info;
}
/** True when a named class expression's body refers to the class value by
* its own name (`class Box { static of() { return new Box(); } }`); type
* annotations naming it (`again(): Box`) do not count. */
function classExpressionNamesItself(lowerer: Lowerer, expr: ts.ClassExpression): boolean {
const name = expr.name;
if (!name) return false;
const symbol = lowerer.checker.getSymbolAtLocation(name);
if (!symbol) return false;
let found = false;
for (const member of expr.members.nodes) {
ts.walkPreorder(member, (n) => {
if (found || ts.isTypeNode(n)) return "skip";
if (
ts.isIdentifier(n) &&
n.text === name.text &&
lowerer.checker.getSymbolAtLocation(n) === symbol
)
found = true;
return undefined;
});
if (found) return true;
}
return false;
}
/** `class {…}` in expression position: a class definition bound to no
* statement — once the static side is a value, the expression IS the
* definition plus a classRef over it. A DECORATED class expression
@@ -6041,8 +6066,16 @@ export function lowerClassExpressionInfo(
* mints a class at all: evaluating the expression IS the
* ReferenceError, so it lowers to exactly that read — every evaluation
* throws identically, which is why the once-evaluated restriction and
* the member fences don't apply. */
export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclaration): IrExpr {
* the member fences don't apply.
*
* `binding` is the local of a function-local class declaration's name,
* declared without a value; the class value assigns it before the class's
* static members are defined (see initializeRuntimeStatics). */
export function lowerClassExpression(
lowerer: Lowerer,
expr: ts.ClassLikeDeclaration,
binding?: IrLocal,
): IrExpr {
if (
decoratorNodesOf(expr).length > 0 ||
expr.members.nodes.some((m) => decoratorNodesOf(m).length > 0)
@@ -6058,6 +6091,41 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara
}
}
const info = lowerClassExpressionInfo(lowerer, expr);
// A function-local class expression's own name is a binding of its body:
// declared before the members lower, so their references to the name
// resolve to it. The class value captures it before it is assigned, as
// it captures a class declaration's binding.
if (
!binding &&
info.localClass &&
ts.isClassExpression(expr) &&
expr.name &&
classExpressionNamesItself(lowerer, expr)
) {
const own = lowerer.declareLocal(
expr.name,
expr.name.text,
{ kind: "classval", className: info.def.name },
true,
);
const value = lowerClassValue(lowerer, expr, info, own);
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: own.id, init: null, loc: value.loc }],
result: value,
type: value.type,
loc: value.loc,
};
}
return lowerClassValue(lowerer, expr, info, binding);
}
function lowerClassValue(
lowerer: Lowerer,
expr: ts.ClassLikeDeclaration,
info: ClassInfo,
binding: IrLocal | undefined,
): IrExpr {
if (
!info.localClass &&
!info.expressionInitializationEmitted &&
@@ -6104,7 +6172,7 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara
const clsRef: IrExpr = { kind: "varRef", localId: clsLocal.id, type: value.type, loc };
const boxed: IrExpr = { kind: "dynFrom", value: clsRef, type: DYN, loc };
info.localPrototypeData = true;
return initializeRuntimeStatics(lowerer, info, {
const built: IrExpr = {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: baseLocal.id, init: base, loc },
@@ -6137,7 +6205,8 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara
result: clsRef,
type: value.type,
loc,
});
};
return initializeRuntimeStatics(lowerer, info, built, binding);
}
if (info.localClass?.baseExpression && info.base && value.kind === "classRef") {
const loc = locOf(expr);
@@ -6152,7 +6221,7 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara
const clsRef: IrExpr = { kind: "varRef", localId: clsLocal.id, type: value.type, loc };
const basePrototype = classPrototypeData(lowerer, info.base, loc, baseRef)!;
info.localPrototypeData = true;
return initializeRuntimeStatics(lowerer, info, {
const built: IrExpr = {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: baseLocal.id, init: base, loc },
@@ -6191,13 +6260,15 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara
result: clsRef,
type: value.type,
loc,
});
};
return initializeRuntimeStatics(lowerer, info, built, binding);
}
if (!info.localClass) return value;
const classValue = initializeRuntimeStatics(
lowerer,
info,
value.kind === "classRef" ? { ...value, captures: info.localClass.captureSources } : value,
binding,
);
// The base resolves statically here, but Node still evaluates the
// heritage expression each time the class definition runs, first.
@@ -7124,12 +7195,102 @@ export function inHierarchy(lowerer: Lowerer, info: ClassInfo): boolean {
* dynamically. Abstract re-declarations don't count (they carry no
* implementation; the concrete ones below them do, via the recursion). */
export function overrideBelow(lowerer: Lowerer, info: ClassInfo, name: string): boolean {
if (info.decl !== null && localOverrideBelow(lowerer, info.decl, name)) return true;
return info.subclasses.some((s) => {
const m = s.methods.get(name);
return (m !== undefined && m.abstract !== true) || lowerer.overrideBelow(s, name);
});
}
/** The class node an `extends` clause names: a class declaration, or the
* class expression a variable is initialized with. */
function extendedClassNode(lowerer: Lowerer, decl: ts.ClassLikeDeclaration): ts.Node | null {
const heritage = decl.heritageClauses?.nodes.find(
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
)?.types?.nodes[0]?.expression;
if (!heritage || !ts.isIdentifier(heritage)) return null;
let symbol = lowerer.checker.getSymbolAtLocation(heritage);
if (symbol && symbol.flags & ts.SymbolFlags.Alias)
symbol = lowerer.checker.getAliasedSymbol(symbol);
const target = symbol?.valueDeclaration;
if (!target) return null;
if (ts.isClassDeclaration(target) || ts.isClassExpression(target)) return target;
if (ts.isVariableDeclaration(target) && target.initializer) {
let init: ts.Expression = target.initializer;
while (ts.isParenthesizedExpression(init)) init = init.expression;
if (ts.isClassExpression(init)) return init;
}
return null;
}
/** The function-local classes that extend the class node `base`. Function
* bodies lower on demand, after code that may already call methods of a
* class their local classes extend, so such a subclass is not yet in its
* base's `subclasses` when those calls lower. */
function localSubclassesOf(lowerer: Lowerer, base: ts.Node): readonly ts.ClassLikeDeclaration[] {
let index = lowerer.localSubclassIndex;
if (index === null) {
index = new Map();
const files = lowerer.moduleOrder.length > 0 ? lowerer.moduleOrder : [lowerer.entry];
for (const sf of files) {
if (!sf.text.includes("extends")) continue;
ts.walkPreorder(sf, (node) => {
if (
(ts.isClassDeclaration(node) || ts.isClassExpression(node)) &&
node.heritageClauses !== undefined &&
functionLocalClass(node)
) {
const extended = extendedClassNode(lowerer, node);
if (extended) {
const list = index!.get(extended);
if (list) list.push(node);
else index!.set(extended, [node]);
}
}
return undefined;
});
}
lowerer.localSubclassIndex = index;
}
return index.get(base) ?? [];
}
/** True when a function-local class below the class node `base` declares
* a concrete instance method or accessor `name` (accessors are keyed
* `get:x` and `set:x`). A call on `base`'s class then dispatches on the
* receiver; devirtualization turns it back into a direct call when no such
* override is reachable. */
function localOverrideBelow(
lowerer: Lowerer,
base: ts.Node,
name: string,
visited?: Set<ts.Node>,
): boolean {
const subclasses = localSubclassesOf(lowerer, base);
if (subclasses.length === 0) return false;
visited ??= new Set();
if (visited.has(base)) return false;
visited.add(base);
const accessor = name.startsWith("get:") ? "get" : name.startsWith("set:") ? "set" : null;
const member = accessor ? name.slice(4) : name;
return subclasses.some(
(sub) =>
sub.members.nodes.some(
(m) =>
(accessor === null
? ts.isMethodDeclaration(m)
: accessor === "get"
? ts.isGetAccessorDeclaration(m)
: ts.isSetAccessorDeclaration(m)) &&
m.body !== undefined &&
m.name !== undefined &&
(ts.isIdentifier(m.name) || ts.isStringLiteral(m.name)) &&
m.name.text === member &&
!ts.getModifiers(m)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword),
) || localOverrideBelow(lowerer, sub, name, visited),
);
}
/** The nearest GENERIC-method declaration of `name` at/above `info` —
* findMethodOn's twin over the genericMethods tables. */
export function findGenericMethodOn(
@@ -1523,6 +1523,14 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt
const info = lowerer.lowerClassExpressionInfo(stmt);
const type: IrType = { kind: "classval", className: info.def.name };
const local = lowerer.declareLocal(stmt.name, stmt.name.text, type, true);
// Static members of a function-local class run while the class value is
// built and may read the class by name: the class value assigns the
// binding before them.
if (info.localClass && (info.runtimeStatics || info.runtimePrototypeMembers))
return [
{ kind: "varDecl", localId: local.id, init: null, loc: locOf(stmt) },
{ kind: "exprStmt", expr: lowerer.lowerClassExpression(stmt, local), loc: locOf(stmt) },
];
const init = lowerer.lowerClassExpression(stmt);
return { kind: "varDecl", localId: local.id, init, loc: locOf(stmt) };
}
@@ -3303,6 +3303,9 @@ export class Lowerer {
* the class and constructed-class counts, it tells caches over the class
* graph when the graph changed. */
subclassLinks = 0;
/** Function-local classes by the class node they extend, built on first
* use (localSubclassesOf in lower-classes.ts). */
localSubclassIndex: Map<ts.Node, ts.ClassLikeDeclaration[]> | null = null;
/** Set while a subclass constructor's super() call notes its base
* constructor edge: that edge does not instantiate the base exactly. */
private superConstructorEdge = false;
@@ -11377,8 +11380,8 @@ export class Lowerer {
}
}
lowerClassExpression(expr: ts.ClassLikeDeclaration): IrExpr {
return lowerClassExpression(this, expr);
lowerClassExpression(expr: ts.ClassLikeDeclaration, binding?: IrLocal): IrExpr {
return lowerClassExpression(this, expr, binding);
}
/* ── the class graph (single inheritance) ─────────────────────────── */
@@ -3366,6 +3366,17 @@ export function isStdlibSymbol(lowerer: Lowerer, symbol: ts.Symbol | undefined):
* tamper-guard prologue) registered in stdlibGlobalAliases;
* - a const binding of the Map or Set constructor (`const M = Map`,
* builtinClassAliasOf). */
/** The symbol an identifier reads. A shorthand property's name (`{ Date }`)
* reads the binding of the same name, not the property it declares. */
export function identifierReadSymbol(lowerer: Lowerer, expr: ts.Identifier): ts.Symbol | undefined {
if (expr.parent && ts.isShorthandPropertyAssignment(expr.parent) && expr.parent.name === expr)
return (
lowerer.checker.getShorthandAssignmentValueSymbol(expr.parent) ??
lowerer.checker.getSymbolAtLocation(expr)
);
return lowerer.checker.getSymbolAtLocation(expr);
}
export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): string | null {
if (ts.isParenthesizedExpression(expr)) return stdlibGlobalNameOf(lowerer, expr.expression);
// The global-object detection chain (`typeof global < "u" ? global :
@@ -3377,7 +3388,7 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin
return stdlibGlobalNameOf(lowerer, taken ? expr.whenTrue : expr.whenFalse);
}
if (ts.isIdentifier(expr)) {
const symbol = lowerer.checker.getSymbolAtLocation(expr);
const symbol = identifierReadSymbol(lowerer, expr);
// `globalThis` itself: a reserved intrinsic — tsc rejects user
// global bindings, but local parameters can shadow it. Its special
// symbol has no ordinary declarations for the provenance check.
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6913-builtin-constructor-shorthand.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6914-dynamic-builtin-construct.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6915-dynamic-builtin-construct.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6916-local-class-static-self-reference.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6917-local-class-expression-self-reference.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6918-local-class-expression-self-reference.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6919-local-class-inspect-name.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6921-local-subclass-override-dispatch.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6922-local-subclass-override-dispatch.mjs"
],
"diags": []
}
+9
View File
@@ -659,6 +659,9 @@ ScrDyn *scr_dyn_native_set_new(const ScrDyn *seed);
ScrDyn *scr_dyn_native_set(ScrMap *value); /* borrowed backing, +1 box */
bool scr_dyn_native_set_is(const ScrDyn *value);
ScrDyn *scr_dyn_native_map(ScrMap *value); /* borrowed backing, +1 box */
/* `new Map(entries)` over a checked-dynamic seed: +1, or NULL with the
* exception pending. */
ScrDyn *scr_dyn_native_map_new(const ScrDyn *seed);
bool scr_dyn_native_map_is(const ScrDyn *value);
void scr_regex_reset_last_index(ScrRegex *re, double index);
ScrRegex *scr_regex_new_checked(const ScrDyn *pattern, const ScrDyn *flags);
@@ -879,6 +882,12 @@ bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee);
bool scr_dyn_data_view_is(const ScrDyn *value);
ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what);
ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what);
/* Installed by scr_dyn_invoke.c when it interns a builtin constructor value
* (scr_dyn_global_constructor): false when `callee` is not one of them;
* otherwise true with `new callee(...args)` in *result (+1, or NULL with
* the exception pending). */
extern SCR_TL bool (*scr_dyn_global_construct_fn)(const ScrDyn *callee, const ScrDyn *args,
ScrDyn **result);
ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key);
/* The Buffer-flavored twin (stream chunks): string coercion/toString
* decode utf8 instead of joining elements. */
+32
View File
@@ -1854,6 +1854,8 @@ static const char dyn_s29[] SCR_DYN_COMPAT_STR = "Date";
static const char dyn_s30[] SCR_DYN_COMPAT_STR = "Map";
static const char dyn_s31[] SCR_DYN_COMPAT_STR = "Set";
static const char dyn_s32[] SCR_DYN_COMPAT_STR = "";
static const char dyn_s33[] SCR_DYN_COMPAT_STR = "constructing %s%s through a stored value is not supported yet";
static const char dyn_s34[] SCR_DYN_COMPAT_STR = " from a primitive";
static const char *const dyn_global_ctor_names[] = {
dyn_s26, dyn_s29, dyn_s25, dyn_s30, dyn_s31, dyn_s21, dyn_s22, dyn_s24,
@@ -1887,12 +1889,42 @@ static ScrDyn *dyn_global_ctor_call(ScrClosure *closure, ScrDyn *const *args, si
return NULL;
}
/* `new C(...args)` through a stored Object, Date, Map, Set, WeakMap or
* WeakSet builds what the `new` expression builds. RegExp, Promise, and
* Object over a primitive (a wrapper object) are fenced. */
static bool dyn_global_construct(const ScrDyn *callee, const ScrDyn *args, ScrDyn **result) {
if (callee->kind != SCR_DYN_FUNC || callee->v.fn.thunk != dyn_global_ctor_call) return false;
const char *name = callee->v.fn.clo->fn;
ScrDyn *const *items = args->v.arr.items;
size_t argc = args->v.arr.len;
const ScrDyn *first = argc ? items[0] : scr_dyn_undefined();
bool primitive = first->kind == SCR_DYN_NUM || first->kind == SCR_DYN_STR || first->kind == SCR_DYN_BOOL ||
first->kind == SCR_DYN_BIGINT || first->kind == SCR_DYN_SYMBOL;
if (strcmp(name, dyn_s26) == 0 && !primitive) *result = dyn_global_ctor_call(callee->v.fn.clo, items, argc);
else if (strcmp(name, dyn_s29) == 0)
*result = argc == 0 ? scr_dyn_native_date_new(args)
: argc == 1 ? scr_dyn_native_date_new_value(args)
: scr_dyn_native_date_new_fields(args);
else if (strcmp(name, dyn_s30) == 0) *result = scr_dyn_native_map_new(first);
else if (strcmp(name, dyn_s31) == 0) *result = scr_dyn_native_set_new(first);
else if (strcmp(name, dyn_s22) == 0) *result = scr_weak_map_new((ScrDyn *)first);
else if (strcmp(name, dyn_s24) == 0) *result = scr_weak_set_new((ScrDyn *)first);
else {
char message[128];
int length = snprintf(message, sizeof message, dyn_s33, name, strcmp(name, dyn_s26) == 0 ? dyn_s34 : dyn_s32);
scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)length, dyn_s27);
*result = NULL;
}
return true;
}
static ScrDyn *dyn_global_ctor(const char *name) {
for (size_t i = 0; i < DYN_GLOBAL_CTORS; i++) {
if (strcmp(dyn_global_ctor_names[i], name) != 0) continue;
if (!dyn_global_ctors_registered) {
dyn_global_ctors_registered = true;
scr_atexit(dyn_global_ctors_cleanup);
scr_dyn_global_construct_fn = dyn_global_construct;
}
if (!dyn_global_ctors[i]) {
/* Each constructor's own length: Object 1, Date 7, RegExp 2, Promise 1,
+33
View File
@@ -4017,6 +4017,8 @@ bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) {
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC1090");
return false;
}
SCR_TL bool (*scr_dyn_global_construct_fn)(const ScrDyn *callee, const ScrDyn *args, ScrDyn **result) = NULL;
ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what) {
if (callee->kind == SCR_DYN_FUNC && scr_array_ctor && callee->v.fn.clo == scr_array_ctor->v.fn.clo)
return scr_array_ctor_call(callee->v.fn.clo, args->v.arr.items, args->v.arr.len);
@@ -4033,6 +4035,9 @@ ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrS
scr_bytes_release(bytes);
return result;
}
ScrDyn *constructed = NULL;
if (scr_dyn_global_construct_fn && scr_dyn_global_construct_fn(callee, args, &constructed))
return constructed;
ScrJsonBuf message;
scr_jb_init(&message);
scr_jb_write(&message, what->data, what->len);
@@ -4180,6 +4185,13 @@ ScrDyn *scr_dyn_class_prototype(ScrDyn *value, ScrDyn *base) {
if (!cls->prototype_data) {
ScrDyn *data = base->kind == SCR_DYN_UNDEF ? scr_dyn_new_obj() : scr_dyn_obj_create(base);
if (!data) return NULL;
/* util.inspect prints an instance with its class's name. */
const char *name = value->v.fn.name;
if (name && name[0]) {
ScrStr *class_name = scr_str_new(name, strlen(name));
scr_dyn_name_class_prototype(data, class_name);
scr_str_release(class_name);
}
cls->prototype_data = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
scr_box_set_ref(cls->prototype_data, data);
}
@@ -14939,6 +14951,27 @@ ScrDyn *scr_dyn_native_map(ScrMap *value) {
return scr_dyn_new_handle(value, SCR_DYNH_MAP);
}
ScrDyn *scr_dyn_native_map_new(const ScrDyn *seed) {
ScrDyn *entries = scr_dyn_map_seed_entries(seed);
if (!entries) return NULL;
ScrMap *map = scr_map_new(SCR_MAP_KEY_DYN, SCR_MAP_VAL_REF, &scr_map_dyn_map_ops);
for (size_t i = 0; i < entries->v.arr.len; i++) {
ScrDyn *entry = scr_dyn_map_seed_entry(entries->v.arr.items[i]);
if (!entry) break;
ScrDyn *key = scr_dyn_entry_read(entry, 0);
ScrDyn *value = scr_exc_pending() ? NULL : scr_dyn_entry_read(entry, 1);
scr_dyn_release(entry);
if (value) scr_map_dyn_set(map, key, value, false);
scr_dyn_release(key);
scr_dyn_release(value);
if (scr_exc_pending()) break;
}
scr_dyn_release(entries);
ScrDyn *result = scr_exc_pending() ? NULL : scr_dyn_native_map(map);
scr_map_release(map);
return result;
}
bool scr_dyn_native_map_is(const ScrDyn *value) {
return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_MAP;
}
+1 -1
View File
@@ -1527,7 +1527,7 @@ ScrMap *scr_map_clone(const ScrMap *source, bool keys_only, const ScrMapOps *ops
const ScrMapOps scr_map_dyn_set_ops = {
scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v, NULL, NULL, NULL, NULL
};
static const ScrMapOps scr_map_dyn_map_ops = {
const ScrMapOps scr_map_dyn_map_ops = {
scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v,
scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v, NULL
};
+3 -1
View File
@@ -214,8 +214,10 @@ void *scr_map_get_span_ref(const ScrMap *m, const char *bytes, size_t length);
* adapters); NULL keeps the source's descriptor. */
ScrMap *scr_map_clone(const ScrMap *source, bool keys_only, const ScrMapOps *ops);
ScrMap *scr_map_clone_dyn(const ScrMap *source, bool keys_only);
/* The descriptor of a Set of checked values (SCR_MAP_KEY_DYN keys). */
/* The descriptors of a Set and a Map of checked values (SCR_MAP_KEY_DYN
* keys; the Map's values are checked values too). */
extern const ScrMapOps scr_map_dyn_set_ops;
extern const ScrMapOps scr_map_dyn_map_ops;
void scr_map_values_into(ScrMap *set, const ScrMap *source, bool checked);
void scr_map_dyn_attach(ScrMap *map, const ScrMapDynOps *ops);
@@ -0,0 +1,10 @@
// Builtin constructors in shorthand properties are the constructors themselves.
const direct: Record<string, unknown> = { Date: Date, Map: Map, Set: Set };
const shorthand: Record<string, unknown> = { Date, Map, Set, Object, Promise, WeakMap };
console.log(shorthand.Date === Date, shorthand.Map === direct.Map, shorthand.Set === direct.Set);
console.log(shorthand.Object === Object, shorthand.Promise === Promise, shorthand.WeakMap === WeakMap);
console.log(typeof shorthand.Date, typeof shorthand.Map);
function pick(table: Record<string, unknown>, name: string): unknown {
return table[name];
}
console.log(pick({ Map, Set }, "Set") === Set, pick({ Date }, "Date") === pick(direct, "Date"));
@@ -0,0 +1,54 @@
// `new` through values that hold builtin constructors.
const ctors: Record<string, unknown> = { Date, Map, Set, Object, WeakMap, WeakSet };
function construct(name: string, ...args: unknown[]): unknown {
const C = ctors[name] as any;
return new C(...args);
}
function make(C: any, ...args: unknown[]): unknown {
return new C(...args);
}
const tag = (value: unknown) => Object.prototype.toString.call(value);
const d = construct("Date", 0) as Date;
console.log(tag(d), d.getTime(), d.toISOString());
const d2 = make(Date, 2020, 1, 3, 4, 5, 6, 7) as Date;
console.log(d2.getFullYear(), d2.getMonth(), d2.getDate(), d2.getHours(), d2.getMilliseconds());
const d3 = make(Date, "2001-02-03T04:05:06Z") as Date;
const d4 = make(Date, d3) as Date;
console.log(d3.getTime(), d4.getTime());
console.log(typeof make(Date), Number.isNaN((make(Date, "nope") as Date).getTime()));
const m = construct("Map", [["a", 1], ["b", 2]]) as Map<unknown, unknown>;
console.log(tag(m), m.size, m.get("b"), [...m.keys()].join());
const m2 = make(Map, m) as Map<unknown, unknown>;
m2.set("c", 3);
console.log(m.size, m2.size, (make(Map) as Map<unknown, unknown>).size, (make(Map, null) as Map<unknown, unknown>).size);
const s = construct("Set", [1, 2, 2, 3]) as Set<unknown>;
console.log(tag(s), s.size, s.has(3), (make(Set, "abca") as Set<unknown>).size);
const o = construct("Object") as Record<string, unknown>;
o.x = 1;
const same = { y: 2 };
console.log(tag(o), JSON.stringify(o), make(Object, same) === same, tag(make(Object, null)));
const key = {};
const w = construct("WeakMap", [[key, "v"]]) as WeakMap<object, unknown>;
console.log(tag(w), w.get(key), w.has({}));
const ws = make(WeakSet, [key]) as WeakSet<object>;
console.log(tag(ws), ws.has(key));
try {
make(Map, [1]);
} catch (e) {
console.log((e as Error).name, (e as Error).message);
}
try {
make(WeakMap, [[1, 2]]);
} catch (e) {
console.log((e as Error).name, (e as Error).message);
}
const DateSlot: new () => unknown = Date;
const MapSlot: new (entries: [string, number][]) => unknown = Map;
console.log(tag(new DateSlot()), (new MapSlot([["k", 1]]) as Map<unknown, unknown>).get("k"));
@@ -0,0 +1,16 @@
// `new` through stored builtin constructors in JavaScript.
const table = [Date, Map, Set, Object, WeakMap, WeakSet];
for (const C of table) {
const value = new C();
console.log(C.name, typeof value, Object.prototype.toString.call(value), value.constructor === C);
}
const Ctor = table[0];
console.log(new Ctor(86400000).toISOString(), new Ctor(2024, 0, 31).getDate());
const MapCtor = table[1];
const map = new MapCtor(Object.entries({ a: 1, b: 2 }));
map.set("c", 3);
console.log(map.size, map.get("a"), [...map.entries()].length);
const SetCtor = table[2];
console.log(new SetCtor([...map.keys()]).size, new SetCtor([NaN, NaN, 0, -0]).size);
const named = { Date, Map, Set };
console.log(named.Date === Date, named.Map === table[1], named.Set === table[2]);
@@ -0,0 +1,52 @@
// Static fields, static blocks and static methods of function-local classes
// that read the class by its name while the class is being defined.
function registry(prefix) {
class Registry {
static label = Registry.name + ":" + prefix;
static instances = [];
static {
Registry.ready = Registry.instances.length === 0;
}
constructor(id) {
this.id = id;
Registry.instances.push(this);
}
static create(id) {
return new Registry(id);
}
}
return Registry;
}
const R = registry("main");
R.create(1);
R.create(2);
console.log(R.label, R.ready, R.instances.map((r) => r.id));
const R2 = registry("other");
console.log(R2.label, R2.instances.length, R2 === R, R.instances.length);
function counter(step) {
class Counter {
static zero = new Counter(0);
static one = Counter.zero.next();
constructor(value) {
this.value = value;
}
next() {
return new Counter(this.value + step);
}
}
return Counter;
}
const C = counter(5);
console.log(C.zero.value, C.one.value, C.one.next().value, C.one instanceof C);
function described() {
class Shape {
static kinds = ["circle", "square"];
static describe() {
return Shape.kinds.map((kind) => kind + "@" + Shape.name).join(",");
}
}
return Shape.describe();
}
console.log(described());
@@ -0,0 +1,41 @@
// Function-local class expressions that refer to themselves by their own name.
function counter(start: number) {
const C = class Counter {
value: number;
constructor(value: number) { this.value = value; }
next(): Counter { return new Counter(this.value + 1); }
static first(): Counter { return new Counter(start); }
static isSelf(value: unknown): boolean { return value === Counter; }
};
return C;
}
const Counter1 = counter(10);
console.log(Counter1.first().next().next().value);
console.log(Counter1.isSelf(Counter1), Counter1.isSelf(counter(1)), Counter1.name);
function box<T>(seed: T) {
return class Box {
value: T;
constructor(value: T) { this.value = value; }
again(): Box { return new Box(this.value); }
static of(): Box { return new Box(seed); }
};
}
const StringBox = box("seed");
console.log(new StringBox("x").again().value, StringBox.of().value);
const NumberBox = box(7);
console.log(NumberBox.of().again().value + 1);
function nested(n: number) {
const Outer = class Tree {
children: Tree[] = [];
static build(depth: number): Tree {
const tree = new Tree();
if (depth > 0) tree.children.push(Tree.build(depth - 1), Tree.build(depth - 1));
return tree;
}
size(): number { return 1 + this.children.reduce((total, child) => total + child.size(), 0); }
};
return Outer.build(n).size();
}
console.log(nested(3));
@@ -0,0 +1,50 @@
// Function-local class expressions in JavaScript whose static members and
// methods read the class by its own name.
function point(scale) {
const P = class Point {
static origin = new Point(0, 0);
static unit = Point.scaled(1);
constructor(x, y) {
this.x = x;
this.y = y;
}
static scaled(n) {
return new Point(n * scale, n * scale);
}
plus(other) {
return new Point(this.x + other.x, this.y + other.y);
}
};
return P;
}
const P = point(3);
const sum = P.origin.plus(P.unit).plus(P.unit);
console.log(sum.x, sum.y, P.unit instanceof P, P.name);
function selfName() {
const K = class Named {
static who() {
return Named === this;
}
};
return [K.who(), K.name, typeof K];
}
console.log(selfName());
function factory(tag) {
return class Tagged {
static count = 0;
constructor() {
Tagged.count++;
}
static make() {
return new Tagged();
}
get tag() {
return tag + Tagged.count;
}
};
}
const T = factory("t");
T.make();
console.log(T.make().tag, T.count, factory("u").count);
@@ -0,0 +1,51 @@
// console.log prints instances of function-local classes with prototype
// methods under their class names.
function point() {
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
plus(other) {
return new Point(this.x + other.x, this.y + other.y);
}
}
return Point;
}
const Point = point();
const a = new Point(1, 2);
console.log(a, a.plus(a));
console.log([new Point(0, 0)], { p: new Point(5, 6) });
function animals() {
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return this.name + " makes a sound";
}
}
class Dog extends Animal {
speak() {
return this.name + " barks";
}
}
return [new Animal("cat"), new Dog("rex")];
}
const [cat, dog] = animals();
console.log(cat, dog);
function anonymous() {
const Named = class {
value() {
return 1;
}
};
return [new Named(), new (class {
value() {
return 2;
}
})()];
}
console.log(anonymous());
@@ -0,0 +1,51 @@
// Overrides declared by function-local subclasses dispatch on the receiver
// when the call site lowers before the function that declares them.
class Shape {
area(): number { return 0; }
get label(): string { return "shape"; }
describe(): string { return this.label + ":" + this.area(); }
}
function square(side: number): Shape {
class Square extends Shape {
area(): number { return side * side; }
get label(): string { return "square"; }
}
return new Square();
}
function scaled(factor: number): Shape {
class Scaled extends Shape {
area(): number { return factor; }
}
class Doubled extends Scaled {
area(): number { return super.area() * 2; }
}
return new Doubled();
}
const shapes: Shape[] = [new Shape(), square(3), scaled(5)];
for (const shape of shapes) console.log(shape.area(), shape.label, shape.describe());
function pair() {
class Base {
name(): string { return "base"; }
}
class Derived extends Base {
name(): string { return "derived"; }
}
return [new Base(), new Derived()];
}
const [first, second] = pair();
console.log(first.name(), second.name());
function counter() {
const Tally = class {
count(): number { return 1; }
};
const Double = class extends Tally {
count(): number { return 2; }
};
return new Double();
}
console.log(counter().count());
@@ -0,0 +1,51 @@
// Overrides declared by function-local subclasses in JavaScript dispatch on
// the receiver when the call site lowers before the declaring function.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return this.name + " makes a sound";
}
}
function dog(name) {
class Dog extends Animal {
speak() {
return this.name + " barks";
}
}
return new Dog(name);
}
function animals() {
class Bird extends Animal {
speak() {
return this.name + " sings";
}
}
return [new Animal("cat"), new Bird("robin"), dog("rex")];
}
for (const animal of animals()) console.log(animal.speak());
const speak = (animal) => animal.speak();
console.log(speak(dog("fido")));
function pets() {
class Pet {
constructor(name) {
this.name = name;
}
greet() {
return this.name + " waves";
}
}
class Cat extends Pet {
greet() {
return this.name + " purrs";
}
}
return [new Pet("ann"), new Cat("tom")];
}
const [pet, cat] = pets();
console.log(pet.greet(), cat.greet());
@@ -1,10 +1,8 @@
function privateBrand() { return class { #value = 1; has(other: object): boolean { return #value in other; } }; }
function selfName() { return class Local { copy() { return new Local(); } }; }
class Base {}
function derived() { return class extends Base {}; }
function generic() { return class { read<T>(value: T): T { return value; } }; }
privateBrand();
selfName();
derived();
generic();
// End of unsupported local-class forms.
@@ -2,18 +2,11 @@ local-class-boundaries.ts:1:34 - error SC1090: function-local classes with priva
1 | function privateBrand() { return class { #value = 1; has(other: object): boolean { return #value in other; } }; }
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 | function selfName() { return class Local { copy() { return new Local(); } }; }
2 | class Base {}
local-class-boundaries.ts:2:64 - error SC1090: named self references inside function-local classes are not supported yet
local-class-boundaries.ts:4:29 - error SC1090: function-local classes with statics, decorators, or generic members are not supported yet
1 | function privateBrand() { return class { #value = 1; has(other: object): boolean { return #value in other; } }; }
2 | function selfName() { return class Local { copy() { return new Local(); } }; }
| ^~~~~
3 | class Base {}
local-class-boundaries.ts:5:29 - error SC1090: function-local classes with statics, decorators, or generic members are not supported yet
4 | function derived() { return class extends Base {}; }
5 | function generic() { return class { read<T>(value: T): T { return value; } }; }
3 | function derived() { return class extends Base {}; }
4 | function generic() { return class { read<T>(value: T): T { return value; } }; }
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
6 | privateBrand();
5 | privateBrand();
@@ -0,0 +1 @@
{ "plain": 3202, "san": 4630 }
@@ -0,0 +1 @@
{ "plain": 3064, "san": 5586 }
@@ -0,0 +1 @@
{ "plain": 2821, "san": 4440 }
@@ -0,0 +1 @@
{ "plain": 3135, "san": 4365 }
@@ -0,0 +1 @@
{ "plain": 3074, "san": 4624 }
@@ -0,0 +1 @@
{ "plain": 3050, "san": 4393 }
@@ -0,0 +1 @@
{ "plain": 3058, "san": 4846 }
@@ -0,0 +1 @@
{ "plain": 2459, "san": 4630 }
@@ -0,0 +1 @@
{ "plain": 3216, "san": 5042 }