Compile local class factories and spread calls natively

- Preserve fresh constructor identity and shared captures across local class instances.
- Carry constructors through unknown values without a JavaScript engine.
- Support runtime-length spreads into fixed and rest parameters.
This commit is contained in:
Chris Tate
2026-09-28 20:11:25 -05:00
committed by GitHub
parent 39db8c0a99
commit 31332427b6
53 changed files with 928 additions and 149 deletions
+1 -1
View File
@@ -59,7 +59,7 @@ fib.s
Different output kinds accumulate in `.scriptc/`; rebuilding a kind updates its file. Different output kinds accumulate in `.scriptc/`; rebuilding a kind updates its file.
The three source outputs require only Node. On macOS 15+ arm64, assembly and object outputs use the matching native helper installed with scriptc and do not invoke an external compiler, archiver, linker, or SDK. The object is a relocatable program object with undefined <code>scr_*</code> runtime symbols and a required <code>scr_runtime_abi_v3</code> marker, not a standalone library. External consumption is experimental and requires the exact runtime version reported by <code>--print=native-link-info</code>. That option still writes the object, performs no link, and prints a versioned JSON recipe with the target, <code>main</code> entry, installed source runtime pack, FFI inputs, and system libraries. It never reports private scriptc cache paths. See <a href="/native-objects">Native Program Objects</a> for complete C-driver and direct-linker examples. <code>--emit=exe</code> is the default. On macOS 15+ arm64, LLVM-tier builds emit the program object through the helper and link release-built runtime objects; explicit C, LLVM fallback, and sanitizer builds retain runtime C compilation. The three source outputs require only Node. On macOS 15+ arm64, assembly and object outputs use the matching native helper installed with scriptc and do not invoke an external compiler, archiver, linker, or SDK. The object is a relocatable program object with undefined <code>scr_*</code> runtime symbols and a required <code>scr_runtime_abi_v4</code> marker, not a standalone library. External consumption is experimental and requires the exact runtime version reported by <code>--print=native-link-info</code>. That option still writes the object, performs no link, and prints a versioned JSON recipe with the target, <code>main</code> entry, installed source runtime pack, FFI inputs, and system libraries. It never reports private scriptc cache paths. See <a href="/native-objects">Native Program Objects</a> for complete C-driver and direct-linker examples. <code>--emit=exe</code> is the default. On macOS 15+ arm64, LLVM-tier builds emit the program object through the helper and link release-built runtime objects; explicit C, LLVM fallback, and sanitizer builds retain runtime C compilation.
## scriptc run ## scriptc run
+1 -1
View File
@@ -14,7 +14,7 @@ TypeScript ──tsc: parse + typecheck──▶ lowering ──▶ typed IR ─
3. **Backends** — `--emit=c` writes readable C and stops; `--emit=llvm` writes textual LLVM IR and stops. Neither source-output command discovers or invokes a native toolchain. On supported macOS, Linux, and Windows hosts (and for WASI), `--emit=asm|obj` sends LLVM IR to a version-matched out-of-process LLVM 22 helper; it needs no clang or linker. Executable builds default to LLVM and can fall back to C on a native program outside the LLVM tier (one stderr note; `--backend llvm` pins it and fails with a diagnostic instead). The production <code>wasm32-wasi</code> target never falls back. 3. **Backends** — `--emit=c` writes readable C and stops; `--emit=llvm` writes textual LLVM IR and stops. Neither source-output command discovers or invokes a native toolchain. On supported macOS, Linux, and Windows hosts (and for WASI), `--emit=asm|obj` sends LLVM IR to a version-matched out-of-process LLVM 22 helper; it needs no clang or linker. Executable builds default to LLVM and can fall back to C on a native program outside the LLVM tier (one stderr note; `--backend llvm` pins it and fails with a diagnostic instead). The production <code>wasm32-wasi</code> target never falls back.
4. **Link** — release packages contain one precompiled object per runtime feature unit plus QuickJS, libregexp, zlib, and mbedTLS archives for every supported target. A hashed manifest maps IR feature gates to an ordered typed link plan, so binaries still pay only for what they use. The user needs the target platform linker and SDK/sysroot, but ordinary LLVM-tier builds compile no C. AddressSanitizer, explicit C builds, and LLVM refusals keep the external C-toolchain path. 4. **Link** — release packages contain one precompiled object per runtime feature unit plus QuickJS, libregexp, zlib, and mbedTLS archives for every supported target. A hashed manifest maps IR feature gates to an ordered typed link plan, so binaries still pay only for what they use. The user needs the target platform linker and SDK/sysroot, but ordinary LLVM-tier builds compile no C. AddressSanitizer, explicit C builds, and LLVM refusals keep the external C-toolchain path.
Program objects define <code>main</code> and leave their selected <code>scr_*</code> runtime functions undefined. The <code>scr_runtime_abi_v3</code> reference is a strong link-time compatibility check. <code>--print=native-link-info</code> exposes the exact source runtime pack and link ordering for external builds; that object ABI is currently experimental and exact-runtime-version compatible, not semver-stable. Program objects define <code>main</code> and leave their selected <code>scr_*</code> runtime functions undefined. The <code>scr_runtime_abi_v4</code> reference is a strong link-time compatibility check. <code>--print=native-link-info</code> exposes the exact source runtime pack and link ordering for external builds; that object ABI is currently experimental and exact-runtime-version compatible, not semver-stable.
Inspect any stage yourself: Inspect any stage yourself:
+2
View File
@@ -37,6 +37,8 @@ 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. - `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. - 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.
- Class expressions inside functions preserve fresh constructor identity and shared captured variables across constructors, methods, accessors, and arrow fields. Local inheritance, static members, decorators, private brands, generic members, explicitly generic factories returning local constructors, and references through the class's internal name remain unsupported. Constructors can pass through `unknown` while preserving identity, `typeof`, `name`, and `length`, and an exact typed cast restores construction; construction or static-member access directly through `unknown` remains unsupported.
- Runtime-length argument spreads compile for fixed parameters and rest parameters whose types support checked `unknown` conversion. Arguments evaluate in source order before defaults run, and surplus arguments still evaluate. Calls without any native parameter slot and parameter types outside that conversion domain remain unsupported.
- 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. - 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** **Standard library**
+1 -1
View File
@@ -13,7 +13,7 @@ surface may change before 1.0, so consumers must use the exact
`@scriptc/runtime` version reported by the same compiler installation. This `@scriptc/runtime` version reported by the same compiler installation. This
is stricter than semver compatibility. is stricter than semver compatibility.
The object intentionally leaves its selected runtime symbols undefined. It also holds a strong reference to `scr_runtime_abi_v3`, which the matching runtime defines. Linking an object against a runtime with another ABI marker fails at link time with the missing versioned symbol; it cannot become a latent runtime incompatibility. The object intentionally leaves its selected runtime symbols undefined. It also holds a strong reference to `scr_runtime_abi_v4`, which the matching runtime defines. Linking an object against a runtime with another ABI marker fails at link time with the missing versioned symbol; it cannot become a latent runtime incompatibility.
## Machine-readable link information ## Machine-readable link information
+1 -1
View File
@@ -32,7 +32,7 @@ Builds use a bounded persistent cache by default. Exact unchanged library builds
- `scriptc run <file.ts>` — compile and run - `scriptc run <file.ts>` — compile and run
- `scriptc coverage <file.ts>` — what compiles statically, and why the rest doesn't - `scriptc coverage <file.ts>` — what compiles statically, and why the rest doesn't
`scriptc build app.ts --emit=ir|c|llvm|asm|obj` selects serialized typed IR, readable C, textual LLVM IR, target assembly, or a relocatable program object as the one primary artifact. `--emit=exe` is the default. Assembly/object emission uses the matching helper on supported macOS, Linux, and Windows hosts (and produces WASI artifacts when that target is selected). Objects retain undefined `scr_*` runtime references plus the `scr_runtime_abi_v3` compatibility marker; they are not library archives. External consumption is experimental and requires the exact matching runtime. `scriptc build app.ts --print=native-link-info -o app.o` prints the versioned JSON target/runtime/link recipe without performing a link. `--emit=asm|obj --sanitize` is rejected until ASan pipeline parity is available. `scriptc build app.ts --emit=ir|c|llvm|asm|obj` selects serialized typed IR, readable C, textual LLVM IR, target assembly, or a relocatable program object as the one primary artifact. `--emit=exe` is the default. Assembly/object emission uses the matching helper on supported macOS, Linux, and Windows hosts (and produces WASI artifacts when that target is selected). Objects retain undefined `scr_*` runtime references plus the `scr_runtime_abi_v4` compatibility marker; they are not library archives. External consumption is experimental and requires the exact matching runtime. `scriptc build app.ts --print=native-link-info -o app.o` prints the versioned JSON target/runtime/link recipe without performing a link. `--emit=asm|obj --sanitize` is rejected until ASan pipeline parity is available.
For embedder-hosted modules that are not installed npm packages, coverage can For embedder-hosted modules that are not installed npm packages, coverage can
map an exact bare specifier to a local declaration with repeatable map an exact bare specifier to a local declaration with repeatable
+2 -2
View File
@@ -63,7 +63,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
minimum_os: "14.0", minimum_os: "14.0",
}, },
program: { object, entry_symbol: "main" }, program: { object, entry_symbol: "main" },
runtime_abi: { version: 3, marker: "scr_runtime_abi_v3" }, runtime_abi: { version: 4, marker: "scr_runtime_abi_v4" },
runtime_pack: { runtime_pack: {
kind: "source", kind: "source",
package: "@scriptc/runtime", package: "@scriptc/runtime",
@@ -147,6 +147,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
const error = await execFileAsync("clang", [ const error = await execFileAsync("clang", [
"-target", "arm64-apple-macosx14.0.0", object, stub, "-o", join(dir, "bad"), "-target", "arm64-apple-macosx14.0.0", object, stub, "-o", join(dir, "bad"),
]).then(() => null, (failure: { stderr?: string }) => failure); ]).then(() => null, (failure: { stderr?: string }) => failure);
expect(error?.stderr).toContain("scr_runtime_abi_v3"); expect(error?.stderr).toContain("scr_runtime_abi_v4");
}); });
}); });
+12 -2
View File
@@ -2659,6 +2659,13 @@ function emitCallExpr(
// address. The +1 retain is a no-op on immortals but keeps the // address. The +1 retain is a no-op on immortals but keeps the
// owned-temps discipline uniform (the regexLit pattern). // owned-temps discipline uniform (the regexLit pattern).
const sym = emitter.classObjSym(e.className); const sym = emitter.classObjSym(e.className);
if (e.captures !== undefined) {
const value = emitter.newTemp(e.type, `scr_classobj_new(&${sym}, ${e.captures.length})`);
e.captures.forEach((id, slot) => {
emitter.line(`${value.name}->caps[${slot}] = scr_box_retain(${mangleLocal(id)});`);
});
return value;
}
return emitter.newTemp(e.type, `scr_classobj_retain(&${sym})`); return emitter.newTemp(e.type, `scr_classobj_retain(&${sym})`);
} }
case "newValue": { case "newValue": {
@@ -2678,8 +2685,8 @@ function emitCallExpr(
const args = e.args.map((a) => emitter.emitExpr(a)); const args = e.args.map((a) => emitter.emitExpr(a));
for (const a of args) emitter.moveTemp(a); // the constructor owns its params for (const a of args) emitter.moveTemp(a); // the constructor owns its params
const paramTypes = ctor.params.slice(1).map((p) => cType(p.type).trim()); const paramTypes = ctor.params.slice(1).map((p) => cType(p.type).trim());
const cast = `(void *(*)(${paramTypes.join(", ") || "void"}))`; const cast = `(void *(*)(ScrClassObj *${paramTypes.length ? ", " + paramTypes.join(", ") : ""}))`;
const call = `(${cast}${callee.name}->ctor)(${args.map((a) => a.name).join(", ")})`; const call = `(${cast}${callee.name}->ctor)(${[callee.name, ...args.map((a) => a.name)].join(", ")})`;
const t = emitter.newTemp(e.type, `(${cType(e.type).trim()})${call}`); const t = emitter.newTemp(e.type, `(${cType(e.type).trim()})${call}`);
if (newValueMayThrow(cls, emitter.classMeta.get(cls), emitter.mayThrow)) emitter.emitPendingCheck(); if (newValueMayThrow(cls, emitter.classMeta.get(cls), emitter.mayThrow)) emitter.emitPendingCheck();
return t; return t;
@@ -2690,6 +2697,9 @@ function emitCallExpr(
// sides are hierarchy members, so the operand has a vt word. // sides are hierarchy members, so the operand has a vt word.
const v = emitter.emitExpr(e.value); const v = emitter.emitExpr(e.value);
const target = emitter.emitExpr(e.classValue); const target = emitter.emitExpr(e.classValue);
if (e.value.type.kind === "object" && emitter.classMeta.get(e.value.type.className)?.def.localCaptures !== undefined) {
return emitter.newTemp(e.type, `${v.name}->sc_class == ${target.name}`);
}
return emitter.newTemp( return emitter.newTemp(
e.type, e.type,
`${v.name}->vt->pre >= ${target.name}->pre && ${v.name}->vt->pre <= ${target.name}->post`, `${v.name}->vt->pre >= ${target.name}->pre && ${v.name}->vt->pre <= ${target.name}->post`,
+13 -4
View File
@@ -127,6 +127,9 @@ export interface StructShape {
// the record's trace must visit it). // the record's trace must visit it).
const rcMembers = (s: StructShape): { member: string; type: IrType; name: string }[] => [ const rcMembers = (s: StructShape): { member: string; type: IrType; name: string }[] => [
...s.fields.map((f) => ({ member: mangleField(f.name), type: f.type, name: f.name })), ...s.fields.map((f) => ({ member: mangleField(f.name), type: f.type, name: f.name })),
...(s.meta?.def.localCaptures !== undefined
? [{ member: "sc_class", type: { kind: "classval" as const, className: s.meta.def.name }, name: "class environment" }]
: []),
...(s.indexValue ...(s.indexValue
? [{ member: OVERFLOW_MEMBER, type: mapOf(STRING, s.indexValue), name: "[key: string] overflow" }] ? [{ member: OVERFLOW_MEMBER, type: mapOf(STRING, s.indexValue), name: "[key: string] overflow" }]
: []), : []),
@@ -163,6 +166,7 @@ export interface StructShape {
out.push(""); out.push("");
for (const s of shapes) { for (const s of shapes) {
out.push(`struct ${s.struct} { /* ${cCommentText(s.comment)} */`, ` size_t rc;`); out.push(`struct ${s.struct} { /* ${cCommentText(s.comment)} */`, ` size_t rc;`);
if (s.meta?.def.localCaptures !== undefined) out.push(` ScrClassObj *sc_class;`);
if (inHierarchy(s)) { if (inHierarchy(s)) {
// The hierarchy prefix: base fields follow at identical offsets in // The hierarchy prefix: base fields follow at identical offsets in
// every subclass, so vt must sit between rc and the field list. // every subclass, so vt must sit between rc and the field list.
@@ -607,9 +611,9 @@ function emitRecordCloneC(
if (!intervalMeta) throw new InternalCompilerError(`emitter bug: class object for ${className} names unknown family ${meta.def.genericOf ?? ""}`); if (!intervalMeta) throw new InternalCompilerError(`emitter bug: class object for ${className} names unknown family ${meta.def.genericOf ?? ""}`);
const nameSym = emitter.internLiteral(meta.def.jsName ?? ""); const nameSym = emitter.internLiteral(meta.def.jsName ?? "");
out.push( out.push(
`static void *${mangleCtorThunk(className)}(${ctorThunkParams(emitter, className).decls || "void"});`, `static void *${mangleCtorThunk(className)}(ScrClassObj *sc_class${ctorThunkParams(emitter, className).decls ? ", " + ctorThunkParams(emitter, className).decls : ""});`,
`static ScrClassObj ${sym} = { SIZE_MAX, ${intervalMeta.pre}, ${intervalMeta.post}, ` + `static ScrClassObj ${sym} = { SIZE_MAX, ${intervalMeta.pre}, ${intervalMeta.post}, ` +
`(void *)&${mangleCtorThunk(className)}, (const ScrStr *)&${nameSym} }; /* class ${className} */`, `(void *)&${mangleCtorThunk(className)}, (const ScrStr *)&${nameSym}, 0, ${meta.def.jsLength ?? 0} }; /* class ${className} */`,
); );
} }
} }
@@ -640,8 +644,11 @@ function emitRecordCloneC(
const struct = mangleClassStruct(className); const struct = mangleClassStruct(className);
const lines = [ const lines = [
``, ``,
`static void *${mangleCtorThunk(className)}(${decls || "void"}) {`, `static void *${mangleCtorThunk(className)}(ScrClassObj *sc_class${decls ? ", " + decls : ""}) {`,
` ${struct} *o = ${mangleClassNew(className)}();`, ` ${struct} *o = ${mangleClassNew(className)}();`,
...(emitter.classMeta.get(className)?.def.localCaptures !== undefined
? [` o->sc_class = scr_classobj_retain(sc_class);`]
: [` (void)sc_class;`]),
` ${mangleFunction(`%${className}.constructor`)}(${[`${mangleClassRetain(className)}(o)`, ...names].join(", ")});`, ` ${mangleFunction(`%${className}.constructor`)}(${[`${mangleClassRetain(className)}(o)`, ...names].join(", ")});`,
]; ];
if (emitter.mayThrow.has(`%${className}.constructor`)) { if (emitter.mayThrow.has(`%${className}.constructor`)) {
@@ -710,6 +717,8 @@ function emitRecordCloneC(
* the type cannot participate in a cycle (see the constructor fixpoint). */ * the type cannot participate in a cycle (see the constructor fixpoint). */
export function traceAdapterC(emitter: CEmitter, t: IrType): string | null { export function traceAdapterC(emitter: CEmitter, t: IrType): string | null {
switch (t.kind) { switch (t.kind) {
case "classval":
return "scr_classobj_trace_v";
case "func": case "func":
return "scr_closure_trace_v"; return "scr_closure_trace_v";
case "union": case "union":
@@ -772,7 +781,7 @@ function emitRecordCloneC(
elem.kind === "netServer" || // server handles: scr_net_server_* adapters, no trace elem.kind === "netServer" || // server handles: scr_net_server_* adapters, no trace
elem.kind === "symbol" || // symbol identities: scr_sym_* adapters, no trace elem.kind === "symbol" || // symbol identities: scr_sym_* adapters, no trace
elem.kind === "bigint" || // immutable numeric values: scr_bigint_* adapters elem.kind === "bigint" || // immutable numeric values: scr_bigint_* adapters
elem.kind === "classval" || // class objects: no-op adapters, no trace (immortal statics) elem.kind === "classval" || // local class objects own traced capture boxes
// Closures: scr_closure_* adapters + scr_closure_trace_v (always // Closures: scr_closure_* adapters + scr_closure_trace_v (always
// cycle-headered — captures can reach back through boxes). // cycle-headered — captures can reach back through boxes).
elem.kind === "func" || elem.kind === "func" ||
+7 -1
View File
@@ -33,7 +33,7 @@ export function emitFunction(emitter: CEmitter, fn: IrFunction): void {
emitter.currentGenerator = fn.generator ?? null; emitter.currentGenerator = fn.generator ?? null;
emitter.labelCounter = 0; emitter.labelCounter = 0;
emitter.currentLocals = new Map(fn.locals.map((l) => [l.id, l])); emitter.currentLocals = new Map(fn.locals.map((l) => [l.id, l]));
emitter.captureIds = new Set((fn.captures ?? []).map((c) => c.localId)); emitter.captureIds = new Set([...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId));
emitter.integerLoopBindings.clear(); emitter.integerLoopBindings.clear();
emitter.integerRanges = analyzeIntegerRanges(fn); emitter.integerRanges = analyzeIntegerRanges(fn);
@@ -55,6 +55,12 @@ export function emitFunction(emitter: CEmitter, fn: IrFunction): void {
(fn.captures ?? []).forEach((c, i) => { (fn.captures ?? []).forEach((c, i) => {
emitter.line(`ScrBox *${mangleLocal(c.localId)} = sc_env->caps[${i}]; /* captured ${c.name} */`); emitter.line(`ScrBox *${mangleLocal(c.localId)} = sc_env->caps[${i}]; /* captured ${c.name} */`);
}); });
(fn.classCaptures ?? []).forEach((c) => {
const self = fn.params[0]!;
const local = emitter.currentLocals.get(self.localId)!;
const receiver = local.boxed ? mangleRawParam(self.localId) : mangleLocal(self.localId);
emitter.line(`ScrBox *${mangleLocal(c.localId)} = ${receiver}->sc_class->caps[${c.slot}]; /* captured ${c.name} */`);
});
const paramIds = new Set(fn.params.map((p) => p.localId)); const paramIds = new Set(fn.params.map((p) => p.localId));
for (const local of fn.locals) { for (const local of fn.locals) {
@@ -807,6 +807,9 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
case "func": case "func":
d.push(` return d->kind == SCR_DYN_FUNC;`); d.push(` return d->kind == SCR_DYN_FUNC;`);
break; break;
case "classval":
d.push(` return scr_dyn_class_is(d, ${cStringLiteral(Buffer.from(key, "utf8"))});`);
break;
case "object": case "object":
if (t.className === "%Error") { if (t.className === "%Error") {
d.push(` return d->kind == SCR_DYN_OBJ && scr_dyn_obj_get(d, "%error", 6) != NULL;`); d.push(` return d->kind == SCR_DYN_OBJ && scr_dyn_obj_get(d, "%error", 6) != NULL;`);
@@ -1224,6 +1227,9 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
d.push(` if (d->kind != SCR_DYN_NUM) { scr_dyn_check_fail(path, ${want}, d); return 0; }`); d.push(` if (d->kind != SCR_DYN_NUM) { scr_dyn_check_fail(path, ${want}, d); return 0; }`);
d.push(` return d->v.num;`); d.push(` return d->v.num;`);
break; break;
case "classval":
d.push(` return scr_dyn_class_check(d, ${cStringLiteral(Buffer.from(key, "utf8"))}, path);`);
break;
case "bool": case "bool":
d.push(` if (d->kind != SCR_DYN_BOOL) { scr_dyn_check_fail(path, ${want}, d); return false; }`); d.push(` if (d->kind != SCR_DYN_BOOL) { scr_dyn_check_fail(path, ${want}, d); return false; }`);
d.push(` return d->v.b;`); d.push(` return d->v.b;`);
@@ -1548,6 +1554,9 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
// immutable-through-copies. // immutable-through-copies.
d.push(` return scr_dyn_retain(v);`); d.push(` return scr_dyn_retain(v);`);
break; break;
case "classval":
d.push(` return scr_dyn_new_class(v, ${cStringLiteral(Buffer.from(key, "utf8"))});`);
break;
case "func": case "func":
// Function-valued record/array fields (EventListenerObject's // Function-valued record/array fields (EventListenerObject's
// handleEvent method) box through the same identity-preserving // handleEvent method) box through the same identity-preserving
@@ -1654,6 +1663,7 @@ export function jsonWriteHelper(emitter: CEmitter, t: IrType): string {
if (cyclicArr) d.push(` scr_dyn_from_enter(v);`); if (cyclicArr) d.push(` scr_dyn_from_enter(v);`);
d.push(` ScrDyn *d = scr_dyn_new_arr();`); d.push(` ScrDyn *d = scr_dyn_new_arr();`);
d.push(` for (size_t i = 0; i < v->len; i++) {`); d.push(` for (size_t i = 0; i < v->len; i++) {`);
d.push(` if (scr_arr_state(v, (double)i) == SCR_ARR_UNDEFINED) { scr_dyn_arr_push(d, scr_dyn_retain(scr_dyn_undefined())); continue; }`);
if (elem.kind === "f64") { if (elem.kind === "f64") {
d.push(` scr_dyn_arr_push(d, scr_dyn_new_num(scr_arr_get_f64(v, (double)i)));`); d.push(` scr_dyn_arr_push(d, scr_dyn_new_num(scr_arr_get_f64(v, (double)i)));`);
} else if (elem.kind === "bool") { } else if (elem.kind === "bool") {
@@ -18,9 +18,11 @@ export function computeTraced(mod: IrModule): { shapes: Set<string>; unions: Set
const shapeDefs = [ const shapeDefs = [
...classes.map((c) => ({ ...classes.map((c) => ({
key: `object:${c.name}`, key: `object:${c.name}`,
fields: c.name === RUNTIME_EMITTER_CLASS fields: [
? [...c.fields, { name: "<listeners>", type: funcOf([], VOID) }] ...c.fields,
: c.fields, ...(c.name === RUNTIME_EMITTER_CLASS ? [{ name: "<listeners>", type: funcOf([], VOID) }] : []),
...(c.localCaptures !== undefined ? [{ name: "<class>", type: { kind: "classval" as const, className: c.name } }] : []),
],
})), })),
...(mod.records ?? []).map((r) => ({ ...(mod.records ?? []).map((r) => ({
key: `record:${r.id}`, key: `record:${r.id}`,
@@ -59,6 +61,7 @@ export function computeTraced(mod: IrModule): { shapes: Set<string>; unions: Set
const cycleCapable = (t: IrType): boolean => { const cycleCapable = (t: IrType): boolean => {
switch (t.kind) { switch (t.kind) {
case "func": case "func":
case "classval":
case "promise": case "promise":
return true; return true;
case "object": case "object":
+18 -6
View File
@@ -174,6 +174,7 @@ function emitterRooted(meta: LlClassMeta): boolean {
* display name) on emitter-rooted classes, then the stream-state slot on * display name) on emitter-rooted classes, then the stream-state slot on
* stream-rooted ones. */ * stream-rooted ones. */
function fieldBase(meta: LlClassMeta): number { function fieldBase(meta: LlClassMeta): number {
if (meta.def.localCaptures !== undefined) return 2;
if (!meta.hierarchy) return 1; if (!meta.hierarchy) return 1;
if (streamRooted(meta)) return 5; if (streamRooted(meta)) return 5;
return emitterRooted(meta) ? 4 : 2; return emitterRooted(meta) ? 4 : 2;
@@ -259,7 +260,7 @@ export function emitClassShapes(
: emitterRooted(meta) ? ["ptr", "ptr", "ptr"] : emitterRooted(meta) ? ["ptr", "ptr", "ptr"]
: ["ptr"] : ["ptr"]
: []; : [];
const members = [...prefix, ...fieldTys]; const members = [...prefix, ...(cls.localCaptures !== undefined ? ["ptr"] : []), ...fieldTys];
typeDefs.push( typeDefs.push(
`%${mangleClassStruct(cls.name)} = type { ${host.sizeType}${members.length ? ", " + members.join(", ") : ""} } ` + `%${mangleClassStruct(cls.name)} = type { ${host.sizeType}${members.length ? ", " + members.join(", ") : ""} } ` +
`; class ${cls.name}${meta.hierarchy ? " (vt at 1)" : ""}${streamRooted(meta) ? " (ScrStream prefix at 2)" : emitterRooted(meta) ? " (ScrEmitter prefix at 2)" : ""} { ${cls.fields.map((f) => llvmCommentText(f.name)).join("; ")} }`, `; class ${cls.name}${meta.hierarchy ? " (vt at 1)" : ""}${streamRooted(meta) ? " (ScrStream prefix at 2)" : emitterRooted(meta) ? " (ScrEmitter prefix at 2)" : ""} { ${cls.fields.map((f) => llvmCommentText(f.name)).join("; ")} }`,
@@ -310,8 +311,13 @@ export function emitClassShapes(
const isEmitterRooted = emitterRooted(meta); const isEmitterRooted = emitterRooted(meta);
const isStreamRooted = streamRooted(meta); const isStreamRooted = streamRooted(meta);
const fieldIndex = (i: number): number => fieldBase(meta) + i; const fieldIndex = (i: number): number => fieldBase(meta) + i;
const refFields = cls.fields const indexedFields = [
.map((f, i) => ({ name: f.name, type: f.type, index: fieldIndex(i) })) ...cls.fields.map((f, i) => ({ name: f.name, type: f.type, index: fieldIndex(i) })),
...(cls.localCaptures !== undefined
? [{ name: "class environment", type: { kind: "classval" as const, className: cls.name }, index: 1 }]
: []),
];
const refFields = indexedFields
.filter((f) => isRefCounted(f.type)); .filter((f) => isRefCounted(f.type));
const sizeOf = `ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 1) to ${host.sizeType})`; const sizeOf = `ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 1) to ${host.sizeType})`;
// An embedded prefix slot (the emitter registry at 2, the stream // An embedded prefix slot (the emitter registry at 2, the stream
@@ -472,7 +478,7 @@ export function emitClassShapes(
// trace: visit exactly the cycle-capable fields; gcFree: release // trace: visit exactly the cycle-capable fields; gcFree: release
// exactly the complement, then free (the trace/teardown complement // exactly the complement, then free (the trace/teardown complement
// contract in scr_runtime.h). // contract in scr_runtime.h).
const indexed = cls.fields.map((f, i) => ({ name: f.name, type: f.type, index: fieldIndex(i) })); const indexed = indexedFields;
const tracedFields = indexed.filter((f) => traceAdapter(host, f.type) !== null); const tracedFields = indexed.filter((f) => traceAdapter(host, f.type) !== null);
const untracedRefFields = indexed.filter( const untracedRefFields = indexed.filter(
(f) => isRefCounted(f.type) && traceAdapter(host, f.type) === null, (f) => isRefCounted(f.type) && traceAdapter(host, f.type) === null,
@@ -543,16 +549,22 @@ export function emitClassObjDefs(
const params = ctor.params.slice(1); const params = ctor.params.slice(1);
const paramDecls = params.map((p, i) => `${llType(p.type)} %a${i}`).join(", "); const paramDecls = params.map((p, i) => `${llType(p.type)} %a${i}`).join(", ");
const ctorArgs = params.map((p, i) => `${llType(p.type)} %a${i}`); const ctorArgs = params.map((p, i) => `${llType(p.type)} %a${i}`);
if (meta.def.localCaptures !== undefined) host.declare(`declare ptr @scr_classobj_retain_v(ptr)`);
out.push( out.push(
`define internal ptr @${mangleCtorThunk(className)}(${paramDecls}) ${FN_ATTRS} { ; construct thunk ${className}`, `define internal ptr @${mangleCtorThunk(className)}(ptr %class${paramDecls ? ", " + paramDecls : ""}) ${FN_ATTRS} { ; construct thunk ${className}`,
`entry:`, `entry:`,
` %o = call ptr @${mangleClassNew(className)}()`, ` %o = call ptr @${mangleClassNew(className)}()`,
...(meta.def.localCaptures !== undefined ? [
` %class.owned = call ptr @scr_classobj_retain_v(ptr %class)`,
` %class.slot = getelementptr inbounds %${mangleClassStruct(className)}, ptr %o, i64 0, i32 1`,
` store ptr %class.owned, ptr %class.slot`,
] : []),
` %r = call ptr @${mangleClassRetain(className)}(ptr %o)`, ` %r = call ptr @${mangleClassRetain(className)}(ptr %o)`,
` call void @${mangleFunction(`%${className}.constructor`)}(${[`ptr %r`, ...ctorArgs].join(", ")})`, ` call void @${mangleFunction(`%${className}.constructor`)}(${[`ptr %r`, ...ctorArgs].join(", ")})`,
` ret ptr %o`, ` ret ptr %o`,
`}`, `}`,
`@${mangleClassObj(className)} = internal global %ScrClassObj ` + `@${mangleClassObj(className)} = internal global %ScrClassObj ` +
`{ ${host.sizeType} -1, ${host.sizeType} ${intervalMeta.pre}, ${host.sizeType} ${intervalMeta.post}, ptr @${mangleCtorThunk(className)}, ptr ${nameSym} } ; class ${className}`, `{ ${host.sizeType} -1, ${host.sizeType} ${intervalMeta.pre}, ${host.sizeType} ${intervalMeta.post}, ptr @${mangleCtorThunk(className)}, ptr ${nameSym}, ${host.sizeType} 0, ${host.sizeType} ${meta.def.jsLength ?? 0} } ; class ${className}`,
``, ``,
); );
} }
@@ -61,7 +61,7 @@ export class LlvmDebugInfo {
const id = this.add(`distinct !DISubprogram(name: ${quoted(fn.name)}, linkageName: ${quoted(mangleFunction(fn.name))}, scope: ${file}, file: ${file}, line: ${pos.line}, type: ${signature}, scopeLine: ${pos.line}, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition, unit: ${this.unit(pos.file).id})`); const id = this.add(`distinct !DISubprogram(name: ${quoted(fn.name)}, linkageName: ${quoted(mangleFunction(fn.name))}, scope: ${file}, file: ${file}, line: ${pos.line}, type: ${signature}, scopeLine: ${pos.line}, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition, unit: ${this.unit(pos.file).id})`);
const scopes: { loc: SrcLoc; id: string }[] = []; const scopes: { loc: SrcLoc; id: string }[] = [];
this.lexicalScopes.set(id, scopes); this.lexicalScopes.set(id, scopes);
const captures = new Set((fn.captures ?? []).map((c) => c.localId)); const captures = new Set([...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId));
const spans = fn.locals.filter((local) => !captures.has(local.id)).flatMap((local) => local.source ? [local.source.scope] : []); const spans = fn.locals.filter((local) => !captures.has(local.id)).flatMap((local) => local.source ? [local.source.scope] : []);
// Outer scopes first; declarations in flattened IR still retain their // Outer scopes first; declarations in flattened IR still retain their
// source block, loop, catch, or function scope (including hoisted var). // source block, loop, catch, or function scope (including hoisted var).
+38
View File
@@ -396,6 +396,13 @@ export class LlDyn {
case "func": case "func":
kindIs(DYN_KIND.FUNC); kindIs(DYN_KIND.FUNC);
break; break;
case "classval": {
this.host.declare(`declare zeroext i1 @scr_dyn_class_is(ptr, ptr)`);
const matched = B.tmp();
B.line(`${matched} = call zeroext i1 @scr_dyn_class_is(ptr %d, ptr ${this.host.cstr(key)})`);
B.terminate(`ret i1 ${matched}`);
break;
}
case "object": { case "object": {
if (t.className !== "%Error") { if (t.className !== "%Error") {
// Exact class capsules returned true before materialization. // Exact class capsules returned true before materialization.
@@ -606,6 +613,14 @@ export class LlDyn {
host.declare(`declare void @scr_dyn_check_fail(ptr, ptr, ptr)`); host.declare(`declare void @scr_dyn_check_fail(ptr, ptr, ptr)`);
const want = host.cstr(dynDesc(t, this.host.recordsById, this.host.unionsById)); const want = host.cstr(dynDesc(t, this.host.recordsById, this.host.unionsById));
const B = new BlockBuilder(); const B = new BlockBuilder();
if (t.kind === "classval") {
host.declare(`declare ptr @scr_dyn_class_check(ptr, ptr, ptr)`);
const checked = B.tmp();
B.line(`${checked} = call ptr @scr_dyn_class_check(ptr %d, ptr ${host.cstr(key)}, ptr %path)`);
B.terminate(`ret ptr ${checked}`);
this.defs.push(`define internal ptr @${name}(ptr %d, ptr %path) ${FN_ATTRS} {`, B.render(), `}`, ``);
return name;
}
if (isRefCounted(t) && t.kind !== "dyn") { if (isRefCounted(t) && t.kind !== "dyn") {
host.declare(`declare zeroext i1 @scr_dyn_typed_ref_is(ptr, ptr, ${host.sizeType})`); host.declare(`declare zeroext i1 @scr_dyn_typed_ref_is(ptr, ptr, ${host.sizeType})`);
host.declare(`declare ptr @scr_dyn_typed_ref_unbox(ptr)`); host.declare(`declare ptr @scr_dyn_typed_ref_unbox(ptr)`);
@@ -1314,6 +1329,13 @@ export class LlDyn {
B.terminate(`ret ptr ${r}`); B.terminate(`ret ptr ${r}`);
break; break;
} }
case "classval": {
host.declare(`declare ptr @scr_dyn_new_class(ptr, ptr)`);
const r = B.tmp();
B.line(`${r} = call ptr @scr_dyn_new_class(ptr %v, ptr ${host.cstr(key)})`);
B.terminate(`ret ptr ${r}`);
break;
}
case "func": { case "func": {
const r = B.tmp(); const r = B.tmp();
B.line( B.line(
@@ -1489,6 +1511,20 @@ export class LlDyn {
const len = B.tmp(); const len = B.tmp();
B.line(`${len} = call double @scr_arr_len(ptr %v)`); B.line(`${len} = call double @scr_arr_len(ptr %v)`);
B.countedLoop(len, (i) => { B.countedLoop(len, (i) => {
host.declare(`declare double @scr_arr_state(ptr, double)`);
const state = B.tmp();
const undefinedState = B.tmp();
const undefinedLabel = B.newLabel("tda.undefined");
const valueLabel = B.newLabel("tda.value");
const doneLabel = B.newLabel("tda.done");
B.line(`${state} = call double @scr_arr_state(ptr %v, double ${i})`);
B.line(`${undefinedState} = fcmp oeq double ${state}, 2.0`); // SCR_ARR_UNDEFINED
B.terminate(`br i1 ${undefinedState}, label %${undefinedLabel}, label %${valueLabel}`);
B.startBlock(undefinedLabel);
const retained = this.retainDyn(B, this.undef(B));
B.line(`call void @scr_dyn_arr_push(ptr ${d}, ptr ${retained})`);
B.terminate(`br label %${doneLabel}`);
B.startBlock(valueLabel);
if (elem.kind === "f64" || elem.kind === "bool") { if (elem.kind === "f64" || elem.kind === "bool") {
const acc = elem.kind; const acc = elem.kind;
const accTy = elem.kind === "f64" ? "double" : "i1"; const accTy = elem.kind === "f64" ? "double" : "i1";
@@ -1513,6 +1549,8 @@ export class LlDyn {
B.line(`call void @scr_dyn_arr_push(ptr ${d}, ptr ${conv})`); B.line(`call void @scr_dyn_arr_push(ptr ${d}, ptr ${conv})`);
B.line(`call void ${releaseSym(host, elem)}(ptr ${e})`); B.line(`call void ${releaseSym(host, elem)}(ptr ${e})`);
} }
B.terminate(`br label %${doneLabel}`);
B.startBlock(doneLabel);
}); });
if (cyclicArr) B.line(`call void @scr_dyn_from_leave()`); if (cyclicArr) B.line(`call void @scr_dyn_from_leave()`);
B.terminate(`ret ptr ${d}`); B.terminate(`ret ptr ${d}`);
+18 -3
View File
@@ -89,7 +89,7 @@ import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/c
import { allocateFfiCallbackAdapters, hasForeignFfiCallback, hasRetainedFfiCallback, type FfiCallbackAdapter } from "../ffi-callbacks.js"; import { allocateFfiCallbackAdapters, hasForeignFfiCallback, hasRetainedFfiCallback, type FfiCallbackAdapter } from "../ffi-callbacks.js";
import { RUNTIME_ABI_MARKER } from "../runtime-abi.js"; import { RUNTIME_ABI_MARKER } from "../runtime-abi.js";
import { computeMayThrow } from "../c/may-throw.js"; import { computeMayThrow } from "../c/may-throw.js";
import { mangleArgPack, mangleAsyncSpawn, mangleClassObj, mangleFnClosure, mangleFunction, mangleGenDrop, mangleGenSpawn, mangleGlobal, mangleLocal, mangleRecordStruct, mangleTrampoline, mangleWrapper } from "../mangle.js"; import { mangleArgPack, mangleAsyncSpawn, mangleClassObj, mangleClassStruct, mangleFnClosure, mangleFunction, mangleGenDrop, mangleGenSpawn, mangleGlobal, mangleLocal, mangleRecordStruct, mangleTrampoline, mangleWrapper } from "../mangle.js";
import { BlockBuilder } from "./blocks.js"; import { BlockBuilder } from "./blocks.js";
import { LlvmDebugInfo } from "./debug-info.js"; import { LlvmDebugInfo } from "./debug-info.js";
import { f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl, llvmCommentText } from "./common.js"; import { f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl, llvmCommentText } from "./common.js";
@@ -1123,7 +1123,7 @@ export class LlEmitter {
// class-object shape { rc, pre, post, ctor, name } — field reads on // class-object shape { rc, pre, post, ctor, name } — field reads on
// builtin errors and classval loads GEP through these. // builtin errors and classval loads GEP through these.
`%ScrError = type { ${this.sizeType}, ptr, ptr, ptr, ptr, ptr }`, `%ScrError = type { ${this.sizeType}, ptr, ptr, ptr, ptr, ptr }`,
`%ScrClassObj = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ptr }`, `%ScrClassObj = type { ${this.sizeType}, ${this.sizeType}, ${this.sizeType}, ptr, ptr, ${this.sizeType}, ${this.sizeType} }`,
// The runtime emitter prefix { rc, vt, reg, cls } — user subclasses // The runtime emitter prefix { rc, vt, reg, cls } — user subclasses
// embed it (classes.ts), and bare-emitter GEPs address through it. // embed it (classes.ts), and bare-emitter GEPs address through it.
`%ScrEmitter = type { ${this.sizeType}, ptr, ptr, ptr }`, `%ScrEmitter = type { ${this.sizeType}, ptr, ptr, ptr }`,
@@ -3102,7 +3102,7 @@ export class LlEmitter {
this.scopes = []; this.scopes = [];
this.jumpTargets = []; this.jumpTargets = [];
this.currentLocals = new Map(fn.locals.map((l) => [l.id, l])); this.currentLocals = new Map(fn.locals.map((l) => [l.id, l]));
this.captureIds = new Set((fn.captures ?? []).map((c) => c.localId)); this.captureIds = new Set([...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId));
this.integerLoopBindings.clear(); this.integerLoopBindings.clear();
this.integerRanges = analyzeIntegerRanges(fn); this.integerRanges = analyzeIntegerRanges(fn);
this.chainSlots.clear(); this.chainSlots.clear();
@@ -3177,6 +3177,21 @@ export class LlEmitter {
B.line(`${box} = load ptr, ptr ${p}`); B.line(`${box} = load ptr, ptr ${p}`);
B.line(`store ptr ${box}, ptr %${mangleLocal(c.localId)} ; captured ${c.name}`); B.line(`store ptr ${box}, ptr %${mangleLocal(c.localId)} ; captured ${c.name}`);
}); });
(fn.classCaptures ?? []).forEach((c) => {
const self = fn.params[0]!;
if (self.type.kind !== "object") throw new InternalCompilerError("class captures require an instance receiver");
const classSlot = B.tmp();
const classValue = B.tmp();
const caps = B.tmp();
const slot = B.tmp();
const box = B.tmp();
B.line(`${classSlot} = getelementptr inbounds %${mangleClassStruct(self.type.className)}, ptr %p_${mangleLocal(self.localId)}, i64 0, i32 1`);
B.line(`${classValue} = load ptr, ptr ${classSlot}`);
B.line(`${caps} = getelementptr inbounds %ScrClassObj, ptr ${classValue}, i64 1`);
B.line(`${slot} = getelementptr inbounds ptr, ptr ${caps}, ${this.sizeType} ${c.slot}`);
B.line(`${box} = load ptr, ptr ${slot}`);
B.line(`store ptr ${box}, ptr %${mangleLocal(c.localId)} ; class capture ${c.name}`);
});
// Params spill into their slots; the function scope owns refcounted // Params spill into their slots; the function scope owns refcounted
// params (callees own their params — callers passed +1). Boxed params // params (callees own their params — callers passed +1). Boxed params
// allocate the shared binding and move the raw value in. // allocate the shared binding and move the raw value in.
@@ -3,7 +3,7 @@ import { InternalCompilerError } from "../../errors.js";
import { newValueMayThrow } from "../../ir/analysis.js"; import { newValueMayThrow } from "../../ir/analysis.js";
import { isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted } from "../../ir/ir.js"; import { isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isRefCounted } from "../../ir/ir.js";
import { collectFfiRetainedOps, parseFfiCallbackKey } from "../ffi-callbacks.js"; import { collectFfiRetainedOps, parseFfiCallbackKey } from "../ffi-callbacks.js";
import { mangleClassNew, mangleClassRetain, mangleFnClosure, mangleFunction, mangleLocal, mangleVtStruct } from "../mangle.js"; import { mangleClassNew, mangleClassRetain, mangleClassStruct, mangleFnClosure, mangleFunction, mangleLocal, mangleVtStruct } from "../mangle.js";
import { classStructSym } from "./classes.js"; import { classStructSym } from "./classes.js";
import { LlvmUnsupportedError } from "./unsupported.js"; import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js"; import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
@@ -479,6 +479,21 @@ export function emitCallExpr(host: LlvmEmitterContext, e: ExprOf<"call" | "ffiCa
// address. The +1 retain is a no-op on immortals but keeps the // address. The +1 retain is a no-op on immortals but keeps the
// owned-temps discipline uniform (the regexLit pattern). // owned-temps discipline uniform (the regexLit pattern).
const sym = host.classObjSym(e.className); const sym = host.classObjSym(e.className);
if (e.captures !== undefined) {
host.declare(`declare ptr @scr_classobj_new(ptr, ${host.sizeType})`);
const value = B.tmp();
B.line(`${value} = call ptr @scr_classobj_new(ptr @${sym}, ${host.sizeType} ${e.captures.length})`);
e.captures.forEach((id, index) => {
const box = host.loadBox(`%${mangleLocal(id)}`);
const owned = host.retainBox(box);
const caps = B.tmp();
const slot = B.tmp();
B.line(`${caps} = getelementptr inbounds %ScrClassObj, ptr ${value}, i64 1`);
B.line(`${slot} = getelementptr inbounds ptr, ptr ${caps}, ${host.sizeType} ${index}`);
B.line(`store ptr ${owned}, ptr ${slot}`);
});
return host.own({ name: value, type: e.type });
}
return host.own({ name: host.retainValue(`@${sym}`, e.type), type: e.type }); return host.own({ name: host.retainValue(`@${sym}`, e.type), type: e.type });
} }
case "newValue": { case "newValue": {
@@ -500,7 +515,7 @@ export function emitCallExpr(host: LlvmEmitterContext, e: ExprOf<"call" | "ffiCa
.map((a, i) => `${host.llType(ctor.params[i + 1]!.type)} ${a.name}`) .map((a, i) => `${host.llType(ctor.params[i + 1]!.type)} ${a.name}`)
.join(", "); .join(", ");
const t = B.tmp(); const t = B.tmp();
B.line(`${t} = call ptr ${thunk}(${argList})`); B.line(`${t} = call ptr ${thunk}(ptr ${callee.name}${argList ? ", " + argList : ""})`);
const out = host.own({ name: t, type: e.type }); const out = host.own({ name: t, type: e.type });
if (newValueMayThrow(cls, host.classMeta.get(cls), host.mayThrow)) host.emitPendingCheck(); if (newValueMayThrow(cls, host.classMeta.get(cls), host.mayThrow)) host.emitPendingCheck();
return out; return out;
@@ -512,6 +527,15 @@ export function emitCallExpr(host: LlvmEmitterContext, e: ExprOf<"call" | "ffiCa
if (e.value.type.kind !== "object") throw new InternalCompilerError("llvm emitter bug: instanceOfValue on a non-object"); if (e.value.type.kind !== "object") throw new InternalCompilerError("llvm emitter bug: instanceOfValue on a non-object");
const v = host.emitExpr(e.value); const v = host.emitExpr(e.value);
const target = host.emitExpr(e.classValue); const target = host.emitExpr(e.classValue);
if (host.classMeta.get(e.value.type.className)?.def.localCaptures !== undefined) {
const slot = B.tmp();
const actual = B.tmp();
const result = B.tmp();
B.line(`${slot} = getelementptr inbounds %${mangleClassStruct(e.value.type.className)}, ptr ${v.name}, i64 0, i32 1`);
B.line(`${actual} = load ptr, ptr ${slot}`);
B.line(`${result} = icmp eq ptr ${actual}, ${target.name}`);
return { name: result, type: e.type };
}
const pre = host.loadVtPre(v.name, e.value.type.className); const pre = host.loadVtPre(v.name, e.value.type.className);
const tprep = B.tmp(); const tprep = B.tmp();
const tpre = B.tmp(); const tpre = B.tmp();
+5 -3
View File
@@ -94,8 +94,7 @@ export function retainSym(host: ShapeHost, t: IrType): string {
export function releaseSym(host: ShapeHost, t: IrType): string { export function releaseSym(host: ShapeHost, t: IrType): string {
const stem = runtimeRcStem(t); const stem = runtimeRcStem(t);
if (stem !== null) { if (stem !== null) {
// scr_classobj_release is header-inline only; LLVM calls its exported // Class objects share the container adapter at direct release sites.
// `_v` wrapper. Every other runtime family exports the typed release.
const suffix = t.kind === "classval" ? "_release_v" : "_release"; const suffix = t.kind === "classval" ? "_release_v" : "_release";
const release = `@${stem}${suffix}`; const release = `@${stem}${suffix}`;
host.declare(`declare void ${release}(ptr)`); host.declare(`declare void ${release}(ptr)`);
@@ -117,6 +116,9 @@ export function releaseSym(host: ShapeHost, t: IrType): string {
* their RC rows refuse first). */ * their RC rows refuse first). */
export function traceAdapter(host: ShapeHost, t: IrType): string | null { export function traceAdapter(host: ShapeHost, t: IrType): string | null {
switch (t.kind) { switch (t.kind) {
case "classval":
host.declare(`declare void @scr_classobj_trace_v(ptr, ptr, ptr)`);
return "@scr_classobj_trace_v";
case "func": case "func":
host.declare(`declare void @scr_closure_trace_v(ptr, ptr, ptr)`); host.declare(`declare void @scr_closure_trace_v(ptr, ptr, ptr)`);
return "@scr_closure_trace_v"; return "@scr_closure_trace_v";
@@ -208,7 +210,7 @@ export function arrNewCall(host: ShapeHost, elem: IrType, capText: string): stri
elem.kind === "map" || elem.kind === "set" || // scr_map_* adapters and typed key/value tracing elem.kind === "map" || elem.kind === "set" || // scr_map_* adapters and typed key/value tracing
elem.kind === "symbol" || // symbol identities: scr_sym_* adapters, no trace elem.kind === "symbol" || // symbol identities: scr_sym_* adapters, no trace
elem.kind === "bigint" || // immutable numeric values: scr_bigint_* adapters elem.kind === "bigint" || // immutable numeric values: scr_bigint_* adapters
elem.kind === "classval" || // class objects: no-op adapters, no trace (immortal statics) elem.kind === "classval" || // local class objects own traced capture boxes
elem.kind === "promise" || // promise entries (Promise.all inputs): full REF story elem.kind === "promise" || // promise entries (Promise.all inputs): full REF story
elem.kind === "child" || // spawned child handles: scr_child_* adapters, no trace elem.kind === "child" || // spawned child handles: scr_child_* adapters, no trace
elem.kind === "netServer" || // server handles ([...set] drains): REF, no trace elem.kind === "netServer" || // server handles ([...set] drains): REF, no trace
+2 -2
View File
@@ -2,8 +2,8 @@
* of the link contract: a program object keeps this symbol undefined and the * of the link contract: a program object keeps this symbol undefined and the
* matching runtime defines it, so incompatible manual links fail before the * matching runtime defines it, so incompatible manual links fail before the
* program can start. */ * program can start. */
export const RUNTIME_ABI_VERSION = 3 as const; export const RUNTIME_ABI_VERSION = 4 as const;
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v3" as const; export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v4" as const;
/** Object consumption is useful today, but the complete scr_* surface may /** Object consumption is useful today, but the complete scr_* surface may
* still change before 1.0. The versioned marker prevents accidental mixing; * still change before 1.0. The versioned marker prevents accidental mixing;
@@ -34,7 +34,7 @@ async function fixture(target: NativeTargetSpec) {
const manifest: RuntimePackManifest = { const manifest: RuntimePackManifest = {
schema: "scriptc.runtime-pack.v1", format: 1, package: target.runtimePackPackage, version: compilerReleaseVersion(), schema: "scriptc.runtime-pack.v1", format: 1, package: target.runtimePackPackage, version: compilerReleaseVersion(),
target: { name: target.name, llvm_triple: target.llvmTriple, architecture: target.architecture, object_format: target.objectFormat, minimum_os: target.minimumOs }, target: { name: target.name, llvm_triple: target.llvmTriple, architecture: target.architecture, object_format: target.objectFormat, minimum_os: target.minimumOs },
runtime_abi: { version: 3, marker: "scr_runtime_abi_v3" }, runtime_abi: { version: 4, marker: "scr_runtime_abi_v4" },
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
flavors: { flavors: {
@@ -102,7 +102,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
object_format: target.objectFormat, object_format: target.objectFormat,
minimum_os: target.minimumOs, minimum_os: target.minimumOs,
}, },
runtime_abi: { version: 3, marker: "scr_runtime_abi_v3" }, runtime_abi: { version: 4, marker: "scr_runtime_abi_v4" },
compiler: { compiler: {
command: "clang", command: "clang",
identity: "fixture clang", identity: "fixture clang",
@@ -470,6 +470,54 @@ function defaultParameterShape(lowerer: Lowerer, param: ts.ParameterDeclaration,
* coercion an ordinary argument gets (coerceInto against its shape, * coercion an ordinary argument gets (coerceInto against its shape,
* DYN conversion in a dyn rest, element coercion in a typed rest). */ * DYN conversion in a dyn rest, element coercion in a typed rest). */
leading?: readonly IrExpr[],): IrExpr[] { leading?: readonly IrExpr[],): IrExpr[] {
if (canCompleteRuntimeSpread(lowerer, shapes) &&
argNodes.some((arg) => ts.isSpreadElement(arg) && !fixedTupleSpreadInfo(lowerer, arg.expression))) {
// Runtime-length spreads determine the complete argument list before
// any parameter default runs. Build it once in source order, then
// extract the fixed native ABI slots; missing elements are undefined.
const pack = lowerer.declareHiddenLocal("%spreadArgs", DYN);
const ref = (): IrExpr => ({ kind: "varRef", localId: pack.id, type: DYN, loc });
const stmts: IrStmt[] = [{ kind: "varDecl", localId: pack.id, init: { kind: "dynArrLit", elems: [], type: DYN, loc }, loc }];
const push = (value: IrExpr, fn: "dyn.packPush" | "dyn.packPushSpreadIter") => {
stmts.push({ kind: "exprStmt", expr: { kind: "libCall", fn, args: [ref(), value], type: VOID, loc: value.loc }, loc: value.loc });
};
for (const value of leading ?? []) push(lowerer.coerceInto(blame, value, DYN), "dyn.packPush");
const spreads = argNodes.filter(ts.isSpreadElement).length;
argNodes.forEach((arg, index) => {
if (!ts.isSpreadElement(arg)) {
push(lowerer.lowerExprExpecting(arg, DYN), "dyn.packPush");
return;
}
let value = lowerer.lowerExpr(arg.expression);
if (value.type.kind === "set") {
value = { kind: "setIntrinsic", method: "toArray", receiver: value, args: [], type: arrayOf(value.type.elem), loc: value.loc };
} else if (value.type.kind === "object") {
value = lowerer.classIteratorDrainCall(value, locOf(arg)) ?? value;
}
if (value.type.kind === "array") {
// Argument iteration materializes holes as present undefined
// before conversion to the dense checked-dynamic argument pack.
value = { kind: "arrayLit", elems: [value], spreads: [0], type: value.type, loc: value.loc };
}
value = lowerer.coerceInto(arg.expression, value, DYN);
if (spreads === 1 && index === argNodes.length - 1) {
stmts.push({ kind: "exprStmt", expr: {
kind: "libCall", fn: "dyn.packPushSpread", args: [ref(), value, { kind: "strLit", value: arg.expression.getText(), type: STRING, loc }], type: VOID, loc,
}, loc });
} else push(value, "dyn.packPushSpreadIter");
});
const args = shapes.map((shape, index): IrExpr => {
if (shape.mode === "arguments") return ref();
const value: IrExpr = shape.mode === "rest" || shape.mode === "dynRest"
? { kind: "dynInvoke", recv: ref(), method: "slice", calleeName: "arguments.slice", args: [{ kind: "dynFrom", value: { kind: "numLit", value: index, type: F64, loc }, type: DYN, loc }], type: DYN, loc }
: { kind: "dynKeyGet", value: ref(), key: { kind: "strLit", value: String(index), type: STRING, loc }, type: DYN, loc };
return lowerer.coerceInto(blame, value, shape.type);
});
// Every supported form has at least one native slot to carry the
// evaluation prefix, including calls whose only slot is their rest.
args[0] = { kind: "seqExpr", stmts, result: args[0]!, type: args[0]!.type, loc };
return args;
}
type ArgSource = ts.Expression | { ir: IrExpr }; type ArgSource = ts.Expression | { ir: IrExpr };
const isIr = (s: ArgSource | undefined): s is { ir: IrExpr } => const isIr = (s: ArgSource | undefined): s is { ir: IrExpr } =>
s !== undefined && !("kind" in s); s !== undefined && !("kind" in s);
@@ -539,9 +587,18 @@ function defaultParameterShape(lowerer: Lowerer, param: ts.ParameterDeclaration,
lowerer.unsupported("SC1090", source, "spread arguments into fixed parameter positions (a spread can only fill a rest parameter)"); lowerer.unsupported("SC1090", source, "spread arguments into fixed parameter positions (a spread can only fill a rest parameter)");
} }
const slotType = i < restAt ? positional[i]!.type : DYN; const slotType = i < restAt ? positional[i]!.type : DYN;
const value = isIr(source) let value: IrExpr;
? lowerer.coerceInto(blame, source.ir, slotType) if (isIr(source)) {
: lowerer.lowerExprExpecting(source, slotType); let ir = source.ir;
// Tuple-spread storage also feeds later arguments. Declare it in
// this shared sequence, not inside one argument's initializer,
// whose statement frame ends before the next field is read.
if (ir.kind === "seqExpr") {
stmts.push(...ir.stmts);
ir = ir.result;
}
value = lowerer.coerceInto(blame, ir, slotType);
} else value = lowerer.lowerExprExpecting(source, slotType);
const saved = lowerer.declareHiddenLocal("%callArg", slotType); const saved = lowerer.declareHiddenLocal("%callArg", slotType);
stmts.push({ kind: "varDecl", localId: saved.id, init: value, loc: value.loc }); stmts.push({ kind: "varDecl", localId: saved.id, init: value, loc: value.loc });
return { kind: "varRef", localId: saved.id, type: slotType, loc: value.loc }; return { kind: "varRef", localId: saved.id, type: slotType, loc: value.loc };
@@ -1657,6 +1714,7 @@ export function genericFnOf(lowerer: Lowerer, ident: ts.Identifier): GenericFnIn
fnCtx.generator = generatorMeta(lowerer, inst.returnType); fnCtx.generator = generatorMeta(lowerer, inst.returnType);
} }
lowerer.fnStack.push(fnCtx); lowerer.fnStack.push(fnCtx);
lowerer.localClassInstantiations.push({ owner: decl, name: inst.name });
try { try {
// STATIC generic methods: `this`/`super` name the RECEIVER class (a // STATIC generic methods: `this`/`super` name the RECEIVER class (a
// dynamic value) — the lowerStaticMethod fence, applied here because // dynamic value) — the lowerStaticMethod fence, applied here because
@@ -1736,6 +1794,7 @@ export function genericFnOf(lowerer: Lowerer, ident: ts.Identifier): GenericFnIn
lowerer.typeParamTsBindings = prevTsBindings; lowerer.typeParamTsBindings = prevTsBindings;
lowerer.implicitParamTypes = prevImplicit; lowerer.implicitParamTypes = prevImplicit;
lowerer.instantiationContext = prevContext; lowerer.instantiationContext = prevContext;
lowerer.localClassInstantiations.pop();
lowerer.suppressStats = prevSuppress; lowerer.suppressStats = prevSuppress;
} }
} }
@@ -5689,6 +5748,7 @@ function lowerOptionalStringNumber(
shapes: readonly ParamShape[], shapes: readonly ParamShape[],
argNodes: readonly ts.Expression[], argNodes: readonly ts.Expression[],
): boolean { ): boolean {
if (canCompleteRuntimeSpread(lowerer, shapes)) return false;
const restAt = shapes.findIndex((s) => s.mode === "rest" || s.mode === "dynRest" || s.mode === "islandRest"); const restAt = shapes.findIndex((s) => s.mode === "rest" || s.mode === "dynRest" || s.mode === "islandRest");
let position = 0; let position = 0;
for (const arg of argNodes) { for (const arg of argNodes) {
@@ -5705,6 +5765,11 @@ function lowerOptionalStringNumber(
return false; return false;
} }
function canCompleteRuntimeSpread(lowerer: Lowerer, shapes: readonly ParamShape[]): boolean {
return shapes.length > 0 && shapes.every((shape) => shape.mode !== "islandRest" && shape.callDefault === undefined &&
canDynCheckTo(shape.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id)));
}
/** METHOD calls on dyn receivers (`pkg.name.replace(...)`, `rawName.split`, /** METHOD calls on dyn receivers (`pkg.name.replace(...)`, `rawName.split`,
* `ws.packages.filter(...)` — JSON.parse-derived values): validate the * `ws.packages.filter(...)` — JSON.parse-derived values): validate the
* receiver's dyn kind, extract, and ride the STATIC method machinery — the * receiver's dyn kind, extract, and ride the STATIC method machinery — the
@@ -5,7 +5,7 @@ import { InternalCompilerError } from "../../errors.js";
* accessor access, upcasts, `new` expressions, and the builtin Error * accessor access, upcasts, `new` expressions, and the builtin Error
* hierarchy registration. */ * hierarchy registration. */
import * as ts from "../ts7/adapter.js"; import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js"; import type { FnCtx, Lowerer } from "./lowerer.js";
import { BOOL, DATE_T, DYN, F64, bytesOf, IrClassDef, IrExpr, IrFunction, IrLocal, IrParam, IrStmt, IrType, JSVAL, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, typeEquals } from "../../ir/ir.js"; import { BOOL, DATE_T, DYN, F64, bytesOf, IrClassDef, IrExpr, IrFunction, IrLocal, IrParam, IrStmt, IrType, JSVAL, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, isUnitType, typeEquals } from "../../ir/ir.js";
import { MAX_GENERIC_INSTANCES, appendImplicitUndefinedReturn, bodyReadsArguments, generatorMeta, genericCallInstance, implicitAnyParamSymbolsOf, implicitCallInstance, implicitMonoFile, omittedArgFor, type GenericFnInfo, type ParamShape } from "./lower-calls.js"; import { MAX_GENERIC_INSTANCES, appendImplicitUndefinedReturn, bodyReadsArguments, generatorMeta, genericCallInstance, implicitAnyParamSymbolsOf, implicitCallInstance, implicitMonoFile, omittedArgFor, type GenericFnInfo, type ParamShape } from "./lower-calls.js";
import { isGenericCallableMemberType, jsOpenObjectType, typeKey } from "../type-mapper.js"; import { isGenericCallableMemberType, jsOpenObjectType, typeKey } from "../type-mapper.js";
@@ -28,8 +28,30 @@ import { rejectStaticThis } from "./static-this.js";
import { lowerUrlNew } from "./lower-url.js"; import { lowerUrlNew } from "./lower-url.js";
import { isNativeProxyInitializer, lowerNativeProxy } from "./expressions/native-proxy.js"; import { isNativeProxyInitializer, lowerNativeProxy } from "./expressions/native-proxy.js";
function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean {
if (!ts.isClassExpression(decl)) return false;
for (let parent: ts.Node | undefined = decl.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) {
if (ts.isFunctionLike(parent)) return true;
}
return false;
}
export function storedClassValueType(lowerer: Lowerer, expression: ts.Expression): IrType | null {
if (ts.isIdentifier(expression)) {
const stored = lowerer.peekLocal(expression) ?? lowerer.globalOf(expression);
if (stored?.type.kind === "classval") return stored.type;
}
return lowerer.mapTypeOf(lowerer.typeOf(expression));
}
export interface ClassInfo { export interface ClassInfo {
def: IrClassDef; def: IrClassDef;
localClass?: {
context: FnCtx | null;
bodies: Map<string, IrFunction>;
captureSources: string[];
ready: boolean;
};
/** ALL fields visible on instances — the inherited ones included — for /** ALL fields visible on instances — the inherited ones included — for
* receiver-side lookup (def.fields carries the layout order). */ * receiver-side lookup (def.fields carries the layout order). */
fields: Map<string, IrType>; fields: Map<string, IrType>;
@@ -1181,6 +1203,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
} }
} }
if (base?.localClass) lowerer.unsupported("SC1090", decl, "extending a function-local class");
const fields = new Map<string, IrType>(base ? base.fields : []); const fields = new Map<string, IrType>(base ? base.fields : []);
const errorRooted = (() => { const errorRooted = (() => {
for (let c = base; c; c = c.base) if (c.builtinError) return true; for (let c = base; c; c = c.base) if (c.builtinError) return true;
@@ -1817,7 +1840,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
// name (the override stays on the vtable), a shadowed field, or // name (the override stays on the vtable), a shadowed field, or
// a generic-class instantiation's member. // a generic-class instantiation's member.
if ( if (
implicitMonoFile(decl.getSourceFile()) && implicitMonoFile(decl.getSourceFile()) && !functionLocalClass(decl) &&
ts.isIdentifier(member.name) && ts.isIdentifier(member.name) &&
!lowerer.virtualJsMethods.has(member) && !lowerer.virtualJsMethods.has(member) &&
inst === undefined && decl.typeParameters === undefined && inst === undefined && decl.typeParameters === undefined &&
@@ -2392,6 +2415,9 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
// INSTANTIATION prints its family's name — JS has one `Box`. // INSTANTIATION prints its family's name — JS has one `Box`.
jsName: jsNameOverride ?? decl.name?.text ?? "", jsName: jsNameOverride ?? decl.name?.text ?? "",
...(base ? { base: base.def.name } : {}), ...(base ? { base: base.def.name } : {}),
jsLength: ctor?.parameters.findIndex((parameter) => parameter.initializer || parameter.dotDotDotToken) === -1
? ctor.parameters.length
: Math.max(0, ctor?.parameters.findIndex((parameter) => parameter.initializer || parameter.dotDotDotToken) ?? 0),
// Layout order: the base chain's fields as an IDENTICAL prefix, // Layout order: the base chain's fields as an IDENTICAL prefix,
// then this class's own — what makes an upcast a reinterpret. // then this class's own — what makes an upcast a reinterpret.
// Redeclared INHERITED fields contribute no slot (their // Redeclared INHERITED fields contribute no slot (their
@@ -3009,6 +3035,9 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla
export function classValueRef(lowerer: Lowerer, info: ClassInfo, blame: ts.Node): IrExpr { export function classValueRef(lowerer: Lowerer, info: ClassInfo, blame: ts.Node): IrExpr {
const display = info.def.name.replace(/^%|^%m\d+\./, ""); const display = info.def.name.replace(/^%|^%m\d+\./, "");
fenceDecorationThrows(lowerer, info, blame); fenceDecorationThrows(lowerer, info, blame);
if (info.localClass && !ts.isClassExpression(blame)) {
lowerer.unsupported("SC1090", blame, "named self references inside function-local classes");
}
if (info.generic) { if (info.generic) {
// `typeof Box` — the uninstantiated FAMILY as a value: no thunk, no // `typeof Box` — the uninstantiated FAMILY as a value: no thunk, no
// single constructor ABI. INSTANTIATIONS have class objects // single constructor ABI. INSTANTIATIONS have class objects
@@ -3155,19 +3184,12 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
return ""; return "";
} }
/** A class EXPRESSION's ClassInfo: collection on first encounter (the /** Collect a class expression once per enclosing specialization. Function-local
* declaration path over the shared ClassLikeDeclaration machinery, with * evaluations allocate fresh constructor objects; top-level expressions use
* NamedEvaluation supplying the runtime .name), idempotent per node — * immortal templates. Static initialization remains limited to whole top-level
* tryLowerExpression's speculative visits and the heritage recursion reuse the * initializers, where pendingClassExprInits preserves its evaluation point. */
* first collection. The honest v1 boundary is TOP-LEVEL evaluation
* positions only: each evaluation of a class expression in JS mints a
* DISTINCT class (fresh identity, fresh statics), and one immortal
* class object is exact only for expressions that evaluate exactly
* once. Statics-bearing expressions additionally restrict to positions
* where "immediately before the enclosing statement" IS the evaluation
* point (lowerFileInit drains pendingClassExprInits there). */
export function lowerClassExpressionInfo(lowerer: Lowerer, expr: ts.ClassExpression): ClassInfo { export function lowerClassExpressionInfo(lowerer: Lowerer, expr: ts.ClassExpression): ClassInfo {
const cached = lowerer.exprClassInfoByNode.get(expr); const cached = lowerer.exprClassInfoByName.get(lowerer.classNamer(expr));
if (cached) return cached; if (cached) return cached;
// Reentrancy guard: heritage resolution can DEMAND another class // Reentrancy guard: heritage resolution can DEMAND another class
// expression's collection (extends through property assignments — // expression's collection (extends through property assignments —
@@ -3183,21 +3205,26 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
"class expressions whose extends chain re-enters their own collection (a cyclic base through property assignments)", "class expressions whose extends chain re-enters their own collection (a cyclic base through property assignments)",
); );
} }
if (lowerer.instantiationContext) { let local = false;
lowerer.unsupported(
"SC1090",
expr,
"class expressions inside generic functions (each instantiation would need its own class)",
);
}
for (let p: ts.Node | undefined = expr.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) { for (let p: ts.Node | undefined = expr.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) {
if (ts.isFunctionLike(p) || ts.isClassStaticBlockDeclaration(p)) { if (ts.isClassStaticBlockDeclaration(p)) {
lowerer.unsupported( lowerer.unsupported(
"SC1090", "SC1090",
expr, expr,
"class expressions inside functions (each evaluation creates a DISTINCT class in JS — fresh identity, fresh statics; declare the class at top level)", "class expressions inside static blocks",
); );
} }
if (ts.isFunctionLike(p)) local = true;
}
if (local && expr.members.some((member) => member.name && ts.isPrivateIdentifier(member.name))) {
lowerer.unsupported("SC1090", expr, "function-local classes with private brands");
}
if (local && (expr.heritageClauses?.length || expr.typeParameters?.length ||
decoratorNodesOf(expr).length || expr.members.some((member) =>
ts.isClassStaticBlockDeclaration(member) || decoratorNodesOf(member).length ||
ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) ||
(ts.isMethodDeclaration(member) && member.typeParameters?.length)))) {
lowerer.unsupported("SC1090", expr, "function-local classes with inheritance, statics, decorators, or generic members");
} }
lowerer.collectingExprClasses.add(expr); lowerer.collectingExprClasses.add(expr);
try { try {
@@ -3207,7 +3234,11 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
} }
const info = lowerer.classes.get(lowerer.classNamer(expr)); const info = lowerer.classes.get(lowerer.classNamer(expr));
if (!info) throw new PoisonError(); // collection poisoned and reported if (!info) throw new PoisonError(); // collection poisoned and reported
lowerer.exprClassInfoByNode.set(expr, info); if (local) {
info.def.localCaptures = [];
info.localClass = { context: null, bodies: new Map(), captureSources: [], ready: false };
}
lowerer.exprClassInfoByName.set(info.def.name, info);
lowerer.exprClasses.push(info); lowerer.exprClasses.push(info);
lowerer.onExprClassCollected?.(info); lowerer.onExprClassCollected?.(info);
// Static field initializers and static blocks run when the class // Static field initializers and static blocks run when the class
@@ -3272,7 +3303,26 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
} }
} }
} }
return classValueRef(lowerer, lowerClassExpressionInfo(lowerer, expr), expr); const info = lowerClassExpressionInfo(lowerer, expr);
if (info.localClass && !info.localClass.ready) {
const context = newFnCtx(true, null, null, VOID);
info.localClass.context = context;
lowerer.fnStack.push(context);
try {
const bodies = lowerClassMembers(lowerer, info);
for (const body of bodies) info.localClass.bodies.set(body.name, body);
info.def.localCaptures = context.captures!;
info.localClass.captureSources = context.captureSources;
info.localClass.ready = true;
} finally {
lowerer.fnStack.pop();
info.localClass.context = null;
}
}
const value = classValueRef(lowerer, info, expr);
return info.localClass && value.kind === "classRef"
? { ...value, captures: info.localClass.captureSources }
: value;
} }
/** The EXACT class a receiver expression is statically known to BE (not /** The EXACT class a receiver expression is statically known to BE (not
@@ -3301,7 +3351,7 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
} }
let init: ts.Expression = decl.initializer; let init: ts.Expression = decl.initializer;
while (ts.isParenthesizedExpression(init)) init = init.expression; while (ts.isParenthesizedExpression(init)) init = init.expression;
if (ts.isClassExpression(init)) return lowerer.exprClassInfoByNode.get(init) ?? null; if (ts.isClassExpression(init)) return lowerer.exprClassInfoByName.get(lowerer.classNamer(init)) ?? null;
if (ts.isIdentifier(init)) { if (ts.isIdentifier(init)) {
const initSym = lowerer.resolveValueSymbol(init); const initSym = lowerer.resolveValueSymbol(init);
const aliased = initSym ? (lowerer.classBySymbol.get(initSym) ?? null) : null; const aliased = initSym ? (lowerer.classBySymbol.get(initSym) ?? null) : null;
@@ -3553,7 +3603,7 @@ export function staticFieldWriteTarget(lowerer: Lowerer, access: ts.PropertyAcce
* value or the member doesn't resolve. */ * value or the member doesn't resolve. */
export function lowerClassValueProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null { export function lowerClassValueProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
if (expr.questionDotToken) return null; if (expr.questionDotToken) return null;
const recvT = lowerer.mapTypeOf(lowerer.typeOf(expr.expression)); const recvT = storedClassValueType(lowerer, expr.expression);
if (recvT?.kind !== "classval") return null; if (recvT?.kind !== "classval") return null;
const loc = locOf(expr); const loc = locOf(expr);
const member = expr.name.text; const member = expr.name.text;
@@ -3910,7 +3960,7 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
// demand-driven like generic-fn instances, not reachability units: // demand-driven like generic-fn instances, not reachability units:
// they are never registered as units, so wantBody's name-keyed gate // they are never registered as units, so wantBody's name-keyed gate
// cannot apply — every member of a demanded instantiation lowers. // cannot apply — every member of a demanded instantiation lowers.
const always = info.genericInstance !== undefined || info.mixinInstance !== undefined; const always = info.genericInstance !== undefined || info.mixinInstance !== undefined || info.localClass !== undefined;
// A FAMILY has no constructor function at all (nothing constructs it; // A FAMILY has no constructor function at all (nothing constructs it;
// construction resolves to instantiations) and declares no instance // construction resolves to instantiations) and declares no instance
// members — only its statics lower below. // members — only its statics lower below.
@@ -3951,15 +4001,26 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
* the base's signature — forward every param to super(), then run own * the base's signature — forward every param to super(), then run own
* field initializers. */ * field initializers. */
export function lowerClassCtor(lowerer: Lowerer, info: ClassInfo): IrFunction { export function lowerClassCtor(lowerer: Lowerer, info: ClassInfo): IrFunction {
const cached = info.localClass?.bodies.get(`%${info.def.name}.constructor`);
if (cached) return cached;
return withInstanceBindings(lowerer, info, () => lowerClassCtorInner(lowerer, info)); return withInstanceBindings(lowerer, info, () => lowerClassCtorInner(lowerer, info));
} }
function classCaptureParams(info: ClassInfo, context: FnCtx): Pick<IrFunction, "classCaptures"> {
if (!info.localClass) return {};
const environment = info.localClass.context;
if (!environment) throw new InternalCompilerError("local class lowered outside its lexical environment");
return { classCaptures: (context.captures ?? []).map((capture, index) => ({
...capture, slot: environment.captures!.findIndex((entry) => entry.localId === context.captureSources[index]),
})) };
}
function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction { function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction {
const className = info.def.name; const className = info.def.name;
const thisType: IrType = { kind: "object", className }; const thisType: IrType = { kind: "object", className };
const prevClass = lowerer.currentClass; const prevClass = lowerer.currentClass;
lowerer.currentClass = info; lowerer.currentClass = info;
lowerer.fnStack.push(newFnCtx(false, null, null, VOID)); lowerer.fnStack.push(newFnCtx(info.localClass !== undefined, null, null, VOID));
try { try {
const thisLocal = lowerer.declareThis(thisType); const thisLocal = lowerer.declareThis(thisType);
const params: IrParam[] = [{ localId: thisLocal.id, name: "this", type: thisType }]; const params: IrParam[] = [{ localId: thisLocal.id, name: "this", type: thisType }];
@@ -4028,6 +4089,7 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
params, params,
returnType: VOID, returnType: VOID,
locals: lowerer.ctx.locals, locals: lowerer.ctx.locals,
...classCaptureParams(info, lowerer.ctx),
body, body,
loc: locOf(info.ctor ?? info.decl!), loc: locOf(info.ctor ?? info.decl!),
}; };
@@ -4285,6 +4347,11 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
* see collectClassShape). */ * see collectClassShape). */
export function lowerClassMethodMember(lowerer: Lowerer, info: ClassInfo, export function lowerClassMethodMember(lowerer: Lowerer, info: ClassInfo,
fnLike: ts.MethodDeclaration | ts.AccessorDeclaration,): IrFunction | null { fnLike: ts.MethodDeclaration | ts.AccessorDeclaration,): IrFunction | null {
if (info.localClass?.ready) {
const member = classMemberNameOf(lowerer, fnLike.name);
const name = ts.isMethodDeclaration(fnLike) ? member : `${ts.isGetAccessor(fnLike) ? "get" : "set"}:${member}`;
return info.localClass.bodies.get(`%${info.def.name}.${name}`) ?? null;
}
return withInstanceBindings(lowerer, info, () => lowerClassMethodMemberInner(lowerer, info, fnLike)); return withInstanceBindings(lowerer, info, () => lowerClassMethodMemberInner(lowerer, info, fnLike));
} }
@@ -4332,7 +4399,7 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
: isAsync && sig.ret.kind === "promise" : isAsync && sig.ret.kind === "promise"
? sig.ret.inner ? sig.ret.inner
: sig.ret; : sig.ret;
const fnCtx = newFnCtx(false, null, null, bodyReturn); const fnCtx = newFnCtx(info.localClass !== undefined, null, null, bodyReturn);
fnCtx.isAsync = isAsync; fnCtx.isAsync = isAsync;
if (genCh !== null) fnCtx.generator = genCh; if (genCh !== null) fnCtx.generator = genCh;
lowerer.fnStack.push(fnCtx); lowerer.fnStack.push(fnCtx);
@@ -4358,6 +4425,7 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
params, params,
returnType: bodyReturn, returnType: bodyReturn,
locals: lowerer.ctx.locals, locals: lowerer.ctx.locals,
...classCaptureParams(info, lowerer.ctx),
body, body,
loc: locOf(fnLike), loc: locOf(fnLike),
}; };
@@ -4939,6 +5007,7 @@ function fenceDecorationThrows(lowerer: Lowerer, info: ClassInfo, blame: ts.Node
function lowerProgramClassNew(lowerer: Lowerer, expr: ts.NewExpression, declaredInfo: ClassInfo, loc: SrcLoc): IrExpr { function lowerProgramClassNew(lowerer: Lowerer, expr: ts.NewExpression, declaredInfo: ClassInfo, loc: SrcLoc): IrExpr {
const info = genericNewTarget(lowerer, expr, declaredInfo); const info = genericNewTarget(lowerer, expr, declaredInfo);
fenceDecorationThrows(lowerer, info, expr); fenceDecorationThrows(lowerer, info, expr);
if (info.localClass) lowerer.unsupported("SC1090", expr, "construction through a function-local class's internal name");
lowerer.noteEdge(`%${info.def.name}.constructor`); lowerer.noteEdge(`%${info.def.name}.constructor`);
// A ctor-less chain into an EventEmitter base inherits `new C()` — // A ctor-less chain into an EventEmitter base inherits `new C()` —
// zero arguments (the options bag fences, like the super() form). // zero arguments (the options bag fences, like the super() form).
@@ -5808,7 +5877,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
// callee (unannotated heterogeneous registries) keeps a pointed // callee (unannotated heterogeneous registries) keeps a pointed
// fence — annotate the slot with the common constructor type. // fence — annotate the slot with the common constructor type.
{ {
const calleeT = lowerer.mapTypeOf(lowerer.typeOf(expr.expression)); const calleeT = storedClassValueType(lowerer, expr.expression);
if (calleeT?.kind === "classval") { if (calleeT?.kind === "classval") {
let info = lowerer.classes.get(calleeT.className); let info = lowerer.classes.get(calleeT.className);
if (!info && ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.name)) { if (!info && ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.name)) {
@@ -26,7 +26,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 { ambientNsRootOf, ambientUndefReadType, ambientUndefVarRootOf, ambientUndefinedFnSymbolOf, contextualUndefReadType, fenceEarlyAliasUse, fenceEarlyNsMemberRef, lowerNsIdentifierValue, nsMemberIdentOf, nsUndefRead, nsWritableTarget } from "./lower-namespaces.js";
import { expandoMemberRead, expandoWritableTarget } from "./lower-expando.js"; import { expandoMemberRead, expandoWritableTarget } from "./lower-expando.js";
import { lowerSocketInstanceOf, lowerTlsRootCertificates } from "./lower-server.js"; import { lowerSocketInstanceOf, lowerTlsRootCertificates } from "./lower-server.js";
import { findGenericMethodOn, lowerStaticFieldRead, staticFieldWriteTarget } from "./lower-classes.js"; import { findGenericMethodOn, lowerStaticFieldRead, staticFieldWriteTarget, storedClassValueType } from "./lower-classes.js";
import { bindingNeverReassigned, funcTypeFromParamShapes, implicitMonoFile, lowerTaggedTemplate, nullishGenericBindingUnitOf, objLitGenericFnInfoOf, objLitGenericFnNodeOf, requireObjLitGenericReceiver } from "./lower-calls.js"; import { bindingNeverReassigned, funcTypeFromParamShapes, implicitMonoFile, lowerTaggedTemplate, nullishGenericBindingUnitOf, objLitGenericFnInfoOf, objLitGenericFnNodeOf, requireObjLitGenericReceiver } from "./lower-calls.js";
import { mixinFnOfCallee } from "./lower-mixins.js"; import { mixinFnOfCallee } from "./lower-mixins.js";
import { isConstAssertionTypeNode, isGenericCallableMemberType, isParseArgsDynTypeName, underConstAssertion, unitOnlyUnion } from "../type-mapper.js"; import { isConstAssertionTypeNode, isGenericCallableMemberType, isParseArgsDynTypeName, underConstAssertion, unitOnlyUnion } from "../type-mapper.js";
@@ -7989,7 +7989,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
// STANDALONE target class has exactly one possible runtime value // STANDALONE target class has exactly one possible runtime value
// (itself — no descendants can flow into the slot), so the answer // (itself — no descendants can flow into the slot), so the answer
// folds statically exactly like the named-target folds below. // folds statically exactly like the named-target folds below.
const rhsClassval = lowerer.mapTypeOf(lowerer.typeOf(expr.right)); const rhsClassval = storedClassValueType(lowerer, expr.right);
if (rhsClassval?.kind === "classval" && !lowerer.caughtLocalOf(expr.left)) { if (rhsClassval?.kind === "classval" && !lowerer.caughtLocalOf(expr.left)) {
const targetInfo = lowerer.classes.get(rhsClassval.className); const targetInfo = lowerer.classes.get(rhsClassval.className);
if (!targetInfo) { if (!targetInfo) {
@@ -8013,7 +8013,8 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
} }
const lhsInfo = lowerer.classes.get(left.type.className); const lhsInfo = lowerer.classes.get(left.type.className);
if (!lhsInfo) throw new InternalCompilerError(`lowerer bug: unknown class ${left.type.className}`); if (!lhsInfo) throw new InternalCompilerError(`lowerer bug: unknown class ${left.type.className}`);
if (lowerer.inHierarchy(targetInfo) && lowerer.inHierarchy(lhsInfo)) { if ((lowerer.inHierarchy(targetInfo) && lowerer.inHierarchy(lhsInfo)) ||
(targetInfo.localClass !== undefined && lhsInfo.localClass !== undefined)) {
const classValue = lowerer.lowerExpr(expr.right); const classValue = lowerer.lowerExpr(expr.right);
if (classValue.type.kind !== "classval") lowerer.badType(expr.right, lowerer.typeOf(expr.right)); if (classValue.type.kind !== "classval") lowerer.badType(expr.right, lowerer.typeOf(expr.right));
return { kind: "instanceOfValue", value: left, classValue, type: BOOL, loc }; return { kind: "instanceOfValue", value: left, classValue, type: BOOL, loc };
@@ -106,6 +106,7 @@ import { FileParts, splitFiles, collectProgram, collectNpmImports, collectJsonIm
import { prepareCjsModuleGraph } from "./lower-node-module.js"; import { prepareCjsModuleGraph } from "./lower-node-module.js";
import { ClassInfo, ClassIteratorInfo, GenericClassInfo, registerBuiltinErrorClasses, registerBuiltinEmitterClass, registerBuiltinStreamClasses, builtinErrorInfoOf, builtinEmitterInfoOf, builtinStreamInfoOf, analyzeClassDecoration, classIteratorDrainCall, classIteratorNextCall, classIteratorOf, classIteratorOpenCall, classIteratorRestDrainCall, classMemberNameOf, classValueRef, collectClassShape, exactClassOfReceiver, collectClassShapeInner, ctorAbiEquals, findMethodOn, findStaticOn, findGenericMethodOn, findGenericStaticOn, genericClassInstanceType, isSubclassOf, inHierarchy, overrideBelow, staticShadowBelow, upcastTo, lowerClassMembers, lowerClassCtor, lowerClassExpression, lowerClassExpressionInfo, lowerClassMethodMember, lowerClassValueProperty, lowerStaticMethod, throwingSetterFn, fieldInitStmts, lowerStaticFieldInits, lowerStaticFieldRead, lowerDerivedCtorBody, superCallStmt, lowerSuperMethodCall, superThisRef, lowerSuperAccessorRead, lowerSuperAccessorWrite, inheritsBuiltinErrorCtor, inheritsBuiltinEmitterCtor, errorConstructorArgs, lowerNew, accessorCall } from "./lower-classes.js"; import { ClassInfo, ClassIteratorInfo, GenericClassInfo, registerBuiltinErrorClasses, registerBuiltinEmitterClass, registerBuiltinStreamClasses, builtinErrorInfoOf, builtinEmitterInfoOf, builtinStreamInfoOf, analyzeClassDecoration, classIteratorDrainCall, classIteratorNextCall, classIteratorOf, classIteratorOpenCall, classIteratorRestDrainCall, classMemberNameOf, classValueRef, collectClassShape, exactClassOfReceiver, collectClassShapeInner, ctorAbiEquals, findMethodOn, findStaticOn, findGenericMethodOn, findGenericStaticOn, genericClassInstanceType, isSubclassOf, inHierarchy, overrideBelow, staticShadowBelow, upcastTo, lowerClassMembers, lowerClassCtor, lowerClassExpression, lowerClassExpressionInfo, lowerClassMethodMember, lowerClassValueProperty, lowerStaticMethod, throwingSetterFn, fieldInitStmts, lowerStaticFieldInits, lowerStaticFieldRead, lowerDerivedCtorBody, superCallStmt, lowerSuperMethodCall, superThisRef, lowerSuperAccessorRead, lowerSuperAccessorWrite, inheritsBuiltinErrorCtor, inheritsBuiltinEmitterCtor, errorConstructorArgs, lowerNew, accessorCall } from "./lower-classes.js";
import { MixinFnShape, mixinCallClassInfoOf, mixinIntersectionInstanceType } from "./lower-mixins.js"; import { MixinFnShape, mixinCallClassInfoOf, mixinIntersectionInstanceType } from "./lower-mixins.js";
import { implicitAnyParamSymbolsOf } from "./lower-calls.js";
import { ParamShape, FnSig, GenericFnInfo, GenericInstance, bindingNeverReassigned, bodyReadsArguments, funcTypeFromParamShapes, implicitMonoFile, isThisParameter, paramShape, paramShapes, checkDefaultParamBodyType, completeArgs, wrappedUndefined, undefinedArgFor, requireExactArityValue, bodyReturnType, declaredReturnType, collectSignature, collectSignatureInner, collectGenericSignature, genericFnOf, lowerGenericCall, lowerGenericFnValue, inferTypeParamBindings, lowerGenericInstance, lowerCall, lowerFfiCall, lowerTimersMemberCall, lowerPromiseMethodCall, lowerFilterNarrowCall, isTopLevelFnSymbol, lowerNestedFunctionDecl, lambdaSignature, lowerLambda, lowerFunction, validateFfiImports } from "./lower-calls.js"; import { ParamShape, FnSig, GenericFnInfo, GenericInstance, bindingNeverReassigned, bodyReadsArguments, funcTypeFromParamShapes, implicitMonoFile, isThisParameter, paramShape, paramShapes, checkDefaultParamBodyType, completeArgs, wrappedUndefined, undefinedArgFor, requireExactArityValue, bodyReturnType, declaredReturnType, collectSignature, collectSignatureInner, collectGenericSignature, genericFnOf, lowerGenericCall, lowerGenericFnValue, inferTypeParamBindings, lowerGenericInstance, lowerCall, lowerFfiCall, lowerTimersMemberCall, lowerPromiseMethodCall, lowerFilterNarrowCall, isTopLevelFnSymbol, lowerNestedFunctionDecl, lambdaSignature, lowerLambda, lowerFunction, validateFfiImports } from "./lower-calls.js";
import { lowerArrayMethodCall, lowerMapMethodCall, lowerMapForEachCall, buildMapForEachFn, lowerRecordOvfCaptureHelper, lowerEnvToPairsHelper, lowerSetMethodCall, lowerSetForEachCall, buildSetForEachFn } from "./lower-containers.js"; import { lowerArrayMethodCall, lowerMapMethodCall, lowerMapForEachCall, buildMapForEachFn, lowerRecordOvfCaptureHelper, lowerEnvToPairsHelper, lowerSetMethodCall, lowerSetForEachCall, buildSetForEachFn } from "./lower-containers.js";
import { lowerBufferStaticCall, lowerBytesMethodCall, lowerBytesNew } from "./containers/bytes.js"; import { lowerBufferStaticCall, lowerBytesMethodCall, lowerBytesNew } from "./containers/bytes.js";
@@ -1201,6 +1202,7 @@ export class Lowerer {
/** Non-null while an instance body lowers: type-parameter symbol → /** Non-null while an instance body lowers: type-parameter symbol →
* concrete IR type, consulted inside mapType's recursion. */ * concrete IR type, consulted inside mapType's recursion. */
typeParamBindings: Map<ts.Symbol, IrType> | null = null; typeParamBindings: Map<ts.Symbol, IrType> | null = null;
localClassInstantiations: { owner: ts.FunctionLikeDeclaration; name: string }[] = [];
/** The ts-level twin of typeParamBindings, non-null while a CALL-keyed /** The ts-level twin of typeParamBindings, non-null while a CALL-keyed
* instance body lowers: type-parameter symbol → the bound CHECKER type, * instance body lowers: type-parameter symbol → the bound CHECKER type,
* consulted where the mapped IrType has already widened away information * consulted where the mapped IrType has already widened away information
@@ -1936,6 +1938,17 @@ export class Lowerer {
this.mixinTypeContext && this.mixinTypeContext.classNode === decl this.mixinTypeContext && this.mixinTypeContext.classNode === decl
? { kind: "object", className: this.mixinTypeContext.className } ? { kind: "object", className: this.mixinTypeContext.className }
: null, : null,
localClassInstance: (decl) => {
for (let parent: ts.Node | undefined = decl.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) {
if (!ts.isFunctionDeclaration(parent) && !ts.isFunctionExpression(parent) &&
!ts.isArrowFunction(parent) && !ts.isMethodDeclaration(parent)) continue;
if (this.localClassInstantiations.some((entry) => entry.owner === parent)) break;
if (parent.typeParameters?.length || (implicitMonoFile(parent.getSourceFile()) && implicitAnyParamSymbolsOf(this, parent))) return null;
}
const className = this.classNamer(decl);
if (!this.collectingExprClasses.has(decl)) this.lowerClassExpressionInfo(decl);
return { kind: "object", className };
},
mixinIntersectionInstance: (widened) => mixinIntersectionInstanceType(this, widened), mixinIntersectionInstance: (widened) => mixinIntersectionInstanceType(this, widened),
isStdlibFile: this.isStdlibFile, isStdlibFile: this.isStdlibFile,
isNpmFile: this.isNpmFile, isNpmFile: this.isNpmFile,
@@ -2023,10 +2036,18 @@ export class Lowerer {
* builds (no counter can drift between invocations), * builds (no counter can drift between invocations),
* program-unique through the file qualifier, and collision-free with * program-unique through the file qualifier, and collision-free with
* user identifiers ('%'). */ * user identifiers ('%'). */
readonly classNamer = (decl: ts.ClassLikeDeclaration): string => readonly classNamer = (decl: ts.ClassLikeDeclaration): string => {
ts.isClassExpression(decl) const name = ts.isClassExpression(decl)
? this.qualify(decl.getSourceFile(), `%cx${decl.getStart()}.${decl.name?.text ?? ""}`) ? this.qualify(decl.getSourceFile(), `%cx${decl.getStart()}.${decl.name?.text ?? ""}`)
: this.qualify(decl.getSourceFile(), nsPathPrefix(decl) + (decl.name ? decl.name.text : "%anon")); : this.qualify(decl.getSourceFile(), nsPathPrefix(decl) + (decl.name ? decl.name.text : "%anon"));
for (let index = this.localClassInstantiations.length - 1; index >= 0; index--) {
const entry = this.localClassInstantiations[index]!;
for (let parent: ts.Node | undefined = decl.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) {
if (parent === entry.owner) return `${name}%${entry.name}`;
}
}
return name;
};
/** Follows import aliases to the original declaration's symbol. Every /** Follows import aliases to the original declaration's symbol. Every
* value reference resolves through here, so it doubles as the flush * value reference resolves through here, so it doubles as the flush
@@ -3660,7 +3681,7 @@ export class Lowerer {
if (typeof catchId === "string") referenced.add(catchId); if (typeof catchId === "string") referenced.add(catchId);
// Closure captures name their source locals as PLAIN STRINGS // Closure captures name their source locals as PLAIN STRINGS
// (captures: string[]) — a captured broken-class local is live. // (captures: string[]) — a captured broken-class local is live.
if (rec["kind"] === "closure" && Array.isArray(rec["captures"])) { if ((rec["kind"] === "closure" || rec["kind"] === "classRef") && Array.isArray(rec["captures"])) {
for (const c of rec["captures"]) if (typeof c === "string") referenced.add(c); for (const c of rec["captures"]) if (typeof c === "string") referenced.add(c);
} }
for (const key of Object.keys(rec)) scan(rec[key]); for (const key of Object.keys(rec)) scan(rec[key]);
@@ -3683,7 +3704,7 @@ export class Lowerer {
for (const fn of brokenLocalFns) { for (const fn of brokenLocalFns) {
// Params and captures list their locals by id too — never prune // Params and captures list their locals by id too — never prune
// those out from under them. // those out from under them.
const referenced = referencedIn([fn.body, fn.params, fn.captures ?? []]); const referenced = referencedIn([fn.body, fn.params, fn.captures ?? [], fn.classCaptures ?? []]);
fn.locals = fn.locals.filter( fn.locals = fn.locals.filter(
(l) => referenced.has(l.id) || !this.typeNamesUnregisteredClass(l.type), (l) => referenced.has(l.id) || !this.typeNamesUnregisteredClass(l.type),
); );
@@ -8588,7 +8609,7 @@ export class Lowerer {
* members ride fp.classDecls; expressions register only when their * members ride fp.classDecls; expressions register only when their
* containing statement lowers). */ * containing statement lowers). */
readonly exprClasses: ClassInfo[] = []; readonly exprClasses: ClassInfo[] = [];
readonly exprClassInfoByNode = new Map<ts.ClassExpression, ClassInfo>(); readonly exprClassInfoByName = new Map<string, ClassInfo>();
/** Class expressions whose collection is IN FLIGHT — the reentrancy /** Class expressions whose collection is IN FLIGHT — the reentrancy
* guard for heritage-demanded collection (lowerClassExpressionInfo). */ * guard for heritage-demanded collection (lowerClassExpressionInfo). */
readonly collectingExprClasses = new Set<ts.ClassExpression>(); readonly collectingExprClasses = new Set<ts.ClassExpression>();
+17 -11
View File
@@ -794,6 +794,7 @@ export interface TypeMapperCtx {
* inside members, self-referential member types), null outside any * inside members, self-referential member types), null outside any
* mixin context (the type alone cannot name a call site). */ * mixin context (the type alone cannot name a call site). */
mixinClassInstance?: (decl: ts.ClassLikeDeclaration) => IrType | null; mixinClassInstance?: (decl: ts.ClassLikeDeclaration) => IrType | null;
localClassInstance?: (decl: ts.ClassExpression) => IrType | null;
/** MIXIN instance INTERSECTIONS (`Tagged.C & Derived` — values built /** MIXIN instance INTERSECTIONS (`Tagged.C & Derived` — values built
* through a mixin result): resolved by chain structure to the unique * through a mixin result): resolved by chain structure to the unique
* pinned instantiation they describe; null when ambiguous or when no * pinned instantiation they describe; null when ambiguous or when no
@@ -938,13 +939,9 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
} }
} }
/** True when a class expression can NEVER register a lowering: one /** Local class types need the enclosing specialization's registered layout.
* enclosed by a function-like body or a class static block mints a * Static-block classes are sent through the same hook for an explicit fence. */
* DISTINCT class per evaluation, which lowerClassExpressionInfo always function classExprNeedsContext(decl: ts.ClassLikeDeclaration): boolean {
* fences. Its instance/static types must stay UNMAPPED — an object type
* naming a struct that will never be emitted is the invalid-C escape
* family (every SITE using the value already carries its own fence). */
function classExprNeverRegisters(decl: ts.ClassLikeDeclaration): boolean {
if (!ts.isClassExpression(decl)) return false; if (!ts.isClassExpression(decl)) return false;
for (let p: ts.Node | undefined = decl.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) { for (let p: ts.Node | undefined = decl.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) {
if (ts.isFunctionLike(p) || ts.isClassStaticBlockDeclaration(p)) return true; if (ts.isFunctionLike(p) || ts.isClassStaticBlockDeclaration(p)) return true;
@@ -1420,9 +1417,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
return viaMixin; return viaMixin;
} }
} }
// A class expression inside a function/static block never registers if (classExprNeedsContext(classDecl)) {
// (a distinct class per evaluation): unmappable for the same reason. const local = ts.isClassExpression(classDecl) ? ctx.localClassInstance?.(classDecl) : null;
if (classExprNeverRegisters(classDecl)) return null; if (local) contextResolutions++;
return local ?? null;
}
// A GENERIC class's instance type (`Box<number>`) maps to the concrete // A GENERIC class's instance type (`Box<number>`) maps to the concrete
// INSTANTIATION's class (`Box%0`), registered on demand — the Lowerer // INSTANTIATION's class (`Box%0`), registered on demand — the Lowerer
// hook owns the instance table (monomorphization by flow). // hook owns the instance table (monomorphization by flow).
@@ -1457,7 +1456,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
return { kind: "classval", className: viaMixin.className }; return { kind: "classval", className: viaMixin.className };
} }
} }
if (classExprNeverRegisters(classDecl)) return null; if (classExprNeedsContext(classDecl)) {
const local = ts.isClassExpression(classDecl) ? ctx.localClassInstance?.(classDecl) : null;
if (local?.kind === "object") {
contextResolutions++;
return { kind: "classval", className: local.className };
}
return null;
}
// A GENERIC class's static side: only an INSTANTIATED one maps — an // A GENERIC class's static side: only an INSTANTIATED one maps — an
// instantiation expression's type (`Box<number>` as a value) carries a // instantiation expression's type (`Box<number>` as a value) carries a
// construct signature returning the concrete instance, which maps to // construct signature returning the concrete instance, which maps to
+1
View File
@@ -80,6 +80,7 @@ export function dynDesc(
return def.arms.map((arm) => dynDesc(arm, recordsById, unionsById)).join(" | "); return def.arms.map((arm) => dynDesc(arm, recordsById, unionsById)).join(" | ");
} }
case "func": return "function"; case "func": return "function";
case "classval": return "class constructor";
case "map": return "Map"; case "map": return "Map";
case "set": return "Set"; case "set": return "Set";
default: { default: {
+2 -2
View File
@@ -50,8 +50,8 @@ export function findConstantNumericTables(mod: IrModule): ReadonlyMap<string, Co
function expr(node: IrExpr): boolean { function expr(node: IrExpr): boolean {
switch (node.kind) { switch (node.kind) {
case "closure": case "closure": case "classRef":
for (const id of node.captures) reject(id); for (const id of node.captures ?? []) reject(id);
break; break;
case "arrIntrinsic": { case "arrIntrinsic": {
const candidate = candidateFor(node.receiver); const candidate = candidateFor(node.receiver);
+1 -1
View File
@@ -15,7 +15,7 @@ const UNSIGNED: IntegerRange = { min: 0, max: 4294967295 };
export function analyzeIntegerRanges(fn: IrFunction): IntegerRanges { export function analyzeIntegerRanges(fn: IrFunction): IntegerRanges {
const ranges = new Map<IrExpr, IntegerRange | null>(); const ranges = new Map<IrExpr, IntegerRange | null>();
if (fn.async || fn.generator) return ranges; if (fn.async || fn.generator) return ranges;
const captures = new Set((fn.captures ?? []).map((c) => c.localId)); const captures = new Set([...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId));
const eligible = new Set(fn.locals.filter((l) => l.type.kind === "f64" && !l.boxed && !l.tdz && !captures.has(l.id)).map((l) => l.id)); const eligible = new Set(fn.locals.filter((l) => l.type.kind === "f64" && !l.boxed && !l.tdz && !captures.has(l.id)).map((l) => l.id));
type Facts = Map<string, IntegerRange>; type Facts = Map<string, IntegerRange>;
+24 -14
View File
@@ -239,11 +239,9 @@ export type IrType =
| { kind: "object"; className: string } // heap, refcounted class instance | { kind: "object"; className: string } // heap, refcounted class instance
/** The class STATIC side as a value — `typeof C`, the type of the class /** The class STATIC side as a value — `typeof C`, the type of the class
* name itself and of `new (…) => T` constructor-typed slots. Runtime * name itself and of `new (…) => T` constructor-typed slots. Runtime
* representation is the per-class IMMORTAL class object (ScrClassObj: * representation is ScrClassObj: an immortal top-level template or a fresh
* preorder interval, construct thunk, .name string) emitted once per * function-local object owning captured binding boxes. Identity is one
* classRef-referenced class — so identity `===` is one pointer compare * pointer comparison; local instances retain their constructor object.
* and retains/releases are no-ops on the immortal (the regex-literal
* discipline; isRefCounted says true for container/RC uniformity).
* Values of `classval:C` are C's class object or a STRICT DESCENDANT's — * Values of `classval:C` are C's class object or a STRICT DESCENDANT's —
* the object kind's nominal, upcast-only story — and every legal flow * the object kind's nominal, upcast-only story — and every legal flow
* preserves the constructor ABI (upcast requires the descendant's * preserves the constructor ABI (upcast requires the descendant's
@@ -251,7 +249,8 @@ export type IrType =
* completion against C's one signature sound. Allowed in locals, * completion against C's one signature sound. Allowed in locals,
* globals, params, returns, class/record fields, capture boxes, array * globals, params, returns, class/record fields, capture boxes, array
* elements, Map VALUES, and union arms; fenced out of Map keys, Set * elements, Map VALUES, and union arms; fenced out of Map keys, Set
* elements, JSON, dyn/jsval conversion, and ToString. */ * elements, JSON, jsval conversion, and ToString. Unknown slots retain
* class identity and allow an exact typed constructor round trip. */
| { kind: "classval"; className: string } | { kind: "classval"; className: string }
/** An ECMAScript module namespace object for one statically-known module. /** An ECMAScript module namespace object for one statically-known module.
* `moduleId` is a canonical compiled source-file or builtin identity. * `moduleId` is a canonical compiled source-file or builtin identity.
@@ -1117,6 +1116,10 @@ export interface IrLibIdentity {
export interface IrClassDef { export interface IrClassDef {
name: string; name: string;
/** Present for a class evaluated inside a function. Each evaluation owns
* fresh identity and these shared binding boxes; instance layouts carry a
* private class-object pointer before their source fields. */
localCaptures?: IrParam[];
/** The JS-observable `.name` of the class (the runtime class object's /** The JS-observable `.name` of the class (the runtime class object's
* name string, and what `C.name` folds to). Differs from `name` because * name string, and what `C.name` folds to). Differs from `name` because
* IR names are program-qualified (`%m1.C`, `%cx…` for class * IR names are program-qualified (`%m1.C`, `%cx…` for class
@@ -1124,6 +1127,8 @@ export interface IrClassDef {
* binding name for `const x = class {}`, or "" for truly anonymous * binding name for `const x = class {}`, or "" for truly anonymous
* expressions. Absent on the runtime-provided defs (never valuable). */ * expressions. Absent on the runtime-provided defs (never valuable). */
jsName?: string; jsName?: string;
/** JavaScript constructor arity, ending before the first default or rest parameter. */
jsLength?: number;
/** RUNTIME-PROVIDED class (the builtin Error hierarchy): the struct, RC /** RUNTIME-PROVIDED class (the builtin Error hierarchy): the struct, RC
* helpers, and vtable live in the runtime (ScrError / scr_error_*), so * helpers, and vtable live in the runtime (ScrError / scr_error_*), so
* backends emit no definitions for it — only the preorder-interval * backends emit no definitions for it — only the preorder-interval
@@ -1340,6 +1345,10 @@ export interface IrFunction {
* variables received through the closure environment, in caps[] order. * variables received through the closure environment, in caps[] order.
* Each is also listed in `locals` (with boxed: true); it is NOT a param. */ * Each is also listed in `locals` (with boxed: true); it is NOT a param. */
captures?: IrParam[]; captures?: IrParam[];
/** Instance methods/constructors of local classes borrow captures through
* their first (`this`) parameter. The slot indexes the class object's
* capture array; no closure parameter is added to the method ABI. */
classCaptures?: (IrParam & { slot: number })[];
/** Async: the body runs on a fiber; `returnType` is the INNER type T (a /** Async: the body runs on a fiber; `returnType` is the INNER type T (a
* `return v` fulfills with v) while ordinary call sites receive * `return v` fulfills with v) while ordinary call sites receive
* Promise<T>. With `generator` present, call sites receive the lazy * Promise<T>. With `generator` present, call sites receive the lazy
@@ -4744,8 +4753,7 @@ export type IrExpr =
/** Numeric operands; comparisons yield bool. `===`/`!==` additionally /** Numeric operands; comparisons yield bool. `===`/`!==` additionally
* accept two same-typed arrays: reference identity (pointer compare), * accept two same-typed arrays: reference identity (pointer compare),
* matching JS object equality — and two same-typed CLASS VALUES, where * matching JS object equality — and two same-typed CLASS VALUES, where
* the pointer compare IS class identity (one immortal object per * the pointer compare IS class identity. */
* class). */
| { kind: "bin"; op: IrNumBinOp; left: IrExpr; right: IrExpr; type: IrType; loc: SrcLoc } | { kind: "bin"; op: IrNumBinOp; left: IrExpr; right: IrExpr; type: IrType; loc: SrcLoc }
/** `~` is JS bitwise NOT: ToInt32 the operand, complement, back to f64. */ /** `~` is JS bitwise NOT: ToInt32 the operand, complement, back to f64. */
| { kind: "unary"; op: "-" | "!" | "~"; operand: IrExpr; type: IrType; loc: SrcLoc } | { kind: "unary"; op: "-" | "!" | "~"; operand: IrExpr; type: IrType; loc: SrcLoc }
@@ -5140,14 +5148,13 @@ export type IrExpr =
* with the new object as arg 0 (retained — the ctor owns and releases its * with the new object as arg 0 (retained — the ctor owns and releases its
* `this` param like any callee). Result is owned (+1). */ * `this` param like any callee). Result is owned (+1). */
| { kind: "new"; className: string; args: IrExpr[]; type: IrType; loc: SrcLoc } | { kind: "new"; className: string; args: IrExpr[]; type: IrType; loc: SrcLoc }
/** The class itself as a value: a pointer to `className`'s immortal /** The class itself as an owned value. `captures` creates fresh identity
* class object (type `classval:className`, +1 — a no-op retain on the * and retains the named boxes; absent captures use an immortal template.
* immortal, kept for the uniform owned-temp discipline, the regexLit * The frontend notes an edge to `%className.constructor` at
* pattern). The frontend notes an edge to `%className.constructor` at
* every classRef, so a value's construct thunk always has a constructor * every classRef, so a value's construct thunk always has a constructor
* to call; backends emit class objects (and thunks) for exactly the * to call; backends emit class objects (and thunks) for exactly the
* classes some classRef in the module names. */ * classes some classRef in the module names. */
| { kind: "classRef"; className: string; type: IrType; loc: SrcLoc } | { kind: "classRef"; className: string; captures?: string[]; type: IrType; loc: SrcLoc }
/** `new X(args)` through a class VALUE: call the class object's /** `new X(args)` through a class VALUE: call the class object's
* construct thunk. `callee` is classval-typed; args are completed * construct thunk. `callee` is classval-typed; args are completed
* against `%<callee.className>.constructor`'s ABI — sound because every * against `%<callee.className>.constructor`'s ABI — sound because every
@@ -6208,6 +6215,7 @@ export function canConvertToDyn(
// listener boundary (a mustCall-wrapped handler receiving the payload). // listener boundary (a mustCall-wrapped handler receiving the payload).
if (t.kind === "object" && t.className === "%Error") return true; if (t.kind === "object" && t.className === "%Error") return true;
if (isDynTypedRefType(t)) return true; if (isDynTypedRefType(t)) return true;
if (t.kind === "classval") return true;
if (t.kind === "func") return canBoxFuncIntoDyn(t, getRecord, getUnion); if (t.kind === "func") return canBoxFuncIntoDyn(t, getRecord, getUnion);
if (DYN_HANDLE_KINDS.has(t.kind)) return true; if (DYN_HANDLE_KINDS.has(t.kind)) return true;
// Promises box by REFERENCE (SCR_DYN_PROMISE): promise<dyn> carries its // Promises box by REFERENCE (SCR_DYN_PROMISE): promise<dyn> carries its
@@ -6231,7 +6239,7 @@ export function canConvertToDyn(
// boundary exactly like a bare func dynFrom). // boundary exactly like a bare func dynFrom).
return !!def && def.arms.every((a) => return !!def && def.arms.every((a) =>
a.kind === "undefinedT" || isJsonSafeType(a, getRecord, getUnion) || a.kind === "undefinedT" || isJsonSafeType(a, getRecord, getUnion) ||
isDynTypedRefType(a) || DYN_HANDLE_KINDS.has(a.kind) || isDynTypedRefType(a) || a.kind === "classval" || DYN_HANDLE_KINDS.has(a.kind) ||
(a.kind === "func" && canBoxFuncIntoDyn(a, getRecord, getUnion)) || (a.kind === "func" && canBoxFuncIntoDyn(a, getRecord, getUnion)) ||
(a.kind === "promise" && canConvertToDyn(a, getRecord, getUnion)), (a.kind === "promise" && canConvertToDyn(a, getRecord, getUnion)),
); );
@@ -6258,6 +6266,7 @@ function canBoxDynComposite(
case "dyn": case "dyn":
case "undefinedT": case "undefinedT":
case "nullT": case "nullT":
case "classval":
return true; return true;
case "bytes": case "bytes":
return t.elem === "u8"; return t.elem === "u8";
@@ -6304,6 +6313,7 @@ export function canDynCheckTo(
// unknown record fields with the undefined value. // unknown record fields with the undefined value.
if (isJsonSafeAt(t, getRecord, getUnion, false, false, new Set(), true)) return true; if (isJsonSafeAt(t, getRecord, getUnion, false, false, new Set(), true)) return true;
if (t.kind === "bytes" && t.elem === "u8") return true; if (t.kind === "bytes" && t.elem === "u8") return true;
if (t.kind === "classval") return true;
if (t.kind === "object" && t.className === "%Error") return true; if (t.kind === "object" && t.className === "%Error") return true;
if (t.kind === "func") return canAdaptDynFuncTo(t, getRecord, getUnion); if (t.kind === "func") return canAdaptDynFuncTo(t, getRecord, getUnion);
if (DYN_HANDLE_KINDS.has(t.kind)) return true; if (DYN_HANDLE_KINDS.has(t.kind)) return true;
+3 -3
View File
@@ -14,7 +14,7 @@ function scalarTemporaries(value: IrExpr): boolean {
} }
function producer(fn: IrFunction, shapes: ReadonlyMap<string, IrRecordShape>): Producer | null { function producer(fn: IrFunction, shapes: ReadonlyMap<string, IrRecordShape>): Producer | null {
if (fn.async || fn.generator || fn.captures?.length || fn.returnType.kind !== "record") return null; if (fn.async || fn.generator || fn.captures?.length || fn.classCaptures?.length || fn.returnType.kind !== "record") return null;
const shape = shapes.get(fn.returnType.shapeId); const shape = shapes.get(fn.returnType.shapeId);
if (!shape || shape.tuple || shape.indexValue || shape.fields.length === 0 || shape.fields.length > MAX_FIELDS || if (!shape || shape.tuple || shape.indexValue || shape.fields.length === 0 || shape.fields.length > MAX_FIELDS ||
shape.fields.some((f) => f.type.kind !== "f64")) return null; shape.fields.some((f) => f.type.kind !== "f64")) return null;
@@ -58,8 +58,8 @@ function fieldOnlyUses(fn: IrFunction, localId: string, shape: IrRecordShape): b
case "varRef": case "incDec": case "assignExpr": case "varRef": case "incDec": case "assignExpr":
if (node.localId === localId) return false; if (node.localId === localId) return false;
break; break;
case "closure": case "closure": case "classRef":
if (node.captures.includes(localId)) return false; if (node.captures?.includes(localId)) return false;
break; break;
} }
return everyExprChild(node, expr, stmt); return everyExprChild(node, expr, stmt);
+34
View File
@@ -5,6 +5,40 @@ import { validateModule } from "./validate.js";
const loc = { file: "numeric-read.ts", start: 0, end: 0 }; const loc = { file: "numeric-read.ts", start: 0, end: 0 };
function localClassModule(): IrModule {
const self: IrType = { kind: "object", className: "Local" };
const mod = expressionModule({ kind: "classRef", className: "Local", captures: ["outer"], type: { kind: "classval", className: "Local" }, loc }, []);
mod.classes = [{ name: "Local", jsName: "Local", fields: [], localCaptures: [{ localId: "shared", name: "value", type: F64 }], loc }];
mod.functions[0]!.locals = [{ id: "outer", name: "value", type: F64, mutable: true, boxed: true }];
mod.functions.push({
name: "%Local.constructor", params: [{ localId: "self", name: "this", type: self }],
locals: [{ id: "self", name: "this", type: self, mutable: false }, { id: "capture", name: "value", type: F64, mutable: true, boxed: true }],
classCaptures: [{ localId: "capture", name: "value", type: F64, slot: 0 }],
returnType: VOID, body: [], loc,
});
return mod;
}
test("local classes retain serialized capture slots and fresh identity", () => {
const mod = localClassModule();
expect(validateModule(mod)).toEqual([]);
expect(deserializeModule(serializeModule(mod))).toEqual(mod);
});
test.each(["missing", "unboxed", "type", "slot", "receiver", "closure", "layout", "direct-new"])("local classes reject an invalid %s environment", (variant) => {
const mod = localClassModule();
const ctor = mod.functions[1]!;
if (variant === "missing") mod.functions[0]!.locals = [];
if (variant === "unboxed") delete mod.functions[0]!.locals[0]!.boxed;
if (variant === "type") ctor.locals[1]!.type = STRING;
if (variant === "slot") ctor.classCaptures![0]!.slot = 1;
if (variant === "receiver") ctor.params[0]!.type = F64;
if (variant === "closure") ctor.captures = [];
if (variant === "layout") mod.classes![0]!.runtime = true;
if (variant === "direct-new") mod.functions[0]!.body = [{ kind: "exprStmt", expr: { kind: "new", className: "Local", args: [], type: { kind: "object", className: "Local" }, loc }, loc }];
expect(validateModule(mod).length).toBeGreaterThan(0);
});
test.each([mapOf(STRING, F64), setOf(STRING), { kind: "promise", inner: F64 } as IrType])("nullable %j payloads preserve an explicit absence tag", (type) => { test.each([mapOf(STRING, F64), setOf(STRING), { kind: "promise", inner: F64 } as IrType])("nullable %j payloads preserve an explicit absence tag", (type) => {
const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [ const mod = expressionModule({ kind: "numLit", value: 0, type: F64, loc }, [
{ id: "nullable", arms: [type, NULL_T, UNDEFINED_T] }, { id: "nullable", arms: [type, NULL_T, UNDEFINED_T] },
+38 -1
View File
@@ -1624,6 +1624,12 @@ export function validateModule(mod: IrModule): IrValidationError[] {
errors.push({ message: `duplicate class "${cls.name}"`, loc: cls.loc }); errors.push({ message: `duplicate class "${cls.name}"`, loc: cls.loc });
} }
classesByName.set(cls.name, cls); classesByName.set(cls.name, cls);
if (cls.localCaptures !== undefined && (cls.runtime || cls.base !== undefined || cls.genericOf !== undefined)) {
errors.push({ message: `class ${cls.name}: local class cannot use a runtime or inherited layout`, loc: cls.loc });
}
if (cls.jsLength !== undefined && (!Number.isSafeInteger(cls.jsLength) || cls.jsLength < 0)) {
errors.push({ message: `class ${cls.name}: invalid constructor length`, loc: cls.loc });
}
const seen = new Set<string>(); const seen = new Set<string>();
for (const f of cls.fields) { for (const f of cls.fields) {
if (seen.has(f.name)) { if (seen.has(f.name)) {
@@ -1664,6 +1670,9 @@ export function validateModule(mod: IrModule): IrValidationError[] {
errors.push({ message: `class ${cls.name}: undeclared base "${cls.base}"`, loc: cls.loc }); errors.push({ message: `class ${cls.name}: undeclared base "${cls.base}"`, loc: cls.loc });
continue; continue;
} }
if (base.localCaptures !== undefined) {
errors.push({ message: `class ${cls.name}: cannot extend a local class`, loc: cls.loc });
}
const seen = new Set<string>([cls.name]); const seen = new Set<string>([cls.name]);
for (let c: IrClassDef | undefined = base; c; c = c.base !== undefined ? classesByName.get(c.base) : undefined) { for (let c: IrClassDef | undefined = base; c; c = c.base !== undefined ? classesByName.get(c.base) : undefined) {
if (seen.has(c.name)) { if (seen.has(c.name)) {
@@ -2065,12 +2074,26 @@ function validateFunction(
} }
if (p.type.kind === "caught") err(`param "${p.name}" is caught-typed`, fn.loc); if (p.type.kind === "caught") err(`param "${p.name}" is caught-typed`, fn.loc);
} }
for (const c of fn.captures ?? []) { for (const c of [...(fn.captures ?? []), ...(fn.classCaptures ?? [])]) {
const local = locals.get(c.localId); const local = locals.get(c.localId);
if (!local) err(`capture "${c.name}" has no local entry "${c.localId}"`, fn.loc); if (!local) err(`capture "${c.name}" has no local entry "${c.localId}"`, fn.loc);
else if (!local.boxed) err(`capture local "${c.localId}" is not boxed`, fn.loc); else if (!local.boxed) err(`capture local "${c.localId}" is not boxed`, fn.loc);
else if (!typeEquals(local.type, c.type)) err(`capture local "${c.localId}" has the wrong type`, fn.loc);
if (c.type.kind === "caught") err(`capture "${c.name}" is caught-typed`, fn.loc); if (c.type.kind === "caught") err(`capture "${c.name}" is caught-typed`, fn.loc);
} }
if (fn.classCaptures !== undefined) {
const self = fn.params[0];
const cls = self?.type.kind === "object" ? classes.get(self.type.className) : undefined;
if (fn.captures !== undefined || cls?.localCaptures === undefined) {
err("class captures require a local-class instance receiver and no closure environment", fn.loc);
}
for (const capture of fn.classCaptures) {
const slot = cls?.localCaptures?.[capture.slot];
if (!Number.isInteger(capture.slot) || !slot || !typeEquals(slot.type, capture.type)) {
err(`class capture "${capture.name}" has an invalid slot or type`, fn.loc);
}
}
}
const expectType = (expr: IrExpr, want: IrType, what: string) => { const expectType = (expr: IrExpr, want: IrType, what: string) => {
if (!typeEquals(expr.type, want)) { if (!typeEquals(expr.type, want)) {
@@ -3241,6 +3264,7 @@ function validateFunction(
break; break;
} }
const ctor = functions.get(`%${e.className}.constructor`); const ctor = functions.get(`%${e.className}.constructor`);
if (cls.localCaptures !== undefined) err(`new ${e.className}: local class requires a class value`, e.loc);
if (!ctor) { if (!ctor) {
err(`new ${e.className}: missing constructor function`, e.loc); err(`new ${e.className}: missing constructor function`, e.loc);
break; break;
@@ -3349,6 +3373,17 @@ function validateFunction(
if (!typeEquals(e.type, { kind: "classval", className: e.className })) { if (!typeEquals(e.type, { kind: "classval", className: e.className })) {
err(`classRef to "${e.className}" must have that classval type`, e.loc); err(`classRef to "${e.className}" must have that classval type`, e.loc);
} }
if ((e.captures === undefined) !== (cls.localCaptures === undefined) ||
(e.captures?.length ?? 0) !== (cls.localCaptures?.length ?? 0)) {
err(`classRef to "${e.className}" must supply its class captures`, e.loc);
}
(e.captures ?? []).forEach((id, index) => {
const local = locals.get(id);
const wanted = cls.localCaptures?.[index];
if (!local?.boxed || (wanted && !typeEquals(local.type, wanted.type))) {
err(`classRef capture "${id}" must name a box with the declared type`, e.loc);
}
});
break; break;
} }
case "newValue": { case "newValue": {
@@ -3389,6 +3424,8 @@ function validateFunction(
// Both sides must be hierarchy members: the operand needs a vt // Both sides must be hierarchy members: the operand needs a vt
// word to read; a standalone target class has one possible value // word to read; a standalone target class has one possible value
// and the frontend folds it statically. // and the frontend folds it statically.
if (classes.get(e.classValue.type.className)?.localCaptures !== undefined &&
e.value.type.kind === "object" && classes.get(e.value.type.className)?.localCaptures !== undefined) break;
if (!hierarchy.has(e.classValue.type.className)) { if (!hierarchy.has(e.classValue.type.className)) {
err(`instanceOfValue against standalone class "${e.classValue.type.className}"`, e.loc); err(`instanceOfValue against standalone class "${e.classValue.type.className}"`, e.loc);
} }
@@ -166,7 +166,7 @@ describe.runIf(supported)("LLVM native helper integration", () => {
expect(await symbols(helperObject, ["-gU"])) expect(await symbols(helperObject, ["-gU"]))
.toEqual(await symbols(clangObject, ["-gU"])); .toEqual(await symbols(clangObject, ["-gU"]));
expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]);
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v3"); expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v4");
const clangExe = join(dir, "clang-program"); const clangExe = join(dir, "clang-program");
const linkDriver = join(dir, "clang-driver.c"); const linkDriver = join(dir, "clang-driver.c");
@@ -460,7 +460,7 @@ describe.runIf(supported)("LLVM native helper integration", () => {
.stdout.trim().split("\n"); .stdout.trim().split("\n");
expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_scale");
expect(undefinedSymbols).toContain("_sf_callback_mix"); expect(undefinedSymbols).toContain("_sf_callback_mix");
expect(undefinedSymbols).toContain("_scr_runtime_abi_v3"); expect(undefinedSymbols).toContain("_scr_runtime_abi_v4");
}); });
test("helper and clang object paths retain the same outbound FFI C ABI", async () => { test("helper and clang object paths retain the same outbound FFI C ABI", async () => {
@@ -11103,6 +11103,48 @@
"<repo>/tests/diagnostics/intl-segmenter.ts" "<repo>/tests/diagnostics/intl-segmenter.ts"
], ],
"diags": [] "diags": []
},
"<repo>/tests/corpus/local-class-call-spreads.js": {
"order": [
"<repo>/tests/corpus/local-class-call-spreads.js"
],
"diags": []
},
"<repo>/tests/corpus/local-class-evaluation.ts": {
"order": [
"<repo>/tests/corpus/local-class-evaluation.ts"
],
"diags": []
},
"<repo>/tests/corpus/local-class-lifetimes.ts": {
"order": [
"<repo>/tests/corpus/local-class-lifetimes.ts"
],
"diags": []
},
"<repo>/tests/corpus/local-class-specialization.js": {
"order": [
"<repo>/tests/corpus/local-class-specialization.js"
],
"diags": []
},
"<repo>/tests/corpus/local-class-unknown.ts": {
"order": [
"<repo>/tests/corpus/local-class-unknown.ts"
],
"diags": []
},
"<repo>/tests/corpus/local-class-values.ts": {
"order": [
"<repo>/tests/corpus/local-class-values.ts"
],
"diags": []
},
"<repo>/tests/diagnostics/local-class-boundaries.ts": {
"order": [
"<repo>/tests/diagnostics/local-class-boundaries.ts"
],
"diags": []
} }
} }
} }
@@ -180,7 +180,7 @@ async function build() {
package: packageManifest.name, package: packageManifest.name,
version: packageManifest.version, version: packageManifest.version,
target: RUNTIME_PACK_MATRIX.target, target: RUNTIME_PACK_MATRIX.target,
runtime_abi: { version: 3, marker: "scr_runtime_abi_v3" }, runtime_abi: { version: 4, marker: "scr_runtime_abi_v4" },
compiler: { compiler: {
command: compiler, command: compiler,
identity: compilerVersion, identity: compilerVersion,
@@ -121,7 +121,7 @@ async function build() {
const licensed = [[join(runtimeRoot, "LICENSE"), "artifacts/licenses/scriptc-runtime.txt", "Apache-2.0"], [join(quickjs, "LICENSE"), "artifacts/licenses/quickjs-ng.txt", "MIT"], [join(vendorRoot, "ryu", "LICENSE-Boost"), "artifacts/licenses/ryu.txt", "BSL-1.0"], [join(zlib, "LICENSE"), "artifacts/licenses/zlib.txt", "Zlib"], [join(mbedtls, "LICENSE"), "artifacts/licenses/mbedtls.txt", "Apache-2.0"]]; const licensed = [[join(runtimeRoot, "LICENSE"), "artifacts/licenses/scriptc-runtime.txt", "Apache-2.0"], [join(quickjs, "LICENSE"), "artifacts/licenses/quickjs-ng.txt", "MIT"], [join(vendorRoot, "ryu", "LICENSE-Boost"), "artifacts/licenses/ryu.txt", "BSL-1.0"], [join(zlib, "LICENSE"), "artifacts/licenses/zlib.txt", "Zlib"], [join(mbedtls, "LICENSE"), "artifacts/licenses/mbedtls.txt", "Apache-2.0"]];
licensed.push([join(vendorRoot, "unicode", "LICENSE"), "artifacts/licenses/unicode.txt", "Unicode-3.0"]); licensed.push([join(vendorRoot, "unicode", "LICENSE"), "artifacts/licenses/unicode.txt", "Unicode-3.0"]);
await Promise.all(licensed.map(async ([source, destination]) => { const output = join(buildRoot, destination); await mkdir(dirname(output), { recursive: true }); await copyFile(source, output); })); await Promise.all(licensed.map(async ([source, destination]) => { const output = join(buildRoot, destination); await mkdir(dirname(output), { recursive: true }); await copyFile(source, output); }));
const manifest = { schema: "scriptc.runtime-pack.v1", format: 1, package: packageManifest.name, version: packageManifest.version, target: matrix.target, runtime_abi: { version: 3, marker: "scr_runtime_abi_v3" }, compiler: { command: compiler, identity: compilerVersion, target: matrix.target.llvm_triple }, macros: { executable: ["SCR_DYNAMIC", "SCR_TEXT_DECODER_LEGACY"], excluded: ["SCR_LIB", "SCR_THREAD_INSTANCES", "SCR_RC_AUDIT", "SCR_ASAN_FIBERS"], sanitizer: "external-toolchain-required" }, flavors, archives: archives.map((entry) => ({ ...entry, predicate: archiveSpecs.get(entry.id).predicate })), system_libraries: matrix.system_libraries, licenses: licensed.map(([, path, license]) => ({ path, license })) }; const manifest = { schema: "scriptc.runtime-pack.v1", format: 1, package: packageManifest.name, version: packageManifest.version, target: matrix.target, runtime_abi: { version: 4, marker: "scr_runtime_abi_v4" }, compiler: { command: compiler, identity: compilerVersion, target: matrix.target.llvm_triple }, macros: { executable: ["SCR_DYNAMIC", "SCR_TEXT_DECODER_LEGACY"], excluded: ["SCR_LIB", "SCR_THREAD_INSTANCES", "SCR_RC_AUDIT", "SCR_ASAN_FIBERS"], sanitizer: "external-toolchain-required" }, flavors, archives: archives.map((entry) => ({ ...entry, predicate: archiveSpecs.get(entry.id).predicate })), system_libraries: matrix.system_libraries, licenses: licensed.map(([, path, license]) => ({ path, license })) };
await writeFile(stagedManifestPath, `${JSON.stringify(manifest, null, 2)}\n`); await writeFile(stagedManifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
const suffix = `${process.pid}-${Math.random().toString(36).slice(2)}`; const suffix = `${process.pid}-${Math.random().toString(36).slice(2)}`;
await installRuntimePack({ outputRoot, manifestPath, stagedOutputRoot, stagedManifestPath, backupRoot: join(packageRoot, `.runtime-pack-artifacts-backup-${suffix}`), backupManifestPath: join(packageRoot, `.runtime-pack-manifest-backup-${suffix}`) }); await installRuntimePack({ outputRoot, manifestPath, stagedOutputRoot, stagedManifestPath, backupRoot: join(packageRoot, `.runtime-pack-artifacts-backup-${suffix}`), backupManifestPath: join(packageRoot, `.runtime-pack-manifest-backup-${suffix}`) });
+2 -2
View File
@@ -451,7 +451,7 @@ static bool scr_assert_dyn_same_value(const ScrDyn *a, const ScrDyn *b) {
return a->v.str->len == b->v.str->len && return a->v.str->len == b->v.str->len &&
memcmp(a->v.str->data, b->v.str->data, a->v.str->len) == 0; memcmp(a->v.str->data, b->v.str->data, a->v.str->len) == 0;
case SCR_DYN_FUNC: case SCR_DYN_FUNC:
return a == b || a->v.fn.clo == b->v.fn.clo; return scr_dyn_strict_eq(a, b);
case SCR_DYN_HANDLE: case SCR_DYN_HANDLE:
/* Identity is the HANDLE (the strict_eq stance). */ /* Identity is the HANDLE (the strict_eq stance). */
return a->v.handle.tag == b->v.handle.tag && a->v.handle.ptr == b->v.handle.ptr; return a->v.handle.tag == b->v.handle.tag && a->v.handle.ptr == b->v.handle.ptr;
@@ -518,7 +518,7 @@ static bool scr_assert_dyn_deep_eq(const ScrDyn *a, const ScrDyn *b) {
return a->v.str->len == b->v.str->len && return a->v.str->len == b->v.str->len &&
memcmp(a->v.str->data, b->v.str->data, a->v.str->len) == 0; memcmp(a->v.str->data, b->v.str->data, a->v.str->len) == 0;
case SCR_DYN_FUNC: case SCR_DYN_FUNC:
return a->v.fn.clo == b->v.fn.clo; return scr_dyn_strict_eq(a, b);
case SCR_DYN_HANDLE: case SCR_DYN_HANDLE:
/* Node's deepStrictEqual over two distinct live handles walks own /* Node's deepStrictEqual over two distinct live handles walks own
* enumerable props we do not model; same-handle is the only case a * enumerable props we do not model; same-handle is the only case a
+2 -2
View File
@@ -198,7 +198,7 @@ static void scr_rej_remove(ScrRejListener *list, size_t *n, ScrDyn *fn) {
for (size_t i = 0; i < *n; i++) { for (size_t i = 0; i < *n; i++) {
ScrDyn *l = list[i].fn; ScrDyn *l = list[i].fn;
bool same = l == fn || (l->kind == SCR_DYN_FUNC && fn->kind == SCR_DYN_FUNC && bool same = l == fn || (l->kind == SCR_DYN_FUNC && fn->kind == SCR_DYN_FUNC &&
l->v.fn.clo == fn->v.fn.clo); scr_dyn_strict_eq(l, fn));
if (same) { if (same) {
scr_dyn_release(l); scr_dyn_release(l);
memmove(list + i, list + i + 1, (*n - i - 1) * sizeof *list); memmove(list + i, list + i + 1, (*n - i - 1) * sizeof *list);
@@ -471,7 +471,7 @@ void scr_process_off_warning(ScrDyn *fn) {
for (size_t i = 0; i < scr_nwarn; i++) { for (size_t i = 0; i < scr_nwarn; i++) {
ScrDyn *l = scr_warn_listeners[i]; ScrDyn *l = scr_warn_listeners[i];
bool same = l == fn || (l->kind == SCR_DYN_FUNC && fn->kind == SCR_DYN_FUNC && bool same = l == fn || (l->kind == SCR_DYN_FUNC && fn->kind == SCR_DYN_FUNC &&
l->v.fn.clo == fn->v.fn.clo); scr_dyn_strict_eq(l, fn));
if (same) { if (same) {
scr_dyn_release(l); scr_dyn_release(l);
memmove(scr_warn_listeners + i, scr_warn_listeners + i + 1, memmove(scr_warn_listeners + i, scr_warn_listeners + i + 1,
+1 -1
View File
@@ -105,7 +105,7 @@ static void scr_enable_utf8_console_output(void) {
} }
#endif #endif
void scr_runtime_abi_v3(void) {} void scr_runtime_abi_v4(void) {}
void scr_init(void) { void scr_init(void) {
#ifdef _WIN32 #ifdef _WIN32
+60
View File
@@ -23,6 +23,15 @@
*/ */
#include "scr_runtime.h" #include "scr_runtime.h"
/* Constructor capsules expose identity and name/length, but do not model
* per-class static property tables yet. Never report a fabricated empty view. */
static bool scr_dyn_class_reflection_fence(const ScrDyn *value) {
if (!value || value->kind != SCR_DYN_FUNC || !value->v.fn.class_obj) return false;
static const char message[] = "scriptc: class reflection through unknown is not supported";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return true;
}
#include <math.h> #include <math.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@@ -852,6 +861,7 @@ bool scr_dyn_proxy_has(const ScrDyn *proxy, const ScrStr *key) {
} }
ScrDyn *scr_dyn_own_descriptor(const ScrDyn *value, const ScrStr *key) { ScrDyn *scr_dyn_own_descriptor(const ScrDyn *value, const ScrStr *key) {
if (scr_dyn_class_reflection_fence(value)) return NULL;
if (value->kind == SCR_DYN_OBJ) { if (value->kind == SCR_DYN_OBJ) {
ScrDynEntry *entry = scr_dyn_entry((ScrDyn *)value, key); ScrDynEntry *entry = scr_dyn_entry((ScrDyn *)value, key);
if (!entry) return scr_dyn_retain(scr_dyn_undefined()); if (!entry) return scr_dyn_retain(scr_dyn_undefined());
@@ -1204,9 +1214,48 @@ ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, con
d->v.fn.sig = sig; d->v.fn.sig = sig;
d->v.fn.name = name; d->v.fn.name = name;
d->v.fn.arity = arity; d->v.fn.arity = arity;
d->v.fn.class_obj = NULL;
return d; return d;
} }
/* A class is function-shaped for typeof and identity, but calling it without
* new always throws. The closure owns the class object and its lexical boxes. */
static ScrDyn *scr_dyn_class_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
(void)args;
(void)argc;
ScrClassObj *cls = scr_box_get_ref(closure->caps[0]);
ScrJsonBuf message;
scr_jb_init(&message);
scr_jb_puts(&message, "Class constructor ");
scr_jb_write(&message, cls->name->data, cls->name->len);
scr_jb_puts(&message, " cannot be invoked without 'new'");
scr_classobj_release(cls);
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message));
return NULL;
}
ScrDyn *scr_dyn_new_class(ScrClassObj *cls, const char *type_key) {
ScrClosure *closure = scr_closure_new(NULL, 1);
closure->caps[0] = scr_box_new_obj(scr_classobj_retain_v, scr_classobj_release_v, scr_classobj_trace_v);
scr_box_set_ref(closure->caps[0], scr_classobj_retain(cls));
ScrDyn *value = scr_dyn_new_func(closure, scr_dyn_class_call, (uint32_t)cls->length, type_key, cls->name->data);
value->v.fn.class_obj = cls; /* borrowed from the closure */
return value;
}
bool scr_dyn_class_is(const ScrDyn *value, const char *type_key) {
return value && value->kind == SCR_DYN_FUNC && value->v.fn.class_obj &&
strcmp(value->v.fn.sig, type_key) == 0;
}
ScrClassObj *scr_dyn_class_check(const ScrDyn *value, const char *type_key, const ScrDynPath *path) {
if (!scr_dyn_class_is(value, type_key)) {
scr_dyn_check_fail(path, "class constructor", value);
return NULL;
}
return scr_classobj_retain(value->v.fn.class_obj);
}
/* Calling a dyn value: kind check (Node's "<what> is not a function" /* Calling a dyn value: kind check (Node's "<what> is not a function"
* TypeError, catchable), then the boxed thunk — per-argument validation * TypeError, catchable), then the boxed thunk — per-argument validation
* into the closure's declared parameter types lives THERE (the thunk is * into the closure's declared parameter types lives THERE (the thunk is
@@ -2687,6 +2736,7 @@ static const char *scr_dyn_kind_name(const ScrDyn *d);
* valid dense index, every other kind false (tsc admits `in` only on * valid dense index, every other kind false (tsc admits `in` only on
* object-typed operands). Proxy traps may throw. Borrows both. */ * object-typed operands). Proxy traps may throw. Borrows both. */
bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key) { bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key) {
if (scr_dyn_class_reflection_fence(v)) return false;
if (v->kind == SCR_DYN_PROXY) return scr_dyn_proxy_has(v, key); if (v->kind == SCR_DYN_PROXY) return scr_dyn_proxy_has(v, key);
if (v->kind == SCR_DYN_JSVAL) return scr_dyn_isl_fence(v, "'in'"); if (v->kind == SCR_DYN_JSVAL) return scr_dyn_isl_fence(v, "'in'");
if (v->kind == SCR_DYN_TYPED_REF) { if (v->kind == SCR_DYN_TYPED_REF) {
@@ -2712,6 +2762,7 @@ bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key) {
} }
void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
if (scr_dyn_class_reflection_fence(recv)) return;
if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_set(recv, key, value); return; } if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_set(recv, key, value); return; }
if (recv->kind == SCR_DYN_TYPED_REF) { if (recv->kind == SCR_DYN_TYPED_REF) {
ScrDyn *materialized = scr_dyn_typed_ref_materialize(recv); ScrDyn *materialized = scr_dyn_typed_ref_materialize(recv);
@@ -3580,6 +3631,7 @@ static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) {
* arrays, typed references and handles keep their explicit boundary; * arrays, typed references and handles keep their explicit boundary;
* deleting from a materialized snapshot would lose the mutation. */ * deleting from a materialized snapshot would lose the mutation. */
void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) { void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) {
if (scr_dyn_class_reflection_fence(recv)) return;
if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_delete(recv, key); return; } if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_delete(recv, key); return; }
if (recv->kind == SCR_DYN_OBJ) { if (recv->kind == SCR_DYN_OBJ) {
ScrDynEntry *entry = scr_dyn_entry(recv, key); ScrDynEntry *entry = scr_dyn_entry(recv, key);
@@ -3834,6 +3886,7 @@ bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b) {
/* The ScrDyn box is a boundary artifact — one closure crossing the /* The ScrDyn box is a boundary artifact — one closure crossing the
* dyn boundary twice is still ONE JS function value, so identity * dyn boundary twice is still ONE JS function value, so identity
* lives in the boxed closure, not the box. */ * lives in the boxed closure, not the box. */
if (a->v.fn.class_obj || b->v.fn.class_obj) return a->v.fn.class_obj == b->v.fn.class_obj;
return a == b || a->v.fn.clo == b->v.fn.clo; return a == b || a->v.fn.clo == b->v.fn.clo;
case SCR_DYN_BYTES: case SCR_DYN_BYTES:
return a->v.bytes == b->v.bytes; return a->v.bytes == b->v.bytes;
@@ -3878,6 +3931,10 @@ ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len) {
if (key_len == 6 && memcmp(key, "length", 6) == 0) { if (key_len == 6 && memcmp(key, "length", 6) == 0) {
return scr_dyn_new_num((double)d->v.fn.arity); return scr_dyn_new_num((double)d->v.fn.arity);
} }
if (d->v.fn.class_obj) {
static const char message[] = "scriptc: class properties through unknown other than name and length are not supported";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
}
return NULL; return NULL;
} }
@@ -4190,6 +4247,7 @@ static bool scr_dyn_objwalk_entry(ScrDyn *out, const ScrDyn *object,
} }
static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) { static ScrDyn *scr_dyn_objwalk(const ScrDyn *v, ScrObjWalk mode) {
if (scr_dyn_class_reflection_fence(v)) return NULL;
if (v->kind == SCR_DYN_PROXY) { if (v->kind == SCR_DYN_PROXY) {
ScrDyn *trap = scr_dyn_proxy_trap(v, "ownKeys"); ScrDyn *trap = scr_dyn_proxy_trap(v, "ownKeys");
if (scr_exc_pending()) return NULL; if (scr_exc_pending()) return NULL;
@@ -4357,6 +4415,7 @@ ScrDyn *scr_dyn_obj_keys(const ScrDyn *v) { return scr_dyn_objwalk(v, SCR_OBJWAL
* neither contributes enumerable keys. Snapshot names without invoking * neither contributes enumerable keys. Snapshot names without invoking
* getters; the loop rechecks live ownership before visiting each name. */ * getters; the loop rechecks live ownership before visiting each name. */
ScrDyn *scr_dyn_for_in_keys(const ScrDyn *v) { ScrDyn *scr_dyn_for_in_keys(const ScrDyn *v) {
if (scr_dyn_class_reflection_fence(v)) return NULL;
switch (v->kind) { switch (v->kind) {
case SCR_DYN_OBJ: case SCR_DYN_OBJ:
case SCR_DYN_ARR: case SCR_DYN_ARR:
@@ -4442,6 +4501,7 @@ static void scr_dyn_assign_from(ScrDyn *target, const ScrDyn *src) {
* nothing; index-keyed sources (arrays/strings/bytes) copy their index * nothing; index-keyed sources (arrays/strings/bytes) copy their index
* keys like Node; the remaining kinds have no own enumerable keys. */ * keys like Node; the remaining kinds have no own enumerable keys. */
ScrDyn *scr_dyn_assign(ScrDyn *target, const ScrDyn *src) { ScrDyn *scr_dyn_assign(ScrDyn *target, const ScrDyn *src) {
if (scr_dyn_class_reflection_fence(target) || scr_dyn_class_reflection_fence(src)) return NULL;
if (target->kind == SCR_DYN_UNDEF || target->kind == SCR_DYN_NULL) { if (target->kind == SCR_DYN_UNDEF || target->kind == SCR_DYN_NULL) {
const char *m = "Cannot convert undefined or null to object"; const char *m = "Cannot convert undefined or null to object";
scr_throw_error_msg(SCR_ERR_TYPE, m, strlen(m)); scr_throw_error_msg(SCR_ERR_TYPE, m, strlen(m));
+39 -5
View File
@@ -1,10 +1,44 @@
/* Class instances are per-class C structs emitted by the compiler; the /* Class instance layouts are emitted by the compiler. Class objects share
* runtime only provides the RC-audit hooks their emitted new/release * one runtime layout, including captured bindings for local classes. */
* helpers call — plus the class-OBJECT entry points (classes as values:
* every class object is an emitted immortal static, so the adapters are
* no-ops behind the uniform container RC machinery). */
#include "scr_runtime.h" #include "scr_runtime.h"
void scr_classobj_trace_v(void *object, ScrTraceVisit visit, void *ctx) {
ScrClassObj *c = object;
for (size_t i = 0; i < c->ncaps; i++) visit(c->caps[i], ctx);
}
static void scr_classobj_gcfree(void *object) {
scr_obj_free_note();
scr_cyc_free(object);
}
ScrClassObj *scr_classobj_new(const ScrClassObj *template, size_t ncaps) {
if (ncaps > (SIZE_MAX - sizeof(ScrClassObj)) / sizeof(ScrBox *))
scr_trap("scriptc: class capture allocation overflow\n");
ScrClassObj *c = scr_cyc_alloc(sizeof(ScrClassObj) + ncaps * sizeof(ScrBox *),
&scr_classobj_trace_v, &scr_classobj_gcfree);
c->rc = 1;
c->pre = template->pre;
c->post = template->post;
c->ctor = template->ctor;
c->name = template->name;
c->ncaps = ncaps;
c->length = template->length;
scr_obj_alloc_note();
return c;
}
void scr_classobj_release(ScrClassObj *c) {
if (!c || c->rc == SIZE_MAX) return;
if (--c->rc == 0) {
scr_cyc_on_dead(c);
for (size_t i = 0; i < c->ncaps; i++) scr_box_release(c->caps[i]);
scr_classobj_gcfree(c);
} else {
scr_cyc_on_release(c);
}
}
void *scr_classobj_retain_v(void *c) { void *scr_classobj_retain_v(void *c) {
return scr_classobj_retain((ScrClassObj *)c); return scr_classobj_retain((ScrClassObj *)c);
} }
+22 -21
View File
@@ -62,7 +62,7 @@ void scr_init(void);
/* Program objects emitted by the bundled LLVM helper reference this symbol. /* Program objects emitted by the bundled LLVM helper reference this symbol.
* Its versioned spelling makes a mismatched manual runtime link fail before * Its versioned spelling makes a mismatched manual runtime link fail before
* the program can start. */ * the program can start. */
void scr_runtime_abi_v3(void); void scr_runtime_abi_v4(void);
/* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ──────
* Every unrecoverable runtime trap — OOM, semantic range traps, internal- * Every unrecoverable runtime trap — OOM, semantic range traps, internal-
@@ -451,34 +451,32 @@ int scr_str_cmp(ScrStr *a, ScrStr *b);
int scr_str_cmp_u16(ScrStr *a, ScrStr *b); int scr_str_cmp_u16(ScrStr *a, ScrStr *b);
/* ── class objects (classes as first-class values) ──────────────────── /* ── class objects (classes as first-class values) ────────────────────
* The class STATIC side as a runtime value: one emitted IMMORTAL static * Top-level classes use immortal templates. Evaluating a function-local
* per class the program takes as a value (`const X = C`, class * class creates a fresh, traced object that owns its captured binding boxes.
* expressions, constructor-typed slots). One struct type covers every * Every construct thunk takes the borrowed class object before source
* class — the fields are class-independent — so containers and casts * arguments. Instances of a local class retain that object for identity and
* never need per-class knowledge. `pre`/`post` are the SAME preorder * method captures. `pre`/`post` still describe the compiled class hierarchy;
* numbering the vtables carry (compile-time constants in the emitted * local-class instanceof additionally compares the fresh object identity. */
* initializer), so `x instanceof X` through a value is the usual O(1)
* interval check with the interval loaded from the class object. `ctor`
* is the emitted construct thunk — the class's own completed-constructor
* ABI returning `void *` (the compiler's flow rules guarantee every value
* in a slot shares one ABI); `name` is the JS-observable `.name` string
* (an interned immortal literal). rc is always SIZE_MAX: retain/release
* are no-ops (the regex-literal discipline), the object holds no
* references, and it can never be part of a cycle (trace = NULL). */
typedef struct ScrClassObj { typedef struct ScrClassObj {
size_t rc; /* SIZE_MAX — every class object is an immortal static */ size_t rc;
size_t pre, post; size_t pre, post;
void *ctor; void *ctor;
const ScrStr *name; const ScrStr *name;
size_t ncaps;
size_t length;
struct ScrBox *caps[];
} ScrClassObj; } ScrClassObj;
static inline ScrClassObj *scr_classobj_retain(ScrClassObj *c) { static inline ScrClassObj *scr_classobj_retain(ScrClassObj *c) {
if (c->rc != SIZE_MAX) c->rc++; if (c && c->rc != SIZE_MAX) {
c->rc++;
scr_cyc_mark_live(c);
}
return c; return c;
} }
static inline void scr_classobj_release(ScrClassObj *c) { void scr_classobj_release(ScrClassObj *c);
(void)c; /* immortal (NULL-tolerant like every release) */ ScrClassObj *scr_classobj_new(const ScrClassObj *template, size_t ncaps);
} void scr_classobj_trace_v(void *c, ScrTraceVisit visit, void *ctx);
/* void*-signature RC adapters (container slots) — scr_object.c. */ /* void*-signature RC adapters (container slots) — scr_object.c. */
void *scr_classobj_retain_v(void *c); void *scr_classobj_retain_v(void *c);
void scr_classobj_release_v(void *c); void scr_classobj_release_v(void *c);
@@ -3462,7 +3460,7 @@ struct ScrDyn {
* has no trace header): trial deletion treats it as an external root, * has no trace header): trial deletion treats it as an external root,
* so nothing dangles — a cycle THROUGH a dyn-boxed function is merely * so nothing dangles — a cycle THROUGH a dyn-boxed function is merely
* never collected (documented divergence). */ * never collected (documented divergence). */
struct { ScrClosure *clo; ScrDynThunk thunk; const char *sig; const char *name; uint32_t arity; } fn; struct { ScrClosure *clo; ScrDynThunk thunk; const char *sig; const char *name; uint32_t arity; ScrClassObj *class_obj; } fn;
/* SCR_DYN_HANDLE: the retained native handle + its type tag. The /* SCR_DYN_HANDLE: the retained native handle + its type tag. The
* dyn→handle edge is NOT visible to the cycle collector (the dyn→ * dyn→handle edge is NOT visible to the cycle collector (the dyn→
* closure stance): handles drop their listener lists at settlement, * closure stance): handles drop their listener lists at settlement,
@@ -3761,6 +3759,8 @@ ScrDyn *scr_dyn_define_props(ScrDyn *target, ScrDyn *descs);
* (callers retain first when they keep their own reference); `sig`/`name` * (callers retain first when they keep their own reference); `sig`/`name`
* must be static literals (the box never frees them; name may be NULL). */ * must be static literals (the box never frees them; name may be NULL). */
ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, const char *sig, const char *name); ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, const char *sig, const char *name);
ScrDyn *scr_dyn_new_class(ScrClassObj *cls, const char *type_key);
bool scr_dyn_class_is(const ScrDyn *value, const char *type_key);
/* Calls a dyn value: a non-function kind throws the catchable TypeError /* Calls a dyn value: a non-function kind throws the catchable TypeError
* "<what> is not a function" (Node's wording — `what` is the call site's * "<what> is not a function" (Node's wording — `what` is the call site's
* callee spelling) and returns NULL; a function kind delegates to the * callee spelling) and returns NULL; a function kind delegates to the
@@ -3783,6 +3783,7 @@ typedef struct ScrDynPath {
const char *key; const char *key;
size_t index; size_t index;
} ScrDynPath; } ScrDynPath;
ScrClassObj *scr_dyn_class_check(const ScrDyn *value, const char *type_key, const ScrDynPath *path);
/* The dynCheck failure path: builds "TypeError: expected <want> at <path>, /* The dynCheck failure path: builds "TypeError: expected <want> at <path>,
* got <kind>" (got == NULL renders as "undefined" — a missing object * got <kind>" (got == NULL renders as "undefined" — a missing object
+32
View File
@@ -334,7 +334,39 @@ static void json_callback_tests(void) {
scr_dyn_release(callback); scr_dyn_release(callback);
} }
static void test_class_values(void) {
ScrStr *name = S("Stored");
ScrClassObj template = { .rc = SIZE_MAX, .name = name, .length = 2 };
ScrClassObj *first = scr_classobj_new(&template, 1);
first->caps[0] = scr_box_new(SCR_BOX_F64);
scr_box_set_f64(first->caps[0], 42);
ScrClassObj *second = scr_classobj_new(&template, 0);
ScrDyn *a = scr_dyn_new_class(first, "classval:Stored");
ScrDyn *again = scr_dyn_new_class(first, "classval:Stored");
ScrDyn *b = scr_dyn_new_class(second, "classval:Stored");
scr_classobj_release(first);
scr_classobj_release(second);
check(scr_dyn_strict_eq(a, again) && !scr_dyn_strict_eq(a, b), "boxed class identity belongs to evaluation");
ScrClassObj *restored = scr_dyn_class_check(a, "classval:Stored", NULL);
check(restored && scr_box_get_f64(restored->caps[0]) == 42, "boxed class retains captured environment");
scr_classobj_release(restored);
check(!scr_dyn_class_check(a, "classval:Other", NULL) && scr_exc_pending(), "class casts check native constructor type");
scr_exc_clear();
ScrDyn *arity = scr_dyn_fn_get(a, "length", 6);
check(arity && arity->kind == SCR_DYN_NUM && arity->v.num == 2, "boxed constructor length");
scr_dyn_release(arity);
check(!scr_dyn_call(a, NULL, 0, "Stored") && scr_exc_pending(), "class call requires new");
scr_exc_clear();
check(!scr_dyn_obj_keys(a) && scr_exc_pending(), "class reflection refuses an incomplete property view");
scr_exc_clear();
scr_dyn_release(a);
scr_dyn_release(again);
scr_dyn_release(b);
scr_str_release(name);
}
int main(void) { int main(void) {
test_class_values();
scr_init(); scr_init();
/* ── primitives ─────────────────────────────────────────────────── */ /* ── primitives ─────────────────────────────────────────────────── */
+37
View File
@@ -0,0 +1,37 @@
let trace = [];
function value(label, result) { trace.push(label); return result; }
class Color {
static rgb(r, g, b, a = value("default", 255)) { return `${r},${g},${b},${a}`; }
}
const rgb = [10, 20, 30];
console.log(Color.rgb(...rgb));
console.log(Color.rgb(...value("spread", [1, 2]), value("last", 3), value("alpha", 4)));
console.log(Color.rgb(value("first", 5), ...value("middle", [6]), ...value("end", [7])));
console.log(trace.join("|"));
function collect(first, second = 9, ...rest) { return `${first}:${second}:${rest.join("-")}`; }
console.log(collect(...[1, 2, 3, 4]));
console.log(collect(...[5]));
console.log(collect(...[], ...[6, undefined, 8]));
function count(first) { return `${first}:${arguments.length}:${arguments[2]}`; }
console.log(count(...[10, 20, 30]));
console.log(count(...rgb));
const sparse = [, 42, ,];
console.log(collect(...sparse));
const grown = [1];
grown.length = 3;
grown[2] = 3;
console.log(collect(...grown));
function fixed(first) { return first; }
console.log(fixed(...value("surplus", [11, 12, 13]), value("discard", 14)));
console.log(trace.join("|"));
function build(seed) {
return class Stored {
constructor(a, b = 2) { this.value = seed + a + b; }
read() { return this.value; }
};
}
const Stored = build(10);
const instance = new Stored(...[3]);
console.log(instance.read());
+32
View File
@@ -0,0 +1,32 @@
function make(value: number) {
return () => class Local {
read(): number { return value; }
async delayed(): Promise<number> { await Promise.resolve(); return ++value; }
};
}
const evaluate = make(2);
const First = evaluate();
const Second = evaluate();
const first = new First();
const second = new Second();
console.log(First === Second, await first.delayed(), second.read());
function loop(): number[] {
const reads: (() => number)[] = [];
for (let i = 0; i < 3; i++) {
const Local = class { read(): number { return i; } };
const value = new Local();
reads.push(() => value.read());
}
return reads.map((read) => read());
}
console.log(loop().join(","));
function tdz(): void {
const Local = class { read(): number { return later; } };
const value = new Local();
try { console.log(value.read()); } catch (error) { console.log((error as Error).name); }
let later = 17;
console.log(value.read());
}
tdz();
+53
View File
@@ -0,0 +1,53 @@
function factory(initial: number, label: string) {
let shared = initial;
const Local = class Counter {
own: number;
constructor(offset: number = shared) {
this.own = offset;
if (offset < 0) throw new Error(label);
}
get value(): number { return shared + this.own; }
set value(next: number) { shared = next - this.own; }
callback = (): string => `${label}:${shared}:${this.own}`;
next(): number { return ++shared; }
};
shared++;
return Local;
}
const A = factory(2, "first");
const B = factory(8, "second");
const a = new A();
const b = new A(10);
const c = new B();
console.log(a.value, b.value, c.value);
a.value = 20;
console.log(a.next(), b.value, c.value, a.callback());
try { new A(-1); } catch (error) { console.log((error as Error).message); }
console.log(b.callback());
function empty() { return class {}; }
const Empty = empty();
const Other = empty();
const e = new Empty();
console.log(Empty === Other, e instanceof Empty, e instanceof Other);
function late() {
const Local = class { read(): number { return value; } };
let value = 13;
return Local;
}
const Late = late();
console.log(new Late().read());
function cycle(value: number): number {
let read = (): number => value;
const Local = class {
read(): number { return read(); }
direct(): number { return value; }
};
const instance = new Local();
read = (): number => instance.direct();
return instance.read();
}
for (let i = 0; i < 300; i++) cycle(i);
console.log(cycle(42));
@@ -0,0 +1,16 @@
function make(value) {
return class Stored {
read() { return value; }
matches(other) { return other === value; }
};
}
const Numbers = make(12);
const OtherNumbers = make(25);
const Strings = make("hello");
const a = new Numbers();
const b = new OtherNumbers();
const c = new Strings();
console.log(Numbers.name, Numbers === OtherNumbers);
console.log(a.read(), b.read(), c.read());
console.log(a.matches(12), b.matches(12), c.matches("hello"));
console.log(a instanceof Numbers, a instanceof OtherNumbers, c instanceof Strings);
+32
View File
@@ -0,0 +1,32 @@
import assert from "node:assert/strict";
function make(value: number) {
return class Stored {
constructor(extra: number = 0) { value += extra; }
read(): number { return value; }
};
}
const A = make(12);
const B = make(25);
const one: unknown = A;
const again: unknown = A;
const two: unknown = B;
console.log(typeof one, one === again, one === two);
assert.strictEqual(one, again);
assert.notStrictEqual(one, two);
assert.deepStrictEqual(one, again);
const properties = one as Record<string, unknown>;
console.log(properties.name, properties.length);
const Restored = one as typeof A;
console.log(new Restored(2).read(), new B().read());
const call = one as () => void;
try { call(); } catch (error) { console.log((error as Error).name, (error as Error).message); }
class Top { constructor(first: number, second = 0) {} }
const top: unknown = Top;
const topProperties = top as Record<string, unknown>;
console.log(topProperties.name, topProperties.length);
const Saved = top as typeof Top;
const saved = new Saved(1);
console.log(saved instanceof Top);
const list: unknown = [A, B];
console.log(JSON.stringify(list));
+18
View File
@@ -0,0 +1,18 @@
function makeCounter(start: number) {
let value = start;
const Local = class Counter {
read(): number { return value; }
increment(): number { return ++value; }
callback = (): number => value;
};
value++;
return Local;
}
const First = makeCounter(10);
const Second = makeCounter(20);
const a = new First();
const b = new First();
const c = new Second();
console.log(First.name, First === First, First === Second);
console.log(a.read(), b.increment(), a.callback(), c.read());
console.log(a instanceof First, a instanceof Second, c instanceof First, c instanceof Second);
+2 -2
View File
@@ -47,9 +47,9 @@ someClass.tag = "write";
// runtime class decides which declaration answers. // runtime class decides which declaration answers.
console.log(someClass.tag); console.log(someClass.tag);
// Class expressions inside functions mint a DISTINCT class per evaluation. // Local classes with statics still need per-evaluation static storage.
function make(): unknown { function make(): unknown {
return class {}; return class { static value = 1; };
} }
make(); make();
@@ -0,0 +1,14 @@
function privateBrand() { return class { #value = 1; read(): number { return this.#value; } }; }
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();
function genericFactory<T>(value: T) {
return class Stored { read(): T { return value; } };
}
genericFactory<number>(7);
// End of unsupported local-class forms.
@@ -58,11 +58,11 @@ class-values.ts:48:13 - error SC1090: reading the static member 'tag' through a
| ^~~~~~~~~~~~~ | ^~~~~~~~~~~~~
49 | 49 |
class-values.ts:52:10 - error SC1090: class expressions inside functions (each evaluation creates a DISTINCT class in JS — fresh identity, fresh statics; declare the class at top level) are not supported yet class-values.ts:52:10 - error SC1090: function-local classes with inheritance, statics, decorators, or generic members are not supported yet
51 | function make(): unknown { 51 | function make(): unknown {
52 | return class {}; 52 | return class { static value = 1; };
| ^~~~~~~~ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~
53 | } 53 | }
class-values.ts:59:18 - error SC1090: class expressions with static initializers or static blocks outside a whole-initializer position (their declaration-time code must run exactly where the expression evaluates — bind the class in its own top-level `const C = class …` statement) is not supported yet class-values.ts:59:18 - error SC1090: class expressions with static initializers or static blocks outside a whole-initializer position (their declaration-time code must run exactly where the expression evaluates — bind the class in its own top-level `const C = class …` statement) is not supported yet
@@ -0,0 +1,33 @@
local-class-boundaries.ts:1:34 - error SC1090: function-local classes with private brands are not supported yet
1 | function privateBrand() { return class { #value = 1; read(): number { return this.#value; } }; }
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 | function selfName() { return class Local { copy() { return new Local(); } }; }
local-class-boundaries.ts:2:60 - error SC1090: construction through a function-local class's internal name is not supported yet
1 | function privateBrand() { return class { #value = 1; read(): number { return this.#value; } }; }
2 | function selfName() { return class Local { copy() { return new Local(); } }; }
| ^~~~~~~~~~~
3 | class Base {}
local-class-boundaries.ts:4:29 - error SC1090: function-local classes with inheritance, statics, decorators, or generic members are not supported yet
3 | class Base {}
4 | function derived() { return class extends Base {}; }
| ^~~~~~~~~~~~~~~~~~~~~
5 | function generic() { return class { read<T>(value: T): T { return value; } }; }
local-class-boundaries.ts:5:29 - error SC1090: function-local classes with inheritance, 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; } }; }
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
6 | privateBrand();
local-class-boundaries.ts:13:1 - error SC2001: values of type 'typeof Stored' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')
12 | }
13 | genericFactory<number>(7);
| ^~~~~~~~~~~~~~~~~~~~~~~~~
14 | // End of unsupported local-class forms.