mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Refuse a stdlib-global snapshot read as a value
`const proc = globalThis.process` lowers to alias plumbing: the declaration emits no storage and no code, because member and call reads resolve through the global it names. A read that needs the VALUE therefore failed at the use with the generic "binding form with no lowering", which names neither the declaration nor the cause. Register every stdlib-global snapshot declaration (TypeScript sources only, matching the elision) and report the cause at the declaration (SC2020), naming the global and, when it has a short spelling, the type the program asked the compiler to hold. The use site keeps the inherited-blocker cascade (SC2004) instead of the generic text. An unread snapshot stays elided: reading a global has no side effects, so dropping an unread `const p = process` is observationally identical to Node, and refusing it would break programs that compile harmlessly today. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
83e26da25e
commit
caf098eff9
@@ -1023,6 +1023,33 @@ export function noLoweringDiag(
|
||||
};
|
||||
}
|
||||
|
||||
/** A stdlib-global snapshot reached a site that needs its VALUE (an
|
||||
* argument, a return, a stored field). The declaration emitted no storage
|
||||
* — alias plumbing only serves member and call reads — and the global's
|
||||
* own value has no lowering, so the read cannot resolve. Reported at the
|
||||
* declaration, where the real cause sits; the use site carries the SC2004
|
||||
* cascade. */
|
||||
export function stdlibSnapshotValueUseDiag(
|
||||
name: string,
|
||||
alias: string,
|
||||
typeText: string | undefined,
|
||||
loc: SrcLoc,
|
||||
): ScrDiagnostic {
|
||||
// Name the requested type when it has a readable spelling; a structural
|
||||
// dump of the global's own object type says less than the global itself.
|
||||
const unholdable =
|
||||
typeText === undefined
|
||||
? "the global it names is not a value scriptc can hold"
|
||||
: `a '${typeText}' is not a value scriptc can hold`;
|
||||
return {
|
||||
code: "SC2020",
|
||||
message: `the stdlib-global snapshot '${name}' ('${alias}') cannot be used as a value: the declaration lowers to alias plumbing (no storage, no code), and ${unholdable}`,
|
||||
loc,
|
||||
milestone: "later",
|
||||
hint: `read the members through the global directly ('${alias}.member') instead of passing '${name}' around as a value`,
|
||||
};
|
||||
}
|
||||
|
||||
/** A REACHED use of a part of the WebAssembly API scriptc does not
|
||||
* implement yet (SC2020): the streaming functions, Tag and Exception, the
|
||||
* classes as values. compile, instantiate, validate, the Module, Instance,
|
||||
|
||||
@@ -286,6 +286,7 @@ import {
|
||||
noLoweringDiag,
|
||||
overloadedSignatureTypeDiag,
|
||||
recordShapeMismatchDiag,
|
||||
stdlibSnapshotValueUseDiag,
|
||||
unsupportedNativeApiDiag,
|
||||
unsupportedPackageValueDiag,
|
||||
unionMismatchDiag,
|
||||
@@ -3378,6 +3379,16 @@ export class Lowerer {
|
||||
* checks resolve through this map (stdlibGlobalNameOf), declarations
|
||||
* emit nothing. */
|
||||
readonly stdlibGlobalAliases = new Map<ts.Symbol, string>();
|
||||
/** The TypeScript declarations registered in stdlibGlobalAliases: the
|
||||
* global each one names and its own name node. Their declarations emit
|
||||
* nothing, so a read that needs the VALUE reports at the declaration
|
||||
* (stdlibGlobalAliasDecl, rejectUnresolvedSymbol) instead of compiling
|
||||
* it away. An unread snapshot stays elided: reading a global has no
|
||||
* side effects. */
|
||||
readonly stdlibGlobalAliasDecls = new Map<ts.Symbol, { alias: string; name: ts.Identifier }>();
|
||||
/** Snapshots whose declaration-side refusal already reported (one
|
||||
* declaration, one report). */
|
||||
private readonly stdlibSnapshotReported = new Set<ts.Symbol>();
|
||||
/** The stdlibGlobalAliases bound through a cast of the global object
|
||||
* (`(globalThis as { Buffer?: B }).Buffer`). Their static type is the
|
||||
* cast's, so their value reads are refused (identifiers.ts). */
|
||||
@@ -4222,6 +4233,10 @@ export class Lowerer {
|
||||
checkedTable("methodLambdasInProgress", () => this.methodLambdasInProgress.size),
|
||||
checkedTable("classMethodValueSelections", () => this.classMethodValueSelections.size),
|
||||
checkedTable("stdlibGlobalAliases", () => this.stdlibGlobalAliases.size),
|
||||
// The snapshots' own declarations and their one-report memory: both
|
||||
// grow with stdlibGlobalAliases, one entry per registered declaration.
|
||||
checkedTable("stdlibGlobalAliasDecls", () => this.stdlibGlobalAliasDecls.size),
|
||||
checkedTable("stdlibSnapshotReported", () => this.stdlibSnapshotReported.size),
|
||||
checkedTable("cjsAccessorFns", () => this.cjsAccessorFns.size),
|
||||
checkedTable("cjsAccessorSnapshots", () => this.cjsAccessorSnapshots.size),
|
||||
checkedTable("cjsSnapshotReady", () => this.cjsSnapshotReady.size),
|
||||
@@ -11358,6 +11373,24 @@ export class Lowerer {
|
||||
throw new PoisonError();
|
||||
}
|
||||
|
||||
/** The declaration-side refusal of a stdlib-global snapshot read as a
|
||||
* VALUE (stdlibGlobalAliasDecl): report once, at the declaration's name,
|
||||
* naming the type the program asked the compiler to hold when it has a
|
||||
* short spelling. */
|
||||
reportStdlibSnapshot(symbol: ts.Symbol): void {
|
||||
const decl = this.stdlibGlobalAliasDecls.get(symbol);
|
||||
if (decl === undefined || this.stdlibSnapshotReported.has(symbol)) return;
|
||||
this.stdlibSnapshotReported.add(symbol);
|
||||
const raw = panicSafe(
|
||||
() => this.checker.typeToString(this.checker.getTypeOfSymbol(symbol)),
|
||||
"",
|
||||
);
|
||||
const typeText = raw !== "" && raw.length <= 48 ? raw : undefined;
|
||||
this.pushDiag(
|
||||
stdlibSnapshotValueUseDiag(decl.name.text, decl.alias, typeText, locOf(decl.name)),
|
||||
);
|
||||
}
|
||||
|
||||
stdlibMemberFence(access: ts.PropertyAccessExpression): void {
|
||||
return stdlibMemberFence(this, access);
|
||||
}
|
||||
@@ -15508,6 +15541,18 @@ export class Lowerer {
|
||||
this.pushDiag(blockedBindingUseDiag(name, locOf(node)));
|
||||
throw new PoisonError();
|
||||
}
|
||||
// A stdlib-global snapshot read as a VALUE (`return proc` for
|
||||
// `const proc = globalThis.process`): the declaration emitted no
|
||||
// storage — alias plumbing only serves member and call reads — and the
|
||||
// global's own value has no lowering. The cause is the declaration, so
|
||||
// report there (once) and cascade here, instead of the generic
|
||||
// binding-form text that names neither.
|
||||
if (symbol !== null && this.stdlibGlobalAliasDecls.has(symbol)) {
|
||||
this.reportStdlibSnapshot(symbol);
|
||||
this.blockedBindings.add(symbol);
|
||||
this.pushDiag(blockedBindingUseDiag(name, locOf(node)));
|
||||
throw new PoisonError();
|
||||
}
|
||||
// A binding whose TYPE keeps a generic call signature and whose
|
||||
// declaration carries no initializer (`declare const o4: undefined |
|
||||
// (<T>(f: (a: T) => T) => T)` — the optional-chained ambient shape):
|
||||
|
||||
@@ -3717,6 +3717,15 @@ export function stdlibGlobalAliasDecl(
|
||||
lowerer.stdlibGlobalAliases.set(symbol, name);
|
||||
if (init !== undefined && castGlobalAliasSource(lowerer, init))
|
||||
lowerer.castGlobalAliases.add(symbol);
|
||||
// The snapshot exists for reads, and its declaration emits nothing: a
|
||||
// read that needs its VALUE (an argument, a return) cannot resolve, so
|
||||
// register the declaration to report the cause there rather than at the
|
||||
// use (rejectUnresolvedSymbol). TypeScript sources only: JavaScript
|
||||
// bindings keep their checked-dynamic value paths. A snapshot no read
|
||||
// resolves stays elided — reading a global has no side effects, so
|
||||
// eliding an unread one is observationally identical to Node.
|
||||
if (!isJsSourceFile(ts.getSourceFileOfNode(nameNode)))
|
||||
lowerer.stdlibGlobalAliasDecls.set(symbol, { alias: name, name: nameNode });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/stdlib-snapshot-value-use.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/unmappable-declaration-initializer.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -31,3 +31,10 @@ console.log(nonEmpty === process.argv.length);
|
||||
const cwd = process.cwd();
|
||||
console.log(cwd);
|
||||
console.log(cwd === process.cwd(), cwd.startsWith("/"));
|
||||
|
||||
// A stdlib-global snapshot (`const proc = globalThis.process`) lowers to
|
||||
// alias plumbing — no storage, no code — and reads through it resolve to
|
||||
// the global itself.
|
||||
const proc = globalThis.process;
|
||||
console.log(proc.pid === process.pid, proc.platform === process.platform);
|
||||
console.log(proc.argv === process.argv);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// The snapshot's declaration emits no storage: only reads that resolve
|
||||
// THROUGH the global (members, calls) can exist. A read that needs the
|
||||
// value reports the declaration's cause — the binding, the global it
|
||||
// snapshots and why neither holds a value — and the use site carries the
|
||||
// inherited-blocker cascade instead of "a binding form with no lowering".
|
||||
function label(): unknown {
|
||||
const proc = globalThis.process;
|
||||
return proc;
|
||||
}
|
||||
console.log(label());
|
||||
@@ -0,0 +1,10 @@
|
||||
// An initializer is never dropped. This declaration's type has no lowering,
|
||||
// so the declaration is refused where it is written — it cannot compile away
|
||||
// with the call in its initializer, which would run under Node.
|
||||
function touch(): number {
|
||||
return 1;
|
||||
}
|
||||
function run(): void {
|
||||
const ref: WeakRef<object> = touch() as unknown as WeakRef<object>;
|
||||
}
|
||||
run();
|
||||
@@ -0,0 +1,17 @@
|
||||
stdlib-snapshot-value-use.ts:7:9 - error SC2020: the stdlib-global snapshot 'proc' ('process') cannot be used as a value: the declaration lowers to alias plumbing (no storage, no code), and the global it names is not a value scriptc can hold
|
||||
|
||||
6 | function label(): unknown {
|
||||
7 | const proc = globalThis.process;
|
||||
| ^~~~
|
||||
8 | return proc;
|
||||
|
||||
hint: read the members through the global directly ('process.member') instead of passing 'proc' around as a value
|
||||
|
||||
stdlib-snapshot-value-use.ts:8:10 - error SC2004: uses of 'proc' inherit the blocker on its declaration
|
||||
|
||||
7 | const proc = globalThis.process;
|
||||
8 | return proc;
|
||||
| ^~~~
|
||||
9 | }
|
||||
|
||||
hint: the declaration of 'proc' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
@@ -0,0 +1,8 @@
|
||||
unmappable-declaration-initializer.ts:8:9 - error SC2020: 'WeakRef<object>' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
7 | function run(): void {
|
||||
8 | const ref: WeakRef<object> = touch() as unknown as WeakRef<object>;
|
||||
| ^~~
|
||||
9 | }
|
||||
|
||||
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
|
||||
Reference in New Issue
Block a user