mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
Support native startup caches and assignment expressions
- Share symbol-keyed global caches across statically compiled modules. - Preserve assignment results and evaluation order for native fields, accessors, and nullish defaults. - Read omitted options correctly in specialized JavaScript package calls.
This commit is contained in:
@@ -4448,6 +4448,14 @@ function emitDynamicLibCall(state: LibCallState): Temp {
|
||||
return finish(`scr_dyn_key_set(${arg(0)}, ${arg(1)}, ${arg(2)})`);
|
||||
case "dyn.keyDelete":
|
||||
return finish(`scr_dyn_key_delete(${arg(0)}, ${arg(1)})`);
|
||||
case "dyn.globalSymbolGet":
|
||||
return finish(`scr_dyn_global_symbol_get(${arg(0)})`);
|
||||
case "dyn.globalSymbolSet":
|
||||
return finish(`scr_dyn_global_symbol_set(${arg(0)}, ${arg(1)})`);
|
||||
case "dyn.globalSymbolHas":
|
||||
return finish(`scr_dyn_global_symbol_has(${arg(0)})`);
|
||||
case "dyn.globalSymbolDelete":
|
||||
return finish(`scr_dyn_global_symbol_delete(${arg(0)})`);
|
||||
case "dyn.iterPack":
|
||||
// Destructuring/for-of pack over a dyn source: both borrowed,
|
||||
// fresh array +1; throws V8's not-iterable TypeError on
|
||||
|
||||
@@ -508,6 +508,10 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"json.stringifyReplacer": "scr_json_stringify_replacer",
|
||||
"dyn.keySet": "scr_dyn_key_set",
|
||||
"dyn.keyDelete": "scr_dyn_key_delete",
|
||||
"dyn.globalSymbolGet": "scr_dyn_global_symbol_get",
|
||||
"dyn.globalSymbolSet": "scr_dyn_global_symbol_set",
|
||||
"dyn.globalSymbolHas": "scr_dyn_global_symbol_has",
|
||||
"dyn.globalSymbolDelete": "scr_dyn_global_symbol_delete",
|
||||
"dyn.iterPack": "scr_dyn_iter_pack",
|
||||
"dyn.arrLen": "scr_dyn_arr_len",
|
||||
"dyn.arrAt": "scr_dyn_arr_at",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import type { IrExpr } from "../../../ir/ir.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { stdlibGlobalNameOf } from "../surfaces.js";
|
||||
|
||||
/** Symbol keys have runtime identity and never overlap the statically
|
||||
* resolved builtin globals. Keep their shared native storage across modules. */
|
||||
export function globalSymbolKey(lowerer: Lowerer, receiver: ts.Expression, key: ts.Expression): IrExpr | null {
|
||||
if (stdlibGlobalNameOf(lowerer, receiver) !== "globalThis" || lowerer.mapTypeOf(lowerer.typeOf(key))?.kind !== "symbol") return null;
|
||||
const value = lowerer.lowerExpr(key);
|
||||
return value.type.kind === "symbol" ? value : null;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { DYN, STRING, VOID, isUnitType, type IrExpr, type IrStmt, type IrType } from "../../../ir/ir.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { arrayValueRead, arrayValueStore } from "../array-values.js";
|
||||
import { fenceNodeModuleMutation } from "../lower-node-module.js";
|
||||
import { globalSymbolKey } from "./global-symbols.js";
|
||||
import { tryLowerExpression } from "./try-lower-expression.js";
|
||||
|
||||
/** The reference and its read evaluate once; the write exists only in the
|
||||
* nullish branch. In particular an accessor setter never runs for a kept value. */
|
||||
export function lowerNullishAssignment(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
const prefix: IrStmt[] = [];
|
||||
const save = (value: IrExpr, name: string): IrExpr => {
|
||||
const local = lowerer.declareHiddenLocal(name, value.type);
|
||||
prefix.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc });
|
||||
return varRef(local.id, value.type, value.loc);
|
||||
};
|
||||
let target = expr.left;
|
||||
while (ts.isParenthesizedExpression(target)) target = target.expression;
|
||||
let read: IrExpr;
|
||||
let storage: IrType;
|
||||
let write: (value: IrExpr) => IrStmt;
|
||||
if (ts.isIdentifier(target)) {
|
||||
const binding = lowerer.resolveWritable(target);
|
||||
if (!binding) lowerer.rejectUnresolved(target, "nullish assignment to a non-writable binding");
|
||||
storage = binding.type;
|
||||
read = varRef(binding.id, binding.type, locOf(target));
|
||||
write = (value) => ({ kind: "assign", localId: binding.id, value, loc });
|
||||
} else if (ts.isPropertyAccessExpression(target) || ts.isElementAccessExpression(target)) {
|
||||
fenceNodeModuleMutation(lowerer, target, "assignment");
|
||||
if (target.questionDotToken || target.expression.kind === ts.SyntaxKind.SuperKeyword) {
|
||||
lowerer.unsupported("SC1090", target, "nullish assignment through optional or super references");
|
||||
}
|
||||
const globalKey = ts.isElementAccessExpression(target) ? globalSymbolKey(lowerer, target.expression, target.argumentExpression) : null;
|
||||
if (globalKey) {
|
||||
const key = save(globalKey, "%nullishKey");
|
||||
storage = DYN;
|
||||
read = { kind: "libCall", fn: "dyn.globalSymbolGet", args: [key], type: DYN, loc };
|
||||
write = (value) => ({ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.globalSymbolSet", args: [key, value], type: VOID, loc }, loc });
|
||||
} else if (ts.isPropertyAccessExpression(target)) {
|
||||
const probed = tryLowerExpression(lowerer, target.expression);
|
||||
if (probed?.type.kind === "dyn") {
|
||||
const receiver = save(probed, "%nullishReceiver");
|
||||
const key: IrExpr = { kind: "strLit", value: target.name.text, type: STRING, loc };
|
||||
storage = DYN;
|
||||
read = { kind: "dynKeyGet", key, value: receiver, type: DYN, loc };
|
||||
write = (value) => ({ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySet", args: [receiver, key, value], type: VOID, loc }, loc });
|
||||
} else {
|
||||
const field = lowerer.fieldTarget(target);
|
||||
if (!field) lowerer.unsupported("SC1090", target, "nullish assignment to an unsupported property");
|
||||
field.obj = save(field.obj, "%nullishReceiver");
|
||||
read = lowerer.fieldGetExpr(field, locOf(target), target);
|
||||
storage = field.fieldType;
|
||||
const blame = target;
|
||||
write = (value) => lowerer.fieldSetStmt(field, value, loc, blame);
|
||||
}
|
||||
} else {
|
||||
const receiver = save(lowerer.lowerExpr(target.expression), "%nullishReceiver");
|
||||
let key = lowerer.lowerExpr(target.argumentExpression);
|
||||
if (receiver.type.kind === "dyn") {
|
||||
if (key.type.kind !== "string" && key.type.kind !== "f64" && key.type.kind !== "bool") {
|
||||
lowerer.unsupported("SC1090", target.argumentExpression, "nullish assignment with non-scalar property keys");
|
||||
}
|
||||
key = save(lowerer.ensureString(key, target.argumentExpression), "%nullishKey");
|
||||
storage = DYN;
|
||||
read = { kind: "dynKeyGet", key, value: receiver, type: DYN, loc };
|
||||
write = (value) => ({ kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySet", args: [receiver, key, value], type: VOID, loc }, loc });
|
||||
} else if (receiver.type.kind === "array" && key.type.kind === "f64") {
|
||||
key = save(key, "%nullishKey");
|
||||
storage = receiver.type.elem;
|
||||
read = arrayValueRead(lowerer, receiver, key, storage, locOf(target));
|
||||
const element = storage;
|
||||
write = (value) => arrayValueStore(lowerer, receiver, key, value, element, loc);
|
||||
} else {
|
||||
lowerer.unsupported("SC1090", target, "nullish assignment through this indexed receiver");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lowerer.unsupported("SC1090", target, "nullish assignment to this reference");
|
||||
}
|
||||
const finish = (result: IrExpr): IrExpr => prefix.length === 0 ? result : { kind: "seqExpr", stmts: prefix, result, type: result.type, loc };
|
||||
if (!isUnitType(read.type) && read.type.kind !== "dyn" && read.type.kind !== "jsval" &&
|
||||
(read.type.kind !== "union" || !lowerer.unions.get(read.type.unionId)?.arms.some(isUnitType))) {
|
||||
return finish(read);
|
||||
}
|
||||
const value = lowerer.lowerExprExpecting(expr.right, storage);
|
||||
const assigned = lowerer.declareHiddenLocal("%nullishValue", value.type);
|
||||
const ref = varRef(assigned.id, value.type, loc);
|
||||
const result = lowerer.coerceInto(expr.right, ref, read.type);
|
||||
const right: IrExpr = {
|
||||
kind: "seqExpr", stmts: [{ kind: "varDecl", localId: assigned.id, init: value, loc }, write(ref)],
|
||||
result, type: result.type, loc,
|
||||
};
|
||||
if (isUnitType(read.type)) {
|
||||
prefix.push({ kind: "exprStmt", expr: read, loc });
|
||||
return finish(right);
|
||||
}
|
||||
return finish({ kind: "nullish", left: read, right, type: read.type, loc });
|
||||
}
|
||||
@@ -33,6 +33,8 @@ import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "./
|
||||
import { countedFor, numLit, varRef } from "../../ir/build.js";
|
||||
import { unionWideningTags } from "../../ir/analysis.js";
|
||||
import { isSafeToDiscard, isSafeToMoveConditionEarlier, isSafeToRepeat } from "./expressions/evaluation-safety.js";
|
||||
import { globalSymbolKey } from "./expressions/global-symbols.js";
|
||||
import { lowerNullishAssignment } from "./expressions/nullish-assignment.js";
|
||||
import { hasOptionalChainGuard, isOptionalChainTail, isRequireMainFilename } from "./expressions/optional-chains.js";
|
||||
import { conditionalSpreadOf, foldedStringKeyOf } from "./expressions/object-literals.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
@@ -2069,6 +2071,17 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
expr,
|
||||
);
|
||||
}
|
||||
// An inferred package-JS call specializes a parameter to the
|
||||
// caller's own record shape. Optional option names omitted by that
|
||||
// caller read undefined; the missing field is not a failed type
|
||||
// assertion. Known prototype members were handled/fenced above.
|
||||
if (recvShape && !recvShape.tuple && lowerer.implicitParamTypes !== null &&
|
||||
isJsSourceFile(expr.getSourceFile()) && npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null) {
|
||||
return {
|
||||
kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: recvLowered, loc }],
|
||||
result: dynUndefinedExpr(loc), type: DYN, loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
// An ABSTRACT property through an abstract-typed receiver: the
|
||||
// declaration is erased at runtime — Node defines no field for it,
|
||||
@@ -4303,6 +4316,8 @@ export function lowerOptionalNumber(
|
||||
// the PROPERTY spelling answers the identity token instead — see
|
||||
// lowerPropertyAccess's globalThis rule).
|
||||
if (!expr.questionDotToken && stdlibGlobalNameOf(lowerer, expr.expression) === "globalThis") {
|
||||
const symbol = globalSymbolKey(lowerer, expr.expression, expr.argumentExpression);
|
||||
if (symbol) return { kind: "libCall", fn: "dyn.globalSymbolGet", args: [symbol], type: DYN, loc: locOf(expr) };
|
||||
return { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc: locOf(expr) };
|
||||
}
|
||||
// `req.headers["x-name"]` — the computed twin of `req.headers.host`
|
||||
@@ -5136,6 +5151,12 @@ export function lowerElementCompound(lowerer: Lowerer, expr: ts.BinaryExpression
|
||||
export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression): IrStmt {
|
||||
const target = expr.left as ts.ElementAccessExpression;
|
||||
fenceNodeModuleMutation(lowerer, target, "assignment");
|
||||
const globalKey = globalSymbolKey(lowerer, target.expression, target.argumentExpression);
|
||||
if (globalKey) {
|
||||
const value = lowerer.lowerExprExpecting(expr.right, DYN);
|
||||
const loc = locOf(expr);
|
||||
return { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.globalSymbolSet", args: [globalKey, value], type: VOID, loc }, loc };
|
||||
}
|
||||
// `process.env[key] = v` — the computed twin of the dotted env write:
|
||||
// setenv(3), string keys and string values only.
|
||||
if (lowerer.isProcessEnv(target.expression) && !target.questionDotToken) {
|
||||
@@ -6123,6 +6144,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
if (cacheHas) return cacheHas;
|
||||
|
||||
if (op === ts.SyntaxKind.EqualsToken || (op >= ts.SyntaxKind.FirstCompoundAssignment && op <= ts.SyntaxKind.LastCompoundAssignment)) {
|
||||
if (op === ts.SyntaxKind.QuestionQuestionEqualsToken) return lowerNullishAssignment(lowerer, expr);
|
||||
if (ts.isPropertyAccessExpression(expr.left) || ts.isElementAccessExpression(expr.left)) {
|
||||
fenceNodeModuleMutation(lowerer, expr.left, "assignment");
|
||||
}
|
||||
@@ -6147,6 +6169,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
}
|
||||
const indexedCompound = COMPOUND_ASSIGN_OPS[op];
|
||||
if (indexedCompound !== undefined && ts.isElementAccessExpression(expr.left)) {
|
||||
if (symbolFieldInfo(lowerer, expr.left)) return lowerFieldCompoundValue(lowerer, expr.left, indexedCompound, expr.right, loc);
|
||||
return lowerElementCompound(lowerer, expr, indexedCompound);
|
||||
}
|
||||
if (indexedCompound !== undefined && ts.isIdentifier(expr.left)) {
|
||||
@@ -6169,6 +6192,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
const value = lowerCompoundValueToTarget(lowerer, expr, indexedCompound, target);
|
||||
return { kind: "assignExpr", localId: target.id, value, type: target.type, loc };
|
||||
}
|
||||
return lowerFieldCompoundValue(lowerer, expr.left, indexedCompound, expr.right, loc);
|
||||
}
|
||||
// `events.defaultMaxListeners = v` — the module-property write
|
||||
// Node validates (validateNumber(n, 'defaultMaxListeners', 0)):
|
||||
@@ -6294,6 +6318,22 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const field = lowerer.fieldTarget(expr.left);
|
||||
if (field) {
|
||||
const recvTmp = lowerer.declareHiddenLocal("%setReceiver", field.obj.type);
|
||||
const receiver = field.obj;
|
||||
field.obj = varRef(recvTmp.id, receiver.type, loc);
|
||||
const value = lowerer.lowerExprExpecting(expr.right, field.fieldType);
|
||||
const valTmp = lowerer.declareHiddenLocal("%setValue", value.type);
|
||||
const result = varRef(valTmp.id, value.type, loc);
|
||||
return {
|
||||
kind: "seqExpr", stmts: [
|
||||
{ kind: "varDecl", localId: recvTmp.id, init: receiver, loc },
|
||||
{ kind: "varDecl", localId: valTmp.id, init: value, loc },
|
||||
lowerer.fieldSetStmt(field, result, loc, expr.left),
|
||||
], result, type: result.type, loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
// Destructuring assignment in VALUE position (`(() => [i] = [i+1])()`,
|
||||
@@ -8257,6 +8297,8 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
* and dyn/unknown stay fenced. Keys are literal strings — a computed key
|
||||
* over a shape would need the runtime key table. */
|
||||
function lowerInExpression(lowerer: Lowerer, expr: ts.BinaryExpression, loc: SrcLoc): IrExpr {
|
||||
const globalKey = globalSymbolKey(lowerer, expr.right, expr.left);
|
||||
if (globalKey) return { kind: "libCall", fn: "dyn.globalSymbolHas", args: [globalKey], type: BOOL, loc };
|
||||
// `#name in obj` — the ergonomic brand check (ES2022) — resolves
|
||||
// before any string-key machinery: the left operand is a private
|
||||
// NAME, not a value.
|
||||
@@ -9903,6 +9945,13 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
op: CompoundOp,
|
||||
rhsNode: ts.Expression | null,
|
||||
loc: SrcLoc,): IrStmt {
|
||||
return { kind: "exprStmt", expr: lowerFieldCompoundValue(lowerer, access, op, rhsNode, loc), loc };
|
||||
}
|
||||
|
||||
function lowerFieldCompoundValue(lowerer: Lowerer, access: ts.PropertyAccessExpression | ts.ElementAccessExpression,
|
||||
op: CompoundOp,
|
||||
rhsNode: ts.Expression | null,
|
||||
loc: SrcLoc,): IrExpr {
|
||||
if (access.expression.kind === ts.SyntaxKind.SuperKeyword) {
|
||||
lowerer.unsupported("SC1090", access, "compound assignment through 'super' (read and write separately)");
|
||||
}
|
||||
@@ -9933,14 +9982,14 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
? { kind: "dynCheck", value: rhs, type: F64, loc: rhs.loc }
|
||||
: lowerOptionalNumber(lowerer, rhs, loc);
|
||||
if (numericRhs.type.kind !== "f64") lowerer.unsupported("SC1043", access);
|
||||
const value: IrExpr = { kind: "bin", op, left: cur, right: numericRhs, type: F64, loc };
|
||||
const value = save({ kind: "bin", op, left: cur, right: numericRhs, type: F64, loc }, "%compoundResult");
|
||||
const boxed: IrExpr = { kind: "dynFrom", value, type: DYN, loc };
|
||||
body.push({
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: "dyn.keySet", args: [receiver, { ...key }, boxed], type: VOID, loc },
|
||||
loc,
|
||||
});
|
||||
return { kind: "block", body, loc };
|
||||
return { kind: "seqExpr", stmts: body, result: value, type: value.type, loc };
|
||||
}
|
||||
}
|
||||
const targetOf = (): FieldTarget | null =>
|
||||
@@ -9996,8 +10045,9 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
} else {
|
||||
lowerer.unsupported("SC1043", access);
|
||||
}
|
||||
body.push(lowerer.fieldSetStmt(target, value, loc, access));
|
||||
return { kind: "block", body, loc };
|
||||
const result = save(value, "%compoundResult");
|
||||
body.push(lowerer.fieldSetStmt(target, result, loc, access));
|
||||
return { kind: "seqExpr", stmts: body, result, type: result.type, loc };
|
||||
}
|
||||
|
||||
/** Stream-rooted receivers' property surface (readableEnded, destroyed,
|
||||
|
||||
@@ -28,6 +28,8 @@ import { builtinMemberRequireDecl, builtinNamespaceDestructureModuleOf, createRe
|
||||
import { lowerEnumDeclaration } from "./lower-enums.js";
|
||||
import { abstractPropertyDeclOf, aliasTypeofNarrows, isMatchSliceType, lowerAbsenceProbe, lowerCompoundValueToTarget, lowerElementCompound, lowerGroupsProjection, matchResultNamedGroupsOf, runtimeOptionalTrueIds, symbolFieldInfo, withRuntimeOptionalNarrowed } from "./lower-exprs.js";
|
||||
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
|
||||
import { globalSymbolKey } from "./expressions/global-symbols.js";
|
||||
import { lowerNullishAssignment } from "./expressions/nullish-assignment.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerUnionFieldWrite } from "./expressions/union-field-write.js";
|
||||
import { UNSUPPORTED, checkerPanicDiag, isCheckerPanic, requiresDynamicDiag } from "../../diagnostics/diagnostic.js";
|
||||
@@ -4382,6 +4384,8 @@ function lowerBranchSwitch(
|
||||
lowerer.unsupported("SC1090", expr, "'delete' of non-property expressions");
|
||||
}
|
||||
fenceNodeModuleMutation(lowerer, target, "delete");
|
||||
const globalKey = ts.isElementAccessExpression(target) ? globalSymbolKey(lowerer, target.expression, target.argumentExpression) : null;
|
||||
if (globalKey) return { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.globalSymbolDelete", args: [globalKey], type: VOID, loc }, loc };
|
||||
const lowerKey = (): IrExpr => {
|
||||
if (ts.isPropertyAccessExpression(target)) {
|
||||
return { kind: "strLit", value: target.name.text, type: STRING, loc: locOf(target.name) };
|
||||
@@ -5255,34 +5259,7 @@ function lowerBranchSwitch(
|
||||
return { kind: "assign", localId: target.id, value, loc: locOf(expr) };
|
||||
}
|
||||
if (opKind === ts.SyntaxKind.QuestionQuestionEqualsToken) {
|
||||
// `x ??= e` on a VARIABLE, statement position: desugar to
|
||||
// `x = x ?? e` (x read once, e lazy). For a plain variable JS's
|
||||
// assign-only-when-nullish is unobservable — the value written back
|
||||
// on the non-nullish path is the same box. Property targets keep a
|
||||
// distinct fence: accessors would make the always-write observable.
|
||||
if (!ts.isIdentifier(expr.left)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr.left,
|
||||
"'??=' on non-variable targets (write it out: if (o.f === undefined) o.f = v)",
|
||||
);
|
||||
}
|
||||
const target = lowerer.resolveWritable(expr.left);
|
||||
if (!target) lowerer.rejectUnresolved(expr.left, `assignment to '${expr.left.text}' (not a writable local or module global)`);
|
||||
const loc = locOf(expr);
|
||||
if (target.type.kind !== "union") {
|
||||
// The checker says never nullish: JS neither evaluates e nor
|
||||
// assigns — the statement is a no-op (lowerNullishCoalesce's
|
||||
// trust-the-checker fold, statement form).
|
||||
return { kind: "block", body: [], loc };
|
||||
}
|
||||
const def = lowerer.unions.get(target.type.unionId);
|
||||
if (!def) lowerer.badType(expr.left, lowerer.typeOf(expr.left));
|
||||
if (!def.arms.some(isUnitType)) return { kind: "block", body: [], loc };
|
||||
const read: IrExpr = { kind: "varRef", localId: target.id, type: target.type, loc: locOf(expr.left) };
|
||||
const right = lowerer.lowerExprExpecting(expr.right, target.type);
|
||||
const value: IrExpr = { kind: "nullish", left: read, right, type: target.type, loc };
|
||||
return { kind: "assign", localId: target.id, value, loc };
|
||||
return { kind: "exprStmt", expr: lowerNullishAssignment(lowerer, expr), loc: locOf(expr) };
|
||||
}
|
||||
const compound = COMPOUND_ASSIGN_OPS[opKind];
|
||||
if (compound !== undefined) {
|
||||
|
||||
@@ -2035,6 +2035,11 @@ export type IrLibFn =
|
||||
/** Delete an ordinary checked-native object's own key. Borrows both
|
||||
* arguments; other receiver representations retain a runtime refusal. */
|
||||
| "dyn.keyDelete"
|
||||
/** Native own properties on globalThis keyed by symbol identity. */
|
||||
| "dyn.globalSymbolGet"
|
||||
| "dyn.globalSymbolSet"
|
||||
| "dyn.globalSymbolHas"
|
||||
| "dyn.globalSymbolDelete"
|
||||
/** Destructuring pack over a dyn source — `const [a, b] = d`, a
|
||||
* destructured dyn callback param (args: the source and the STATIC
|
||||
* TypeError spelling, "" when the source has none — both borrowed;
|
||||
|
||||
@@ -113,6 +113,10 @@ export const LIB_FN_SIGS: Record<IrLibFn, { argTypes: (IrType | null)[]; result:
|
||||
"json.stringifyReplacer": { argTypes: [DYN, DYN, STRING], result: DYN },
|
||||
"dyn.keySet": { argTypes: [DYN, STRING, DYN], result: VOID },
|
||||
"dyn.keyDelete": { argTypes: [DYN, STRING], result: VOID },
|
||||
"dyn.globalSymbolGet": { argTypes: [SYMBOL_T], result: DYN },
|
||||
"dyn.globalSymbolSet": { argTypes: [SYMBOL_T, DYN], result: VOID },
|
||||
"dyn.globalSymbolHas": { argTypes: [SYMBOL_T], result: BOOL },
|
||||
"dyn.globalSymbolDelete": { argTypes: [SYMBOL_T], result: VOID },
|
||||
"dyn.iterPack": { argTypes: [DYN, STRING], result: DYN },
|
||||
"dyn.arrLen": { argTypes: [DYN], result: F64 },
|
||||
"dyn.arrAt": { argTypes: [DYN, F64], result: DYN },
|
||||
|
||||
@@ -10679,6 +10679,25 @@
|
||||
"<repo>/tests/corpus/async-local-typed-results.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/global-symbol-cache/main.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/global-symbol-cache/remote.js",
|
||||
"<repo>/tests/corpus/global-symbol-cache/main.js"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/nullish-assignment-values.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/nullish-assignment-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/field-assignment-values.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/field-assignment-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2869,6 +2869,72 @@ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key) {
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, message, strlen(message), "SC2020");
|
||||
}
|
||||
|
||||
typedef struct ScrGlobalSymbolProperty {
|
||||
ScrSym *key;
|
||||
ScrDyn *value;
|
||||
struct ScrGlobalSymbolProperty *next;
|
||||
} ScrGlobalSymbolProperty;
|
||||
|
||||
static SCR_TL ScrGlobalSymbolProperty *scr_global_symbols = NULL;
|
||||
static SCR_TL bool scr_global_symbols_cleanup_registered = false;
|
||||
|
||||
static void scr_global_symbols_cleanup(void) {
|
||||
while (scr_global_symbols) {
|
||||
ScrGlobalSymbolProperty *entry = scr_global_symbols;
|
||||
scr_global_symbols = entry->next;
|
||||
scr_sym_release(entry->key);
|
||||
scr_dyn_release(entry->value);
|
||||
free(entry);
|
||||
}
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_global_symbol_get(ScrSym *key) {
|
||||
for (ScrGlobalSymbolProperty *entry = scr_global_symbols; entry; entry = entry->next) {
|
||||
if (entry->key == key) return scr_dyn_retain(entry->value);
|
||||
}
|
||||
return scr_dyn_retain(scr_dyn_undefined());
|
||||
}
|
||||
|
||||
void scr_dyn_global_symbol_set(ScrSym *key, ScrDyn *value) {
|
||||
for (ScrGlobalSymbolProperty *entry = scr_global_symbols; entry; entry = entry->next) {
|
||||
if (entry->key != key) continue;
|
||||
ScrDyn *previous = entry->value;
|
||||
entry->value = scr_dyn_retain(value);
|
||||
scr_dyn_release(previous);
|
||||
return;
|
||||
}
|
||||
if (!scr_global_symbols_cleanup_registered) {
|
||||
scr_atexit(scr_global_symbols_cleanup);
|
||||
scr_global_symbols_cleanup_registered = true;
|
||||
}
|
||||
ScrGlobalSymbolProperty *entry = malloc(sizeof *entry);
|
||||
if (!entry) scr_json_oom();
|
||||
entry->key = scr_sym_retain(key);
|
||||
entry->value = scr_dyn_retain(value);
|
||||
entry->next = scr_global_symbols;
|
||||
scr_global_symbols = entry;
|
||||
}
|
||||
|
||||
bool scr_dyn_global_symbol_has(ScrSym *key) {
|
||||
for (ScrGlobalSymbolProperty *entry = scr_global_symbols; entry; entry = entry->next) {
|
||||
if (entry->key == key) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void scr_dyn_global_symbol_delete(ScrSym *key) {
|
||||
ScrGlobalSymbolProperty **slot = &scr_global_symbols;
|
||||
while (*slot) {
|
||||
ScrGlobalSymbolProperty *entry = *slot;
|
||||
if (entry->key != key) { slot = &entry->next; continue; }
|
||||
*slot = entry->next;
|
||||
scr_sym_release(entry->key);
|
||||
scr_dyn_release(entry->value);
|
||||
free(entry);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static ScrDyn *scr_json_revive(ScrDyn *holder, const ScrStr *key,
|
||||
const ScrDyn *reviver, size_t depth) {
|
||||
if (!scr_json_callback_depth(depth)) return NULL;
|
||||
|
||||
@@ -3618,6 +3618,10 @@ void scr_dyn_obj_set(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value
|
||||
* wording). All three operands BORROWED (the value is retained in). */
|
||||
void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value);
|
||||
void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key);
|
||||
ScrDyn *scr_dyn_global_symbol_get(ScrSym *key); /* borrowed key; +1 value */
|
||||
void scr_dyn_global_symbol_set(ScrSym *key, ScrDyn *value); /* both borrowed */
|
||||
bool scr_dyn_global_symbol_has(ScrSym *key);
|
||||
void scr_dyn_global_symbol_delete(ScrSym *key);
|
||||
/* `key in v` with a runtime key — the dynHasKey fold per value (OBJ own
|
||||
* members, ARR length/valid indices, false elsewhere). Never throws. */
|
||||
bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
let trace = "";
|
||||
class Counter {
|
||||
stored = 1;
|
||||
get value(): number { trace += "get;"; return this.stored; }
|
||||
set value(value: number) { trace += "set;"; this.stored = value; }
|
||||
}
|
||||
let current = new Counter();
|
||||
const first = current;
|
||||
function receiver(): Counter { trace += "receiver;"; return current; }
|
||||
function replacement(): number { trace += "rhs;"; current = new Counter(); return 4; }
|
||||
console.log(receiver().value += replacement(), first.stored, current.stored, trace);
|
||||
trace = "";
|
||||
console.log(receiver().value = replacement(), current.stored, trace);
|
||||
const record = { a: 1, nested: { value: 2 }, text: "a" };
|
||||
console.log(record.a = 7, record.nested.value *= 3, record.text += "b");
|
||||
console.log(JSON.stringify(record));
|
||||
const key = Symbol("value");
|
||||
class Keyed {
|
||||
[key] = 2;
|
||||
}
|
||||
const keyed = new Keyed();
|
||||
console.log(keyed[key] += 5, keyed[key]);
|
||||
let sideEffects = 0;
|
||||
function fail(): number { sideEffects++; throw new Error("failed rhs"); }
|
||||
try { console.log(record.a += fail()); }
|
||||
catch (e) { if (e instanceof Error) console.log(e.message, record.a, sideEffects); }
|
||||
@@ -0,0 +1,29 @@
|
||||
import { remoteRead, remoteClear } from "./remote.js";
|
||||
const cacheKey = Symbol.for("scriptc.cache");
|
||||
function singleton(key, factory) {
|
||||
const bag = globalThis[cacheKey] ??= {};
|
||||
if (!(key in bag)) bag[key] = factory();
|
||||
return bag[key];
|
||||
}
|
||||
let made = 0;
|
||||
function make() { made++; return { value: made }; }
|
||||
const first = singleton("item", make);
|
||||
const second = singleton("item", make);
|
||||
console.log(first === second, first.value, made);
|
||||
const sameKey = Symbol.for("scriptc.cache");
|
||||
console.log(globalThis[cacheKey] === globalThis[sameKey], cacheKey in globalThis, remoteRead() === globalThis[cacheKey]);
|
||||
const unique = Symbol("scriptc.cache");
|
||||
console.log(globalThis[unique] === undefined, unique in globalThis);
|
||||
globalThis[unique] = undefined;
|
||||
console.log(globalThis[unique] === undefined, unique in globalThis);
|
||||
delete globalThis[unique];
|
||||
console.log(unique in globalThis);
|
||||
remoteClear();
|
||||
console.log(globalThis[sameKey] === undefined, sameKey in globalThis);
|
||||
const replacement = singleton("item", make);
|
||||
console.log(replacement === first, replacement.value, made);
|
||||
let reads = 0;
|
||||
function key() { reads++; return cacheKey; }
|
||||
const again = globalThis[key()] ??= { unused: true };
|
||||
console.log(reads, again === globalThis[cacheKey]);
|
||||
delete globalThis[cacheKey];
|
||||
@@ -0,0 +1 @@
|
||||
{"type":"module"}
|
||||
@@ -0,0 +1,3 @@
|
||||
const key = Symbol.for("scriptc.cache");
|
||||
export function remoteRead() { return globalThis[key]; }
|
||||
export function remoteClear() { delete globalThis[key]; }
|
||||
@@ -0,0 +1,39 @@
|
||||
let calls = 0;
|
||||
function supply(): number { calls++; return 7; }
|
||||
let empty: number | undefined;
|
||||
let zero: number | undefined = 0;
|
||||
console.log(empty ??= supply(), zero ??= supply(), calls);
|
||||
const values: (number | undefined)[] = [undefined, 0];
|
||||
let indexes = 0;
|
||||
function index(): number { indexes++; return 0; }
|
||||
console.log(values[index()] ??= supply(), values[index()] ??= supply(), indexes, calls);
|
||||
console.log(values[4] ??= supply(), values.length, calls);
|
||||
let trace = "";
|
||||
class Slot {
|
||||
stored: number | null = null;
|
||||
get value(): number | null { trace += "get;"; return this.stored; }
|
||||
set value(value: number | null) { trace += "set;"; this.stored = value; }
|
||||
}
|
||||
let slot = new Slot();
|
||||
const original = slot;
|
||||
function receiver(): Slot { trace += "receiver;"; return slot; }
|
||||
function replace(): number { trace += "rhs;"; slot = new Slot(); return 9; }
|
||||
console.log(receiver().value ??= replace(), original.stored, slot.stored, trace);
|
||||
trace = "";
|
||||
console.log(original.value ??= replace(), trace);
|
||||
const record: { value?: number } = {};
|
||||
console.log(record.value ??= 4, record.value ??= 5);
|
||||
let recordGets = 0;
|
||||
let recordSets = 0;
|
||||
let saved: string | undefined;
|
||||
const accessor = {
|
||||
get value(): string | undefined { recordGets++; return saved; },
|
||||
set value(value: string | undefined) { recordSets++; saved = value; },
|
||||
};
|
||||
console.log(accessor.value ??= "kept", accessor.value ??= "unused", recordGets, recordSets);
|
||||
let absent: undefined;
|
||||
let nil: null = null;
|
||||
function supplyUndefined(): undefined { trace += "undefined;"; return undefined; }
|
||||
function supplyNull(): null { trace += "null;"; return null; }
|
||||
trace = "";
|
||||
console.log(absent ??= supplyUndefined(), nil ??= supplyNull(), trace);
|
||||
+3
-1
@@ -1,4 +1,4 @@
|
||||
import { Base, FactoryInput, Input, Metrics, View } from "bundled-methods";
|
||||
import { Base, FactoryInput, Input, Metrics, View, describeOptions } from "bundled-methods";
|
||||
|
||||
const view = new View("selected");
|
||||
const base: Base = view;
|
||||
@@ -51,3 +51,5 @@ factory.close();
|
||||
console.log(factory.read(), saved?.describe());
|
||||
factory.open("replacement");
|
||||
console.log(factory.read(), factory.save() === saved);
|
||||
console.log(describeOptions({ name: "omitted" }));
|
||||
console.log(describeOptions({ name: "present", required: false, default: 0, type: "number" }));
|
||||
|
||||
+1
@@ -34,3 +34,4 @@ export class FactoryInput {
|
||||
read(): string;
|
||||
save(): { describe(): string } | null;
|
||||
}
|
||||
export function describeOptions(config: { name: string; required?: boolean; default?: number; type?: string }): string;
|
||||
|
||||
+4
@@ -21,3 +21,7 @@ export class FactoryInput {
|
||||
read() { return this.parser ? this.parser.describe() : "absent"; }
|
||||
save() { return this.parser; }
|
||||
}
|
||||
|
||||
export function describeOptions(config) {
|
||||
return [config.name, String(config.required), String(config.default), typeof config.type].join(":");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user