diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index c7fb8d36e..c4029f089 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -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, { diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 1769e8562..1e6d771b1 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -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, 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, + 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 }); diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 6808713e7..7dc178f03 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -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); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 09f5baab0..c9570e1ca 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -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. 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. diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 169d66e90..c562970a6 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2025,6 +2025,9 @@ export class Lowerer { * exact binding discipline as globals/functions. */ readonly staticCallables = new Map(); readonly genericFnsBySymbol = new Map(); + /** Typed declarations' instances for arguments that keep their own + * record or array layout (structural-instances.ts). */ + readonly structuralFnInfos = new Map(); /** Object-literal GENERIC methods (`{ m(x: T) {...} }`), interned by * their function-like node — instances ride the same monomorphization * queue (objLitGenericFnInfoOf). */ diff --git a/packages/compiler/src/frontend/lowering/structural-instances.ts b/packages/compiler/src/frontend/lowering/structural-instances.ts new file mode 100644 index 000000000..a6c59eb73 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/structural-instances.ts @@ -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(); + 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); +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/width-layout-identity.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/width-layout-identity.ts.json new file mode 100644 index 000000000..543e1f640 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/width-layout-identity.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/width-layout-identity.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/width-layout-parameter-writes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/width-layout-parameter-writes.ts.json new file mode 100644 index 000000000..6aa2085bb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/width-layout-parameter-writes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/width-layout-parameter-writes.ts" + ], + "diags": [] +} diff --git a/tests/corpus/width-layout-identity.ts b/tests/corpus/width-layout-identity.ts new file mode 100644 index 000000000..7ebc16475 --- /dev/null +++ b/tests/corpus/width-layout-identity.ts @@ -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); diff --git a/tests/corpus/width-layout-parameter-writes.ts b/tests/corpus/width-layout-parameter-writes.ts new file mode 100644 index 000000000..6f3d70448 --- /dev/null +++ b/tests/corpus/width-layout-parameter-writes.ts @@ -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); diff --git a/tests/harness/corpus-costs/width-layout-identity.ts.json b/tests/harness/corpus-costs/width-layout-identity.ts.json new file mode 100644 index 000000000..4f6ccc65c --- /dev/null +++ b/tests/harness/corpus-costs/width-layout-identity.ts.json @@ -0,0 +1 @@ +{ "plain": 4655, "san": 33380 } diff --git a/tests/harness/corpus-costs/width-layout-parameter-writes.ts.json b/tests/harness/corpus-costs/width-layout-parameter-writes.ts.json new file mode 100644 index 000000000..de667a408 --- /dev/null +++ b/tests/harness/corpus-costs/width-layout-parameter-writes.ts.json @@ -0,0 +1 @@ +{ "plain": 2940, "san": 38518 }