Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions packages/compiler/src/backend/emission/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1486,16 +1486,19 @@ export class CEmitter {
* assigns later, a constructor branch skips it, a base constructor's
* virtual call reads a derived field before super() returns. Node reads
* `undefined` there; a NULL payload pointer would be a segfault (union
* fields) or a silent nothing (jsval fields). Undefined-armed unions get
* the interned immortal unit instance (free; releases skip it); jsval
* (`any`) fields get an engine undefined cell — such classes exist only
* in --dynamic builds, and the field's release balances it. Empty for
* fields) or a silent nothing (jsval/dyn fields). Undefined-armed unions
* get the interned immortal unit instance (free; releases skip it); jsval
* (`any`) fields get an engine undefined cell, while dyn (`unknown`)
* fields get the checked-dynamic immortal undefined singleton. Empty for
* every type that cannot hold undefined (tsc's SPI guards those) and for
* record shapes' construction paths, which write every field. */
undefFieldInitLineC(name: string, t: IrType): string[] {
if (t.kind === "jsval") {
return [` o->${mangleField(name)} = scr_jsval_undefined(); /* ${name} starts undefined */`];
}
if (t.kind === "dyn") {
return [` o->${mangleField(name)} = scr_dyn_retain(scr_dyn_undefined()); /* ${name} starts undefined */`];
}
const tag = this.undefinedArmTag(t);
if (tag < 0 || t.kind !== "union") return [];
return [` o->${mangleField(name)} = ${this.unitInstanceRef(t.unionId, tag)}; /* ${name} starts undefined */`];
Expand Down
12 changes: 7 additions & 5 deletions packages/compiler/src/backend/llvm/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,16 +213,18 @@ export interface ClassHost extends ShapeHost {

/** The newFn initialization stores for fields whose type ADMITS undefined
* (undefFieldInitLineC's LLVM twin): undefined-armed union fields start
* at the interned unit instance; jsval fields (an `any` class field under
* --dynamic) start at the engine's undefined cell. */
* at the interned unit instance; jsval (`any`) fields start at the engine's
* undefined cell, while dyn (`unknown`) fields start at the checked-dynamic
* immortal undefined singleton. */
function undefFieldInits(host: ClassHost, meta: LlClassMeta): string[] {
const out: string[] = [];
meta.def.fields.forEach((f, i) => {
const { index } = classFieldIndex(meta, f.name);
if (f.type.kind === "jsval") {
host.declare(`declare ptr @scr_jsval_undefined()`);
if (f.type.kind === "jsval" || f.type.kind === "dyn") {
const fn = f.type.kind === "jsval" ? "scr_jsval_undefined" : "scr_dyn_undefined";
host.declare(`declare ptr @${fn}()`);
out.push(
` %ufv${i} = call ptr @scr_jsval_undefined()`,
` %ufv${i} = call ptr @${fn}()`,
` %uf${i} = getelementptr inbounds %${mangleClassStruct(meta.def.name)}, ptr %o, i64 0, i32 ${index}`,
` store ptr %ufv${i}, ptr %uf${i} ; ${f.name} starts undefined`,
);
Expand Down
17 changes: 5 additions & 12 deletions packages/compiler/src/frontend/lowering/lower-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1229,9 +1229,6 @@ export function collectClassShapeInner(L: Lowerer, decl: ts.ClassLikeDeclaration
) {
const type = L.irTypeOf(member.name);
if (type.kind === "void") L.badType(member.name, L.typeOf(member.name));
if (type.kind === "dyn") {
L.unsupported("SC1090", member.name, "'unknown'-typed static fields");
}
staticFields.push({
name: member.name.text,
type,
Expand Down Expand Up @@ -1473,11 +1470,9 @@ export function collectClassShapeInner(L: Lowerer, decl: ts.ClassLikeDeclaration
// the ordinary undefined-armed union machinery.
const type = L.irTypeOf(member.name);
if (type.kind === "void") L.badType(member.name, L.typeOf(member.name));
// dyn stays out of class fields (KEEP NARROW; record
// fields and array elements are unmappable via mapType already).
if (type.kind === "dyn") {
L.unsupported("SC1090", member.name, "'unknown'-typed class fields");
}
// `unknown` fields use the same checked-dynamic dyn kind as
// unknown locals/params. Allocation initializes them to the dyn
// undefined singleton before field initializers run.
if (fields.has(member.name.text)) {
// REDECLARING an inherited field: Node [[Define]]s the OWN
// property again when THIS class's field initializers run
Expand Down Expand Up @@ -1588,10 +1583,8 @@ export function collectClassShapeInner(L: Lowerer, decl: ts.ClassLikeDeclaration
const shape = L.paramShape(p);
const type = shape.bodyType ?? shape.type;
if (type.kind === "void") L.badType(p.name, L.typeOf(p.name));
// The class-field dyn rule verbatim (KEEP NARROW).
if (type.kind === "dyn") {
L.unsupported("SC1090", p.name, "'unknown'-typed class fields");
}
// Unknown parameter properties use the ordinary dyn parameter
// ABI and assign into the dyn class slot after super().
// `override x` (and any same-named inherited member) would
// redeclare a base slot — the declared-field rule verbatim.
if (fields.has(name)) {
Expand Down
3 changes: 1 addition & 2 deletions tests/diagnostics/json-dyn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function localCapture(): () => number {
return () => local as number;
}
class Holder {
data: unknown = JSON.parse("{}");
data: unknown = JSON.parse("{}"); // unknown class fields compile as dyn storage now — no fence
}
const anything: any = 5; // checker-`any` bindings ride the checked-dynamic tree now — no fence
const dynArray: unknown[] = []; // unknown[] IS the dyn array now — no fence (corpus 2585)
Expand All @@ -49,7 +49,6 @@ function mkMaybe(): string | undefined {
return undefined;
}
const stringifyUndef = JSON.stringify(mkMaybe());

// Reached: unreached bodies never lower, so their rejections only exist
// when something on the entry path uses them.
localCapture();
Expand Down
16 changes: 1 addition & 15 deletions tests/harness/__snapshots__/json-dyn.ts.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,6 @@ json-dyn.ts:28:19 - error SC1090: a checked cast of 'unknown' to 'Point' (a dyna
| ^~~~~~~~~~~~~~~~~~~~~~~~~
29 | // (casts of unknown to ADAPTABLE function types compile now — the kind

json-dyn.ts:38:3 - error SC1090: 'unknown'-typed class fields are not supported yet

37 | class Holder {
38 | data: unknown = JSON.parse("{}");
| ^~~~
39 | }

json-dyn.ts:42:7 - error SC2007: values of type '{ (text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined): any; (text: string): unknown; }' cannot be compiled: the type declares multiple call signatures (overloads), and a compiled function value is always one concrete signature

41 | const dynArray: unknown[] = []; // unknown[] IS the dyn array now — no fence (corpus 2585)
Expand All @@ -67,11 +60,4 @@ json-dyn.ts:51:39 - error SC1090: JSON.stringify of 'string | undefined' values
50 | }
51 | const stringifyUndef = JSON.stringify(mkMaybe());
| ^~~~~~~~~
52 |

json-dyn.ts:59:1 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet

58 | // them relevant; these references are what makes them count.
59 | new Holder();
| ^~~~~~~~~~~~
60 |
52 | // Reached: unreached bodies never lower, so their rejections only exist
105 changes: 105 additions & 0 deletions tests/harness/unknown-fields.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { describe, expect, test } from "vitest";
import { compile } from "@scriptc/compiler";

const execFileAsync = promisify(execFile);
const cacheDir = join(tmpdir(), "scriptc-unknown-fields-tests");
const sanitize = process.env["SCRIPTC_SAN"] === "1";

interface RunResult {
stdout: Buffer;
stderr: Buffer;
exitCode: number;
}

async function run(cmd: string, args: string[]): Promise<RunResult> {
try {
const { stdout, stderr } = await execFileAsync(cmd, args, { encoding: "buffer" });
return { stdout, stderr, exitCode: 0 };
} catch (err) {
if (
typeof err !== "object" || err === null ||
!("code" in err) || typeof err.code !== "number" ||
!("stdout" in err) || !Buffer.isBuffer(err.stdout) ||
!("stderr" in err) || !Buffer.isBuffer(err.stderr)
) {
throw err;
}
return { stdout: err.stdout, stderr: err.stderr, exitCode: err.code };
}
}

async function compileAndCompare(name: string, source: string, backend: "c" | "llvm"): Promise<void> {
const key = createHash("sha256")
.update(source)
.update(backend)
.update(sanitize ? "san" : "plain")
.digest("hex")
.slice(0, 16);
const outDir = join(cacheDir, key);
mkdirSync(outDir, { recursive: true });
const file = join(outDir, `${name}.ts`);
writeFileSync(file, source);
const result = await compile(file, {
outPath: join(outDir, "program"),
outDir,
sanitize,
backend,
});
if (!result.ok) {
throw new Error(
"unknown-fields program failed to compile:\n" +
result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"),
);
}
const [nodeResult, nativeResult] = await Promise.all([
run("node", ["--experimental-transform-types", "--disable-warning=ExperimentalWarning", file]),
run(result.binaryPath, []),
]);
expect(nativeResult.stdout).toEqual(nodeResult.stdout);
expect(nativeResult.stderr).toEqual(nodeResult.stderr);
expect(nativeResult.exitCode).toBe(nodeResult.exitCode);
}

const source = `class Base {
inherited: unknown;
}

class Holder extends Base {
value: unknown;
initialized: unknown = { count: 3 };
static current: unknown = "ready";

constructor(public argument: unknown) {
super();
}
}

const h = new Holder(42);
console.log(h.inherited === undefined);
h.inherited = "from base";
console.log(h.inherited === "from base");
console.log(h.value === undefined, h.argument === 42);
if (typeof h.initialized === "object" && h.initialized !== null && "count" in h.initialized) {
console.log((h.initialized as { count: number }).count);
}
console.log(typeof Holder.current, Holder.current);
h.value = ["a", "b"];
if (Array.isArray(h.value)) console.log(h.value.length, h.value[1]);
Holder.current = false;
console.log(Holder.current === false);
`;

describe.each(["c", "llvm"] as const)(
`unknown-typed class fields, %s backend${sanitize ? " (sanitized)" : ""}`,
(backend) => {
test("matches Node", async () => {
await compileAndCompare(`${backend}-backend`, source, backend);
});
},
);