mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Keep the layout of arrays and records passed to wider parameter types
An array or record converts by copying it when its layout differs from the
type it flows into (`{ id; name; owned?: true }[]` into a `{ id; name }[]`
parameter). JavaScript passes the same object, so a callee's `push`, index
stores and element-field writes reached the caller's array in Node but a
copy in the compiled program, and `===` against the original was false.
When an argument names an existing array or record whose layout has every
field of the parameter's type plus optional fields, a direct call of a
declared function now targets an instance of the function lowered with that
parameter bound to the argument's own type. The body reads and writes the
caller's value; a value it stores converts into the argument's layout by
completing the optional fields as absent. Instances are keyed by the
parameter layouts. An instance whose body does not lower is discarded with
everything it recorded, and the call converts as declared.
With the same rule, a non-exported function whose every return names such
a value returns its layout (including the parameter's element or field),
and a const keeps its initializer's layout when the initializer names an
existing value or is a call.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { layoutReturnType } from "./structural-instances.js";
|
||||
import {
|
||||
typedArgumentsCandidate,
|
||||
typedArgumentsShapes,
|
||||
@@ -890,6 +891,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara
|
||||
lowerer.badType(nameBlame, lowerer.typeOf(nameBlame));
|
||||
}
|
||||
|
||||
if (!isAsync && !isGenerator) returnType = layoutReturnType(lowerer, decl, returnType);
|
||||
const symbol = declSymbolOf(lowerer, decl);
|
||||
if (!symbol) lowerer.unsupported("SC1090", decl, "this function form");
|
||||
lowerer.fnSigsBySymbol.set(symbol, {
|
||||
|
||||
@@ -1650,7 +1650,7 @@ export function implicitMonoFile(sf: ts.SourceFile): boolean {
|
||||
* parameter symbol — assignment, compound assignment, ++/--, a
|
||||
* destructuring-assignment target, or a for-in/of cursor. A written
|
||||
* param's binding could lie after the write, so it stays dyn. */
|
||||
function paramWrittenInBody(
|
||||
export function paramWrittenInBody(
|
||||
lowerer: Lowerer,
|
||||
body: ts.Node,
|
||||
sym: ts.Symbol,
|
||||
@@ -2370,18 +2370,46 @@ export function implicitDefaultInstance(
|
||||
return internImplicitInstance(lowerer, blame, info, shapes, argTypes);
|
||||
}
|
||||
|
||||
function internImplicitInstance(
|
||||
export function internImplicitInstance(
|
||||
lowerer: Lowerer,
|
||||
blame: ts.Node,
|
||||
info: GenericFnInfo,
|
||||
shapes: ParamShape[],
|
||||
argTypes: Map<ts.Symbol, ts.Type>,
|
||||
patternKey = "",
|
||||
declaredReturn?: IrType,
|
||||
): GenericInstance {
|
||||
return internImplicitInstanceOr(
|
||||
lowerer,
|
||||
blame,
|
||||
info,
|
||||
shapes,
|
||||
argTypes,
|
||||
patternKey,
|
||||
declaredReturn,
|
||||
false,
|
||||
)!;
|
||||
}
|
||||
|
||||
/** internImplicitInstance; with `fallible`, a body that does not lower
|
||||
* leaves no instance and no diagnostics, and the answer is null (the caller
|
||||
* keeps its own lowering). */
|
||||
export function internImplicitInstanceOr(
|
||||
lowerer: Lowerer,
|
||||
blame: ts.Node,
|
||||
info: GenericFnInfo,
|
||||
shapes: ParamShape[],
|
||||
argTypes: Map<ts.Symbol, ts.Type>,
|
||||
patternKey: string,
|
||||
declaredReturn: IrType | undefined,
|
||||
fallible: boolean,
|
||||
): GenericInstance | null {
|
||||
const key = shapes.map((s) => typeKey(s.type)).join(",") + (patternKey ? `|${patternKey}` : "");
|
||||
let inst = info.instances.get(key);
|
||||
if (inst?.implicitState === "failed")
|
||||
if (inst?.implicitState === "failed") {
|
||||
if (fallible) return null;
|
||||
lowerer.unsupported("SC1090", blame, `the failed specialization of '${info.baseName}'`);
|
||||
}
|
||||
if (inst) {
|
||||
// A recursive caller has observed this ABI before inference settles.
|
||||
// Keep the promised return slot and coerce the completed body's returns.
|
||||
@@ -2405,7 +2433,8 @@ function internImplicitInstance(
|
||||
shapes[index]?.type.kind === "dyn" &&
|
||||
broadPromiseParam(lowerer, lowerer.typeOf(param.name)),
|
||||
);
|
||||
const declared = dynamicBroadPromise ? null : implicitDeclaredReturn(lowerer, info);
|
||||
const declared =
|
||||
declaredReturn ?? (dynamicBroadPromise ? null : implicitDeclaredReturn(lowerer, info));
|
||||
inst = {
|
||||
path,
|
||||
name: keyedHelperName(lowerer, `${info.qualifiedName}%`, key, ""),
|
||||
@@ -2426,6 +2455,25 @@ function internImplicitInstance(
|
||||
// JS body must still have a throwing implementation: later references
|
||||
// (including recursive ones) can already hold its cached signature.
|
||||
const diagsBefore = lowerer.diags.length;
|
||||
const fencesBefore = lowerer.runtimeFences.length;
|
||||
const totalBefore = lowerer.stats.statementsTotal;
|
||||
const failedBefore = lowerer.stats.statementsFailed;
|
||||
const skippedBefore = lowerer.stats.functionsSkipped;
|
||||
const blockedBefore = fallible ? new Set(lowerer.blockedBindings) : null;
|
||||
const deferredBefore = fallible ? new Set(lowerer.deferredDiags.keys()) : null;
|
||||
/** Undoes what a failed fallible body recorded: the instance's
|
||||
* statements share their symbols with the declaration's own body. */
|
||||
const rollback = (): void => {
|
||||
lowerer.diags.splice(diagsBefore);
|
||||
lowerer.runtimeFences.splice(fencesBefore);
|
||||
lowerer.stats.statementsTotal = totalBefore;
|
||||
lowerer.stats.statementsFailed = failedBefore;
|
||||
lowerer.stats.functionsSkipped = skippedBefore;
|
||||
for (const symbol of [...lowerer.blockedBindings])
|
||||
if (!blockedBefore!.has(symbol)) lowerer.blockedBindings.delete(symbol);
|
||||
for (const symbol of [...lowerer.deferredDiags.keys()])
|
||||
if (!deferredBefore!.has(symbol)) lowerer.deferredDiags.delete(symbol);
|
||||
};
|
||||
// The instance is cached even when a receiver probe requested it.
|
||||
// Its body must retain statement fences instead of dropping failures
|
||||
// into that probe's temporary diagnostic sink and caching empty code.
|
||||
@@ -2445,10 +2493,22 @@ function internImplicitInstance(
|
||||
...(info.decl.body ? [info.decl.body] : []),
|
||||
]);
|
||||
const fn = lowerer.lowerGenericInstance(info, inst);
|
||||
if (
|
||||
fallible &&
|
||||
(lowerer.diags.length > diagsBefore || lowerer.runtimeFences.length > fencesBefore)
|
||||
) {
|
||||
inst.implicitState = "failed";
|
||||
rollback();
|
||||
return null;
|
||||
}
|
||||
lowerer.implicitFns.push(fn);
|
||||
} catch (e) {
|
||||
if (!(e instanceof PoisonError)) throw e;
|
||||
inst.implicitState = "failed";
|
||||
if (fallible) {
|
||||
rollback();
|
||||
return null;
|
||||
}
|
||||
const types: IrType[] = shapes.map((shape) => shape.type);
|
||||
if (info.member?.kind === "method")
|
||||
types.unshift({ kind: "object", className: info.member.cls.def.name });
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
arrayClassReceiver,
|
||||
lowerArrayClassStaticCall,
|
||||
} from "./builtins/array-classes.js";
|
||||
import { structuralCallInstance } from "./structural-instances.js";
|
||||
import { checkedPrivateMethodReceiver, privateNameClass } from "./private-brands.js";
|
||||
import { assertFunctionAliasOf } from "./lower-assert.js";
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
@@ -1360,6 +1361,16 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
sig &&
|
||||
!(jsSpreadArgs && spreadNeedsRuntimeArity(lowerer, sig.params, expr.arguments.nodes))
|
||||
) {
|
||||
const layout = structuralCallInstance(lowerer, expr, sig);
|
||||
if (layout) {
|
||||
return reconcileOverloadReturn(lowerer, expr, {
|
||||
kind: "call",
|
||||
callee: layout.name,
|
||||
args: lowerer.completeArgs(expr.arguments.nodes, layout.params, loc, expr),
|
||||
type: layout.returnType,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
lowerer.noteEdge(sig.name);
|
||||
const args = lowerer.completeArgs(expr.arguments.nodes, sig.params, loc, expr);
|
||||
const symbol = lowerer.resolveValueSymbol(expr.expression);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { keepsInitializerLayout } from "./structural-instances.js";
|
||||
import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js";
|
||||
import {
|
||||
allBytesArms,
|
||||
@@ -5930,6 +5931,14 @@ export function lowerVarDecl(
|
||||
)
|
||||
g.type = DYN;
|
||||
g.type = inferredOptionalRecordType(lowerer, decl, g.type, raw.type);
|
||||
// A module const keeps its initializer's record or array layout, as a
|
||||
// function's const does (keepsInitializerLayout).
|
||||
if (
|
||||
!isVarDeclared(decl) &&
|
||||
(ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 &&
|
||||
keepsInitializerLayout(lowerer, decl.initializer, raw.type, g.type)
|
||||
)
|
||||
g.type = raw.type;
|
||||
const arithmetic =
|
||||
raw.type.kind === "union" ? lowerer.unions.get(raw.type.unionId)?.arms : undefined;
|
||||
if (
|
||||
@@ -6077,6 +6086,11 @@ export function lowerVarDecl(
|
||||
// Promise.prototype is an ordinary object the checker types Promise<any>.
|
||||
if (!decl.type && !hasJsTypeAnnotation(decl) && isPromisePrototypeRead(lowerer, decl.initializer))
|
||||
type = DYN;
|
||||
// A const whose initializer has another record or array layout than the
|
||||
// binding's type keeps the initializer's layout: converting would copy the
|
||||
// value, so the binding would not be the value its initializer names.
|
||||
if (!isLet && type !== null && keepsInitializerLayout(lowerer, decl.initializer, init.type, type))
|
||||
type = init.type;
|
||||
// A typed-arguments function expression (typed-arguments.ts): the
|
||||
// checker types the binding variadic, but its value has the fixed
|
||||
// signature every use asserts.
|
||||
|
||||
@@ -2025,6 +2025,9 @@ export class Lowerer {
|
||||
* exact binding discipline as globals/functions. */
|
||||
readonly staticCallables = new Map<ts.Symbol, StaticCallableProjection>();
|
||||
readonly genericFnsBySymbol = new Map<ts.Symbol, GenericFnInfo>();
|
||||
/** Typed declarations' instances for arguments that keep their own
|
||||
* record or array layout (structural-instances.ts). */
|
||||
readonly structuralFnInfos = new Map<ts.Symbol, GenericFnInfo>();
|
||||
/** Object-literal GENERIC methods (`{ m<T>(x: T) {...} }`), interned by
|
||||
* their function-like node — instances ride the same monomorphization
|
||||
* queue (objLitGenericFnInfoOf). */
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { IrType } from "../../ir/ir.js";
|
||||
import { typeEquals, UNDEFINED_T } from "../../ir/ir.js";
|
||||
import type { FnSig, ParamShape } from "./call-signatures.js";
|
||||
import { isThisParameter } from "./call-signatures.js";
|
||||
import {
|
||||
internImplicitInstanceOr,
|
||||
paramWrittenInBody,
|
||||
type GenericFnInfo,
|
||||
type GenericInstance,
|
||||
} from "./generic-functions.js";
|
||||
import { nsPathPrefix } from "./lower-namespaces.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
/** Instances of a typed function for arguments that keep their own layout.
|
||||
*
|
||||
* A record or array whose layout differs from a parameter's declared
|
||||
* structural type converts by copying it (`{ id; name; owned?: true }[]`
|
||||
* into `{ id; name }[]`). JavaScript passes the same object, so the
|
||||
* callee's writes (`push`, index stores, element-field writes) and identity
|
||||
* comparisons see the caller's value. When an argument names an existing
|
||||
* value, the call instead targets an instance of the function lowered with
|
||||
* that parameter bound to the argument's own type: the body reads and
|
||||
* writes the caller's storage. Instances are keyed by the parameter
|
||||
* layouts, so each layout combination compiles once. */
|
||||
|
||||
/** The argument expressions that can name a value another reference sees:
|
||||
* bindings, member reads and `this`. Fresh values (literals, calls) have no
|
||||
* other reference, so their copy is not observable. */
|
||||
function namesExistingValue(arg: ts.Expression): boolean {
|
||||
let e = arg;
|
||||
for (;;) {
|
||||
if (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e)) e = e.expression;
|
||||
else if (ts.isAsExpression(e) || ts.isSatisfiesExpression(e)) e = e.expression;
|
||||
else break;
|
||||
}
|
||||
return (
|
||||
ts.isIdentifier(e) ||
|
||||
ts.isPropertyAccessExpression(e) ||
|
||||
ts.isElementAccessExpression(e) ||
|
||||
e.kind === ts.SyntaxKind.ThisKeyword
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether a value of layout `from` can stand for a value of layout `to`
|
||||
* in code written against `to`: the same layout, or a record that has every
|
||||
* field of `to` at such a layout plus fields `to` lacks that are optional
|
||||
* (`{ id; name; owned?: true }` for `{ id; name }`), or an array of such
|
||||
* elements. Every value that code stores converts into `from` by completing
|
||||
* the optional fields as absent, so reads and writes through either type see
|
||||
* one value. */
|
||||
function standsFor(lowerer: Lowerer, from: IrType, to: IrType): boolean {
|
||||
if (typeEquals(from, to)) return true;
|
||||
if (from.kind === "array" && to.kind === "array") return standsFor(lowerer, from.elem, to.elem);
|
||||
if (from.kind !== "record" || to.kind !== "record") return false;
|
||||
const source = lowerer.shapes.get(from.shapeId);
|
||||
const target = lowerer.shapes.get(to.shapeId);
|
||||
if (!source || !target || source.tuple || target.tuple) return false;
|
||||
if (source.indexValue || target.indexValue) return false;
|
||||
for (const field of target.fields) {
|
||||
const kept = source.fields.find((f) => f.name === field.name);
|
||||
if (!kept || !standsFor(lowerer, kept.type, field.type)) return false;
|
||||
}
|
||||
return source.fields.every(
|
||||
(field) =>
|
||||
target.fields.some((f) => f.name === field.name) ||
|
||||
(field.type.kind === "union" && lowerer.armTag(field.type.unionId, UNDEFINED_T) >= 0),
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether converting `from` into `to` copies a value that could keep its
|
||||
* own layout instead. */
|
||||
function copies(lowerer: Lowerer, from: IrType, to: IrType): boolean {
|
||||
if (typeEquals(from, to) || !standsFor(lowerer, from, to)) return false;
|
||||
const plan = lowerer.widthLiftPlan(from, to);
|
||||
return plan !== null && (plan.how === "arr" || plan.how === "width");
|
||||
}
|
||||
|
||||
/** The structural-instance info of a typed function declaration, created on
|
||||
* first use and kept on the Lowerer. */
|
||||
function structuralInfoOf(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.FunctionDeclaration,
|
||||
symbol: ts.Symbol,
|
||||
): GenericFnInfo {
|
||||
const existing = lowerer.structuralFnInfos.get(symbol);
|
||||
if (existing) return existing;
|
||||
const nameText = decl.name?.text ?? "%default";
|
||||
const body = decl.body!;
|
||||
const info: GenericFnInfo = {
|
||||
decl,
|
||||
baseName: nameText,
|
||||
qualifiedName: lowerer.qualify(
|
||||
ts.getSourceFileOfNode(decl),
|
||||
nsPathPrefix(lowerer, decl) + nameText + "%layout",
|
||||
),
|
||||
typeParams: [],
|
||||
instances: new Map(),
|
||||
implicitParams: decl.parameters.nodes.map((param) => {
|
||||
if (!ts.isIdentifier(param.name) || param.dotDotDotToken || param.initializer) return null;
|
||||
if (param.questionToken) return null;
|
||||
const sym = lowerer.checker.getSymbolAtLocation(param.name);
|
||||
if (!sym || paramWrittenInBody(lowerer, body, sym, param.name.text)) return null;
|
||||
return sym;
|
||||
}),
|
||||
};
|
||||
lowerer.structuralFnInfos.set(symbol, info);
|
||||
return info;
|
||||
}
|
||||
|
||||
/** The instance a direct call of the declared function `sig` targets when
|
||||
* one of its arguments names a record or array whose layout the parameter
|
||||
* would copy; null when every argument converts as declared. */
|
||||
export function structuralCallInstance(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
sig: FnSig,
|
||||
): GenericInstance | null {
|
||||
const symbol = lowerer.resolveValueSymbol(expr.expression as ts.Identifier);
|
||||
const decl = symbol?.valueDeclaration;
|
||||
if (!symbol || !decl || !ts.isFunctionDeclaration(decl) || !decl.body) return null;
|
||||
if (
|
||||
decl.asteriskToken ||
|
||||
decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)
|
||||
)
|
||||
return null;
|
||||
if (decl.parameters.nodes.some(isThisParameter)) return null;
|
||||
if (symbol.declarations.filter((d) => ts.isFunctionDeclaration(d)).length !== 1) return null;
|
||||
const args = expr.arguments.nodes;
|
||||
if (args.some((a) => ts.isSpreadElement(a))) return null;
|
||||
const params = decl.parameters.nodes;
|
||||
if (sig.params.length !== params.length) return null;
|
||||
const bound: { index: number; type: IrType; checker: ts.Type }[] = [];
|
||||
params.forEach((_, index) => {
|
||||
const arg = args[index];
|
||||
const declared = sig.params[index]!;
|
||||
if (!arg || declared.mode !== "required" || !namesExistingValue(arg)) return;
|
||||
// A signature that widens the declared type's layout (record parameters
|
||||
// whose fields also take undefined) keeps its own boundary conversion.
|
||||
const own = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(params[index]!.name));
|
||||
if (!own || !typeEquals(own, declared.type)) return;
|
||||
const checker = lowerer.typeOf(arg);
|
||||
const type = lowerer.mapTypeOf(checker);
|
||||
if (type && copies(lowerer, type, declared.type)) bound.push({ index, type, checker });
|
||||
});
|
||||
if (bound.length === 0) return null;
|
||||
const info = structuralInfoOf(lowerer, decl, symbol);
|
||||
const shapes: ParamShape[] = [...sig.params];
|
||||
const argTypes = new Map<ts.Symbol, ts.Type>();
|
||||
for (const { index, type, checker } of bound) {
|
||||
const sym = info.implicitParams![index];
|
||||
if (!sym) continue;
|
||||
shapes[index] = { type, mode: "required" };
|
||||
argTypes.set(sym, checker);
|
||||
}
|
||||
if (argTypes.size === 0) return null;
|
||||
// A body that returns a bound parameter returns the argument's layout.
|
||||
const previous = lowerer.implicitParamTypes;
|
||||
lowerer.implicitParamTypes = argTypes;
|
||||
let returnType: IrType;
|
||||
try {
|
||||
returnType = layoutReturnType(lowerer, decl, sig.returnType);
|
||||
} finally {
|
||||
lowerer.implicitParamTypes = previous;
|
||||
}
|
||||
return internImplicitInstanceOr(lowerer, expr, info, shapes, argTypes, "", returnType, true);
|
||||
}
|
||||
|
||||
/** The layout of the value an existing-value expression names: a binding's
|
||||
* own type, or the element or field of such a value read through `[i]` or
|
||||
* `.name`. Reads of a parameter bound to an argument's layout follow that
|
||||
* layout rather than the parameter's declared type. */
|
||||
function layoutOf(lowerer: Lowerer, expression: ts.Expression): IrType | null {
|
||||
let e = expression;
|
||||
while (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e)) e = e.expression;
|
||||
if (ts.isElementAccessExpression(e)) {
|
||||
const base = layoutOf(lowerer, e.expression);
|
||||
if (
|
||||
base?.kind === "array" &&
|
||||
lowerer.mapTypeOf(lowerer.typeOf(e.argumentExpression))?.kind === "f64"
|
||||
)
|
||||
return base.elem;
|
||||
} else if (ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.name)) {
|
||||
const base = layoutOf(lowerer, e.expression);
|
||||
if (base?.kind === "record") {
|
||||
const field = lowerer.shapes.get(base.shapeId)?.fields.find((f) => f.name === e.name.text);
|
||||
if (field) return field.type;
|
||||
}
|
||||
}
|
||||
return lowerer.mapTypeOf(lowerer.typeOf(e));
|
||||
}
|
||||
|
||||
/** The return type of a declared function whose every `return` names an
|
||||
* existing value of one record or array layout that the declared return type
|
||||
* would copy (`function items(values: Refined[]): Value[] { return values; }`):
|
||||
* that layout, so the caller receives the value itself. `declared` otherwise. */
|
||||
export function layoutReturnType(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.FunctionDeclaration,
|
||||
declared: IrType,
|
||||
): IrType {
|
||||
if (!decl.body || (declared.kind !== "array" && declared.kind !== "record")) return declared;
|
||||
// An exported function keeps its declared result: a library build calls it
|
||||
// from outside the program, through the declared ABI.
|
||||
if (decl.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) return declared;
|
||||
const returned: ts.Expression[] = [];
|
||||
let open = false;
|
||||
ts.walkPreorder(decl.body, (node) => {
|
||||
if (node !== decl.body && ts.isFunctionLike(node)) return "skip";
|
||||
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) return "skip";
|
||||
if (!ts.isReturnStatement(node)) return undefined;
|
||||
if (!node.expression || !namesExistingValue(node.expression)) open = true;
|
||||
else returned.push(node.expression);
|
||||
return undefined;
|
||||
});
|
||||
if (open || returned.length === 0) return declared;
|
||||
let layout: IrType | null = null;
|
||||
for (const expression of returned) {
|
||||
const type = layoutOf(lowerer, expression);
|
||||
if (!type || !copies(lowerer, type, declared)) return declared;
|
||||
if (layout && !typeEquals(layout, type)) return declared;
|
||||
layout = type;
|
||||
}
|
||||
return layout ?? declared;
|
||||
}
|
||||
|
||||
/** Whether a `const` binding typed `declared` keeps the record or array
|
||||
* layout `actual` of an initializer that names an existing value or returns
|
||||
* one, instead of converting it by a copy. Fresh literals convert as
|
||||
* declared: their copy is not observable. */
|
||||
export function keepsInitializerLayout(
|
||||
lowerer: Lowerer,
|
||||
initializer: ts.Expression,
|
||||
actual: IrType,
|
||||
declared: IrType,
|
||||
): boolean {
|
||||
let e = initializer;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!namesExistingValue(e) && !ts.isCallExpression(e)) return false;
|
||||
if (lowerer.isRuntimeOptionalRecordVariant(actual, declared)) return false;
|
||||
return copies(lowerer, actual, declared);
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/width-layout-identity.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/width-layout-parameter-writes.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// An array or record passed to a parameter, returned, or bound to a const
|
||||
// under a type that lacks some of its optional fields stays the same value:
|
||||
// identity comparisons and writes through any of the names see one object.
|
||||
|
||||
interface Entry {
|
||||
key: string;
|
||||
hits: number;
|
||||
note?: string;
|
||||
}
|
||||
type Counted = { key: string; hits: number };
|
||||
|
||||
function same(list: Counted[], other: Entry[]): boolean {
|
||||
return (list as unknown) === other;
|
||||
}
|
||||
function keep(list: Counted[]): Counted[] {
|
||||
return list;
|
||||
}
|
||||
function first(list: Counted[]): Counted {
|
||||
return list[0]!;
|
||||
}
|
||||
function grow(list: Counted[], key: string): Counted[] {
|
||||
list.push({ key, hits: 0 });
|
||||
return list;
|
||||
}
|
||||
|
||||
const entries: Entry[] = [{ key: "a", hits: 1, note: "first" }];
|
||||
console.log(same(entries, entries), keep(entries) === entries, first(entries) === entries[0]);
|
||||
const grown = grow(entries, "b");
|
||||
grown.push({ key: "c", hits: 2 });
|
||||
console.log(grown === entries, entries.length, entries.map((e) => e.key).join(","));
|
||||
|
||||
function local(list: Entry[]): boolean {
|
||||
const view: Counted[] = list;
|
||||
const kept = keep(list);
|
||||
view[0]!.hits += 10;
|
||||
return view === list && kept === list;
|
||||
}
|
||||
console.log(local(entries), entries[0]!.hits, entries[0]!.note);
|
||||
@@ -0,0 +1,73 @@
|
||||
// An array or record passed to a parameter whose type lacks some of its
|
||||
// optional fields is the caller's value inside the callee: pushes, index
|
||||
// stores, element-field writes and removals through the parameter reach the
|
||||
// caller's array, as in Node.
|
||||
|
||||
interface Param {
|
||||
id: string;
|
||||
name: string;
|
||||
owned?: true;
|
||||
}
|
||||
type Slot = { id: string; name: string };
|
||||
|
||||
function addSlot(params: Slot[], name: string): void {
|
||||
params.push({ id: `${name}.0`, name });
|
||||
}
|
||||
function rename(params: Slot[], index: number, name: string): void {
|
||||
params[index]!.name = name;
|
||||
}
|
||||
function replace(params: Slot[], index: number, slot: Slot): void {
|
||||
params[index] = slot;
|
||||
}
|
||||
function removeFirst(params: Slot[]): string | undefined {
|
||||
return params.shift()?.name;
|
||||
}
|
||||
function forward(params: Slot[], name: string): void {
|
||||
addSlot(params, name);
|
||||
addSlot(params, `${name}2`);
|
||||
}
|
||||
function names(params: Slot[]): string {
|
||||
return params.map((p) => p.name).join(",");
|
||||
}
|
||||
|
||||
const params: Param[] = [{ id: "a.0", name: "a", owned: true }];
|
||||
addSlot(params, "b");
|
||||
rename(params, 0, "first");
|
||||
replace(params, 1, { id: "z.0", name: "z" });
|
||||
forward(params, "c");
|
||||
console.log(params.length, JSON.stringify(params));
|
||||
console.log(removeFirst(params), params.length, params[0]?.owned, names(params));
|
||||
params.splice(1, 1);
|
||||
console.log(names(params));
|
||||
|
||||
const holder = { list: params };
|
||||
addSlot(holder.list, "d");
|
||||
console.log(holder.list.length, params.length, holder.list === params);
|
||||
|
||||
// A record parameter: writes to its fields reach the caller's record, and the
|
||||
// optional field the parameter's type lacks stays as the caller left it.
|
||||
interface Counter {
|
||||
label: string;
|
||||
hits: number;
|
||||
note?: string;
|
||||
}
|
||||
function bump(target: { label: string; hits: number }): void {
|
||||
target.hits += 1;
|
||||
target.label = target.label.toUpperCase();
|
||||
}
|
||||
const counter: Counter = { label: "c", hits: 1 };
|
||||
const noted: Counter = { label: "n", hits: 0, note: "kept" };
|
||||
bump(counter);
|
||||
bump(counter);
|
||||
bump(noted);
|
||||
console.log(JSON.stringify(counter), "note" in counter, JSON.stringify(noted));
|
||||
|
||||
// Recursion through the same parameter.
|
||||
function fill(params: Slot[], n: number): void {
|
||||
if (n === 0) return;
|
||||
addSlot(params, `r${n}`);
|
||||
fill(params, n - 1);
|
||||
}
|
||||
const deep: Param[] = [];
|
||||
fill(deep, 3);
|
||||
console.log(names(deep), deep.length);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4655, "san": 33380 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2940, "san": 38518 }
|
||||
Reference in New Issue
Block a user