mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
Read builtins into untyped TypeScript slots as Node's values (#2556)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */
|
||||
import { globalMemberValue } from "../global-values.js";
|
||||
import { globalMemberValue, tsUntypedGlobalValue } from "../global-values.js";
|
||||
import { predeclareForwardClass } from "../lower-stmts.js";
|
||||
import { cjsMemberAliasValueOf } from "../cjs-exports-object.js";
|
||||
import { builtinClassValue, builtinMemberClassKind } from "../builtins/builtin-class-dyn.js";
|
||||
@@ -825,6 +825,8 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(expr))) {
|
||||
return { kind: "strLit", value: `[builtin ${canonical}]`, type: STRING, loc };
|
||||
}
|
||||
const untyped = tsUntypedGlobalValue(lowerer, expr, canonical, loc);
|
||||
if (untyped !== null) return untyped;
|
||||
// The families with a WHY: each hint states what makes the
|
||||
// surface genuinely non-static (or what to use instead).
|
||||
const globalHints: Record<string, string | undefined> = {
|
||||
|
||||
@@ -47,6 +47,7 @@ import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.
|
||||
import { builtinGlobalValue } from "./property-access.js";
|
||||
import { holdsEvolvingArray } from "../lower-stmts.js";
|
||||
import { builtinNamespaceValue } from "../builtin-namespaces.js";
|
||||
import { tsUntypedGlobalValue } from "../global-values.js";
|
||||
|
||||
/** `{ a: 1, b: "x" }` → recordLit. The record type comes from the
|
||||
* contextual type when tsc has one (annotated declarations, arguments,
|
||||
@@ -446,8 +447,11 @@ export function lowerDynObjectLiteral(
|
||||
// A PURE member read a JS file cannot lower (a namespace object
|
||||
// in an export aggregate): the slot takes a boxed fence closure —
|
||||
// the diagnostics defer to the runtime-fence ledger, the object
|
||||
// builds, and only USING the member stops the run.
|
||||
const pureMember = ts.isIdentifier(valueExpr);
|
||||
// builds, and only USING the member stops the run. A TypeScript
|
||||
// source keeps the compile-time refusal: the closure would answer
|
||||
// typeof "function" and undefined members where Node reads the
|
||||
// value.
|
||||
const pureMember = ts.isIdentifier(valueExpr) && isJsSourceFile(ts.getSourceFileOfNode(prop));
|
||||
if (!(err instanceof PoisonError) || !pureMember) throw err;
|
||||
const fence = lowerer.deferToRuntimeFence(propDiagsBefore, prop, {
|
||||
kind: "closure",
|
||||
@@ -3540,7 +3544,8 @@ function shorthandBuiltinValue(
|
||||
const name = ts.symbolName(symbol);
|
||||
return (
|
||||
(js ? builtinNamespaceValue(lowerer, name, locOf(prop.name)) : null) ??
|
||||
builtinGlobalValue(name, locOf(prop.name), js)
|
||||
builtinGlobalValue(name, locOf(prop.name), js) ??
|
||||
tsUntypedGlobalValue(lowerer, prop.name as ts.Identifier, name, locOf(prop.name))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -138,6 +138,55 @@ export function jsGlobalValueByName(
|
||||
);
|
||||
}
|
||||
|
||||
/** Globals whose values a TypeScript read keeps refused: the Array
|
||||
* constructor value has none of Array's static members (`o.Array.isArray`
|
||||
* would read undefined), and the timer values lack the
|
||||
* util.promisify.custom getter Node's util.inspect lists. */
|
||||
const UNTYPED_UNMODELED: ReadonlySet<string> = new Set(["Array", "setTimeout", "setImmediate"]);
|
||||
|
||||
/** The value a TypeScript source reads for the declared global `name` at
|
||||
* `node` when the checker types the slot it flows into as any or unknown
|
||||
* (`const o: any = { j: JSON }`, a call to a parameter typed unknown): the
|
||||
* value a JavaScript source reads. Null in JavaScript sources, in typed
|
||||
* slots, and for a global without a value, whose read keeps its SC2020
|
||||
* refusal. Null for the primitive constructors too: a TypeScript read of
|
||||
* String, Number, Boolean or BigInt is the closure with the typed
|
||||
* signature (lowerIdentifierExpr), and `{ String }` must equal it. */
|
||||
export function tsUntypedGlobalValue(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Expression,
|
||||
name: string,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(node)) || UNTYPED_UNMODELED.has(name)) return null;
|
||||
if (name === "String" || name === "Number" || name === "Boolean" || name === "BigInt")
|
||||
return null;
|
||||
if (!flowsIntoUntypedSlot(lowerer, node)) return null;
|
||||
return jsGlobalValueByName(lowerer, name, loc);
|
||||
}
|
||||
|
||||
/** Whether the checker types the slot `node` flows into as any or unknown.
|
||||
* A member of an object or array literal whose own slot is any has no
|
||||
* contextual type, so the literal's slot decides. */
|
||||
function flowsIntoUntypedSlot(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
let value: ts.Expression = node;
|
||||
for (;;) {
|
||||
const slot = lowerer.checker.getContextualType(value);
|
||||
if (slot !== undefined) return (slot.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0;
|
||||
const parent = value.parent;
|
||||
if (
|
||||
(ts.isPropertyAssignment(parent) && parent.initializer === value) ||
|
||||
ts.isShorthandPropertyAssignment(parent)
|
||||
) {
|
||||
value = parent.parent as ts.ObjectLiteralExpression;
|
||||
} else if (ts.isArrayLiteralExpression(parent) || ts.isParenthesizedExpression(parent)) {
|
||||
value = parent;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** jsGlobalValueByName for the globals other than the interned builtin
|
||||
* functions and constructors (builtinGlobalValue). */
|
||||
export function jsSpecialGlobalValue(lowerer: Lowerer, name: string, loc: SrcLoc): IrExpr | null {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/builtin-values-untyped-slots.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/object-to-string-tag-inspect.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/builtin-values-without-lowering.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -1220,6 +1220,9 @@ void scr_dyn_install_species_symbol(ScrDyn *key);
|
||||
/* Records each well-known symbol the symbol unit creates. */
|
||||
void scr_dyn_install_well_known_symbol(ScrDyn *key);
|
||||
void scr_dyn_install_to_string_tag_symbol(ScrDyn *key);
|
||||
/* Symbol.toStringTag (borrowed), or NULL before the program first
|
||||
* evaluates it. */
|
||||
const ScrDyn *scr_dyn_to_string_tag_symbol(void);
|
||||
/* Whether key is Symbol.asyncIterator. */
|
||||
bool scr_dyn_is_async_iterator_symbol(const ScrDyn *key);
|
||||
/* Symbol.asyncIterator (+1), or NULL before the symbol unit installed it. */
|
||||
|
||||
@@ -1708,7 +1708,48 @@ static bool insp_obj_shows(const ScrDynProp *ent) {
|
||||
return ent->enumerable || (g_show_hidden && !(ent->key_len && ent->key[0] == '%'));
|
||||
}
|
||||
|
||||
/* The @@toStringTag Node brackets after a plain object's prefix
|
||||
* (`Object [Math] {}`), +1: a nonempty string read through the object and
|
||||
* its prototypes, unless the object's own tag is listed with its keys
|
||||
* (an enumerable one, or any under showHidden). NULL otherwise, and when
|
||||
* the tag's getter throws, as in Node. */
|
||||
static ScrStr *insp_plain_object_tag(ScrDyn *d) {
|
||||
const ScrDyn *key = scr_dyn_to_string_tag_symbol();
|
||||
if (!key) return NULL;
|
||||
ScrDynProp own;
|
||||
if (scr_dyn_own_symbol_prop(d, key, &own) && (own.enumerable || g_show_hidden)) return NULL;
|
||||
ScrDyn *tag = scr_dyn_symbol_key_get(d, key, true);
|
||||
if (!tag) {
|
||||
if (scr_exc_pending()) scr_exc_clear();
|
||||
return NULL;
|
||||
}
|
||||
ScrStr *out = tag->kind == SCR_DYN_STR && tag->v.str->len ? scr_str_retain(tag->v.str) : NULL;
|
||||
scr_dyn_release(tag);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Node's getPrefix for a plain object, without the trailing space: "",
|
||||
* "Object [tag]" ("Object" for the tag "Object"), or the null-prototype
|
||||
* marker followed by " [tag]". +1. */
|
||||
static ScrStr *insp_plain_object_prefix(const ScrDyn *d, const ScrStr *tag) {
|
||||
InspBuf out = {0};
|
||||
if (d->null_proto) ib_cstr(&out, "[Object: null prototype]");
|
||||
else if (tag) ib_cstr(&out, "Object");
|
||||
if (tag && !(tag->len == 6 && memcmp(tag->data, "Object", 6) == 0)) {
|
||||
ib_cstr(&out, " [");
|
||||
ib_bytes(&out, tag->data, tag->len);
|
||||
ib_char(&out, ']');
|
||||
}
|
||||
return ib_take(&out);
|
||||
}
|
||||
|
||||
static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char *cls) {
|
||||
/* A plain object with a tag takes Node's tagged prefix in all three
|
||||
* forms: `Object [Math] {}`, `[Object [Math]]` past the depth limit,
|
||||
* and `Object [Math] { key: value }`. */
|
||||
ScrStr *tag = cls ? NULL : insp_plain_object_tag(d);
|
||||
ScrStr *tagged = tag ? insp_plain_object_prefix(d, tag) : NULL;
|
||||
if (tag) scr_str_release(tag);
|
||||
size_t shown = 0;
|
||||
for (size_t i = 0; i < scr_dyn_obj_count(d); i++) {
|
||||
ScrDynProp ent = scr_dyn_obj_prop_at(d, i);
|
||||
@@ -1719,6 +1760,15 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char
|
||||
ScrDynProp ent;
|
||||
shown += scr_insp_symbol_entry(d, i, &ent);
|
||||
}
|
||||
if (tagged && (shown == 0 || recurse > depth)) {
|
||||
InspBuf out = {0};
|
||||
bool bracket = shown != 0 && !d->null_proto;
|
||||
if (bracket) ib_char(&out, '[');
|
||||
ib_bytes(&out, tagged->data, tagged->len);
|
||||
ib_cstr(&out, bracket ? "]" : shown == 0 ? " {}" : "");
|
||||
scr_str_release(tagged);
|
||||
return shown == 0 ? ib_take(&out) : insp_styled(ib_take(&out), INSP_SPECIAL);
|
||||
}
|
||||
if (shown == 0) {
|
||||
if (cls) return scr_insp_class_tag(cls, " {}");
|
||||
return d->null_proto ? scr_str_new("[Object: null prototype] {}", 27)
|
||||
@@ -1771,6 +1821,7 @@ static ScrStr *scr_insp_obj(ScrDyn *d, double recurse, double depth, const char
|
||||
scr_str_release(entry);
|
||||
}
|
||||
ScrStr *base = cls ? scr_str_new(cls, strlen(cls))
|
||||
: tagged ? tagged
|
||||
: d->null_proto ? scr_str_new("[Object: null prototype]", 24)
|
||||
: scr_str_new("", 0);
|
||||
ScrStr *b0 = scr_str_new("{", 1);
|
||||
|
||||
@@ -261,6 +261,7 @@ void scr_dyn_install_to_string_tag_symbol(ScrDyn *key) {
|
||||
scr_to_string_tag_symbol = scr_dyn_retain(key);
|
||||
scr_atexit(scr_to_string_tag_symbol_cleanup);
|
||||
}
|
||||
const ScrDyn *scr_dyn_to_string_tag_symbol(void) { return scr_to_string_tag_symbol; }
|
||||
|
||||
void scr_dyn_install_iterator_symbol(ScrDyn *key) {
|
||||
if (scr_iterator_symbol) return;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// Builtins read into any- and unknown-typed slots hold the values Node reads.
|
||||
function take(x: unknown): any {
|
||||
return x;
|
||||
}
|
||||
function give(): any {
|
||||
return Math;
|
||||
}
|
||||
|
||||
const record: Record<string, unknown> = {
|
||||
json: JSON,
|
||||
math: Math,
|
||||
buffer: Buffer,
|
||||
};
|
||||
const loose: any = {
|
||||
JSON,
|
||||
Reflect,
|
||||
k: Map,
|
||||
bytes: Uint8Array,
|
||||
clone: structuredClone,
|
||||
};
|
||||
const list: any[] = [JSON, Math, atob, btoa];
|
||||
const json: any = record.json;
|
||||
const math: any = record.math;
|
||||
|
||||
console.log(typeof json, typeof math, typeof loose.Reflect, typeof loose.clone);
|
||||
console.log(
|
||||
json === loose.JSON,
|
||||
json === list[0],
|
||||
math === list[1],
|
||||
math === give(),
|
||||
);
|
||||
console.log(
|
||||
take(JSON) === json,
|
||||
take(Map) === loose.k,
|
||||
take(Buffer) === record.buffer,
|
||||
);
|
||||
|
||||
console.log(json.stringify({ a: [1, "x"] }), json.parse("[2,3]").length);
|
||||
console.log(math.max(3, 9, 4), math.PI.toFixed(5), list[1].sqrt(16));
|
||||
console.log(loose.Reflect.ownKeys({ b: 1, c: 2 }));
|
||||
console.log(new loose.k([[1, "one"]]).get(1), new loose.bytes(3).length);
|
||||
console.log((record.buffer as any).from("hi").toString("hex"));
|
||||
console.log(loose.clone({ n: [1, 2] }).n[1], list[2]("aGk="), list[3]("hi"));
|
||||
console.log(math.max.name, math.max.length, json.parse.name, loose.clone.name);
|
||||
|
||||
console.log(Math, JSON, Reflect);
|
||||
console.log({ nested: { math: record.math } });
|
||||
console.log(Object.prototype.toString.call(math), String(json));
|
||||
console.log(JSON.stringify({ math, json }));
|
||||
console.log(
|
||||
Object.keys(math).length,
|
||||
Object.getOwnPropertyNames(json).join(","),
|
||||
);
|
||||
@@ -0,0 +1,37 @@
|
||||
// util.inspect brackets a plain object's Symbol.toStringTag after its prefix.
|
||||
const hidden = {};
|
||||
Object.defineProperty(hidden, Symbol.toStringTag, { value: "X" });
|
||||
const keyed = { a: 1 };
|
||||
Object.defineProperty(keyed, Symbol.toStringTag, { value: "Y" });
|
||||
const bare = Object.create(null);
|
||||
Object.defineProperty(bare, Symbol.toStringTag, { value: "N" });
|
||||
const bareKeyed = Object.create(null);
|
||||
bareKeyed.k = 1;
|
||||
Object.defineProperty(bareKeyed, Symbol.toStringTag, { value: "N" });
|
||||
const empty = {};
|
||||
Object.defineProperty(empty, Symbol.toStringTag, { value: "" });
|
||||
const named = { z: 1 };
|
||||
Object.defineProperty(named, Symbol.toStringTag, { value: "Object" });
|
||||
const getter = { c: 1 };
|
||||
Object.defineProperty(getter, Symbol.toStringTag, { get: () => "G" });
|
||||
const throwing = { c: 1 };
|
||||
Object.defineProperty(throwing, Symbol.toStringTag, {
|
||||
get() {
|
||||
throw new Error("boom");
|
||||
},
|
||||
});
|
||||
const numeric = { c: 1 };
|
||||
Object.defineProperty(numeric, Symbol.toStringTag, { value: 7 });
|
||||
const inherited = Object.create(hidden);
|
||||
const listed = { [Symbol.toStringTag]: "Z", b: 2 };
|
||||
|
||||
console.log(hidden, keyed, bare, bareKeyed);
|
||||
console.log(empty, named, getter, throwing, numeric);
|
||||
console.log(inherited, listed, [keyed], { keyed });
|
||||
console.log({ a: { b: { keyed, bareKeyed, named, hidden } } });
|
||||
console.log(Math, JSON, Reflect, { a: { b: { c: Math } } });
|
||||
console.log(
|
||||
Object.prototype.toString.call(hidden),
|
||||
String(keyed),
|
||||
`${inherited}`,
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
// Builtins without a lowered value refuse at compile time in TypeScript,
|
||||
// including as object literal members.
|
||||
function take(x: unknown): unknown {
|
||||
return x;
|
||||
}
|
||||
|
||||
const inferred = { json: JSON };
|
||||
const loose: any = { intl: Intl, proxy: Proxy };
|
||||
const timers: Record<string, unknown> = { setTimeout };
|
||||
const list: any[] = [Array, parseInt];
|
||||
console.log(inferred, loose, timers, list, take(process));
|
||||
@@ -0,0 +1,53 @@
|
||||
builtin-values-without-lowering.ts:7:7 - error SC2009: values of type '{ json: JSON; }' cannot be compiled: the record shape is supported, but its member 'json.parse' has type '{ (text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined): any; (text: string): unknown; (text: string, reviver: (this: unknown, key: string, value: unknown) => unknown): unknown; }', which does not compile
|
||||
|
||||
6 |
|
||||
7 | const inferred = { json: JSON };
|
||||
| ^~~~~~~~
|
||||
8 | const loose: any = { intl: Intl, proxy: Proxy };
|
||||
|
||||
hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown')
|
||||
|
||||
builtin-values-without-lowering.ts:7:18 - error SC2009: values of type '{ json: JSON; }' cannot be compiled: the record shape is supported, but its member 'json.parse' has type '{ (text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined): any; (text: string): unknown; (text: string, reviver: (this: unknown, key: string, value: unknown) => unknown): unknown; }', which does not compile
|
||||
|
||||
6 |
|
||||
7 | const inferred = { json: JSON };
|
||||
| ^~~~~~~~~~~~~~
|
||||
8 | const loose: any = { intl: Intl, proxy: Proxy };
|
||||
|
||||
hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown')
|
||||
|
||||
builtin-values-without-lowering.ts:8:28 - error SC2020: 'Intl' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
7 | const inferred = { json: JSON };
|
||||
8 | const loose: any = { intl: Intl, proxy: Proxy };
|
||||
| ^~~~
|
||||
9 | const timers: Record<string, unknown> = { setTimeout };
|
||||
|
||||
hint: native Intl supports default Unicode grapheme segmentation and the composed new Intl.NumberFormat("en-US").format(x); locale negotiation and other ICU-backed operations remain unsupported
|
||||
|
||||
builtin-values-without-lowering.ts:9:43 - error SC2020: 'setTimeout' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
8 | const loose: any = { intl: Intl, proxy: Proxy };
|
||||
9 | const timers: Record<string, unknown> = { setTimeout };
|
||||
| ^~~~~~~~~~
|
||||
10 | const list: any[] = [Array, parseInt];
|
||||
|
||||
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
|
||||
|
||||
builtin-values-without-lowering.ts:10:22 - error SC2020: 'Array' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
9 | const timers: Record<string, unknown> = { setTimeout };
|
||||
10 | const list: any[] = [Array, parseInt];
|
||||
| ^~~~~
|
||||
11 | console.log(inferred, loose, timers, list, take(process));
|
||||
|
||||
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
|
||||
|
||||
builtin-values-without-lowering.ts:11:13 - error SC2004: uses of 'inferred' inherit the blocker on its declaration
|
||||
|
||||
10 | const list: any[] = [Array, parseInt];
|
||||
11 | console.log(inferred, loose, timers, list, take(process));
|
||||
| ^~~~~~~~
|
||||
12 |
|
||||
|
||||
hint: the declaration of 'inferred' did not compile — fix the diagnostic reported there and these sites clear with it
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4051, "san": 5400 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2793, "san": 3480 }
|
||||
Reference in New Issue
Block a user