diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index e132f27da..a86ed217e 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -24,6 +24,8 @@ title: "Limitations" ../limitations/language-features/async-iteration.mdx +../limitations/language-features/iterable-values.mdx + ../limitations/language-features/generics.mdx ../limitations/language-features/function-values.mdx diff --git a/docs/content/limitations/language-features/iterable-values.mdx b/docs/content/limitations/language-features/iterable-values.mdx new file mode 100644 index 000000000..2feedc02f --- /dev/null +++ b/docs/content/limitations/language-features/iterable-values.mdx @@ -0,0 +1,2 @@ +- **Iterable values:** A value typed `Iterable` can hold an array, a Set, a Map (for `Iterable<[K, V]>`), a string (for `Iterable`), a generator, or a Map, Set or array iterator. + Class instances with their own `[Symbol.iterator]` method and typed arrays can't be stored in an `Iterable` and are reported at the conversion. diff --git a/packages/compiler/src/frontend/lowering/class-iterables.ts b/packages/compiler/src/frontend/lowering/class-iterables.ts index 8c58fc0bd..0f4d6c254 100644 --- a/packages/compiler/src/frontend/lowering/class-iterables.ts +++ b/packages/compiler/src/frontend/lowering/class-iterables.ts @@ -1,13 +1,18 @@ import type { IrExpr, IrType } from "../../ir/ir.js"; import type { Lowerer } from "./lowerer.js"; import { accessorCall } from "./lower-classes.js"; +import { iterableGenerator, iterableUnionOf } from "./iterable-values.js"; /** The generator a class instance's `[Symbol.iterator]` returns, when that - * method is a generator method taking no arguments: `obj[Symbol.iterator]()`, + * method is a generator method taking no arguments (or the iterator an + * Iterable value opens over its current arm): `obj[Symbol.iterator]()`, * called once, as for-of, spread and Array.from call it. Null for other * values, and for classes whose iterator the program can replace at run * time (an instance property, a prototype write). */ export function classGeneratorIterable(lowerer: Lowerer, value: IrExpr): IrExpr | null { + // An Iterable value opens an iterator over its current arm. + const iterable = iterableUnionOf(lowerer, value.type); + if (iterable) return iterableGenerator(lowerer, value, iterable); if (value.type.kind !== "object" || !iteratesThroughGenerator(lowerer, value.type)) return null; const className = value.type.className; const ret = lowerer.findMethodOn(lowerer.classes.get(className)!, "sym:iterator")!.sig.ret; @@ -17,6 +22,7 @@ export function classGeneratorIterable(lowerer: Lowerer, value: IrExpr): IrExpr /** Whether values of `type` iterate through a generator method (the check * classGeneratorIterable makes, from the type alone). */ export function iteratesThroughGenerator(lowerer: Lowerer, type: IrType): boolean { + if (iterableUnionOf(lowerer, type)) return true; if (type.kind !== "object") return false; const info = lowerer.classes.get(type.className); if (!info || info.def.runtime || info.fields.has("sym:iterator")) return false; diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index d3169ec5c..e043daf68 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -22,6 +22,7 @@ import { isPrimitiveCollectionKey, isUnitType, typeEquals, + UNDEFINED_T, } from "../../../ir/ir.js"; import { COLLECTION_ITERATOR_METHODS, @@ -44,7 +45,7 @@ import { strCharsCall } from "./array-construction.js"; import { arrayElementRead } from "./array-iteration.js"; import { isSearchParamsIterable } from "../builtins/url.js"; import { classGeneratorIterable } from "../class-iterables.js"; -import { generatorDrain } from "../iterator-adapters.js"; +import { generatorDrain, lowerCollectionIteratorValue } from "../iterator-adapters.js"; /** Ambient Map method calls. `get`/`set`/`has`/`delete`/`clear` lower to * mapIntrinsic; `forEach` desugars to a direct call of a synthetic loop @@ -1345,6 +1346,17 @@ export function mapFromSeedValue( const drained = generatorDrain(lowerer, generator, entry, loc); return drained ? mapFromSeedValue(lowerer, drained, mapT) : null; } + // A Set of [key, value] pairs walks its values like a generator of them. + if (seed.type.kind === "set" && seed.type.elem.kind === "record") { + const walk = lowerCollectionIteratorValue( + lowerer, + seed, + "values", + { kind: "generator", yieldT: seed.type.elem, retT: VOID, nextT: UNDEFINED_T }, + loc, + ); + return walk ? mapFromSeedValue(lowerer, walk, mapT) : null; + } if (seed.type.kind === "map") { if (!typeEquals(seed.type, mapT)) return null; return { kind: "mapIntrinsic", method: "clone", receiver: seed, args: [], type: mapT, loc }; diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index 63175d50a..eca3c33f5 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -41,6 +41,7 @@ import { runtimeOptionalUseOf, } from "../lower-exprs.js"; import { runtimeOptionalReceiverRead } from "./narrowing.js"; +import { iterableUnionOf } from "../iterable-values.js"; import { builtinGlobalValueAt } from "./property-access.js"; import { varRef } from "../../../ir/build.js"; import { @@ -322,6 +323,16 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: // value arm before routing the member/element operation so dynamic // keys and builtin record walkers do not see a union receiver. const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr)); + // An optional Iterable proven present re-tags into the Iterable. + if ( + g.type.kind === "union" && + narrowed?.kind === "union" && + narrowed.unionId !== g.type.unionId && + iterableUnionOf(lowerer, narrowed) + ) { + const helper = lowerer.narrowedRetagHelper(expr, g.type.unionId, narrowed.unionId, loc); + if (helper) return { kind: "call", callee: helper, args: [ref], type: narrowed, loc }; + } const arithmeticUnion = g.type.kind === "union" && lowerer.unions.get(g.type.unionId)?.arms.length === 2 && diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts index 786f3a337..acac99610 100644 --- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts +++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts @@ -46,6 +46,7 @@ import { } from "../lower-exprs.js"; import { isConstAssertionTypeNode, unitOnlyUnion } from "../../type-mapper.js"; import { assertsGlobalObject, extractionCopiesReference } from "../assertion-identity.js"; +import { iterableSlotValue, iterableUnionOf } from "../iterable-values.js"; /** `c ? a : b`. An explicit destination constructs each branch at that * layout before joining it. Without one, retain the expression's inferred @@ -692,6 +693,18 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx const hosted = hostedStorageView(lowerer, expr, narrowed, expr.loc); if (hosted) return hosted; } + // An optional Iterable narrowed to the iterable union (`xs !== undefined`): + // re-tag into it, so for-of, spread and the other consumers see the + // Iterable itself. + if ( + narrowed?.kind === "union" && + narrowed.unionId !== expr.type.unionId && + iterableUnionOf(lowerer, narrowed) + ) { + const helper = lowerer.narrowedRetagHelper(node, expr.type.unionId, narrowed.unionId, expr.loc); + if (helper) + return { kind: "call", callee: helper, args: [expr], type: narrowed, loc: expr.loc }; + } // A checker type narrowed to a UNIT arm (the `=== undefined` branch) // also stays union-typed: a unit arm has no payload to extract, and // nothing useful reads such a value anyway. (Standalone undefined maps @@ -1524,6 +1537,10 @@ export function lowerAsExpression( // reach it (assertion-identity.ts). if (extractionCopiesReference(lowerer, target) && assertsGlobalObject(lowerer, expr)) return inner; + // `value as Iterable`: the value's own iterator, stepped lazily with + // each element checked against T (the generator arm's adapter). + const iterable = iterableSlotValue(lowerer, inner, target); + if (iterable) return iterable; if ( target.kind === "void" || !canDynCheckTo( diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts index 9500d6d2d..5ede3efe3 100644 --- a/packages/compiler/src/frontend/lowering/function-returns.ts +++ b/packages/compiler/src/frontend/lowering/function-returns.ts @@ -1,3 +1,4 @@ +import { iterableGeneratorArm } from "./iterable-values.js"; import { mapAsyncGeneratorAnnotation } from "../type-mapper.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import * as ts from "../ts7/adapter.js"; @@ -147,6 +148,9 @@ export function declaredReturnType( // (`.action(() => { throw ... })`). `never` VALUES stay unmapped. if (retTsType.flags & ts.TypeFlags.Never) return VOID; let mappedReturn = lowerer.mapTypeOf(retTsType); + // A generator declared to return Iterable returns a generator over T. + if (mappedReturn && decl.asteriskToken) + mappedReturn = iterableGeneratorArm(lowerer, mappedReturn); if ( mappedReturn === null && decl.asteriskToken && diff --git a/packages/compiler/src/frontend/lowering/iterable-values.ts b/packages/compiler/src/frontend/lowering/iterable-values.ts new file mode 100644 index 000000000..78de9720c --- /dev/null +++ b/packages/compiler/src/frontend/lowering/iterable-values.ts @@ -0,0 +1,342 @@ +/* `Iterable` values. The type maps to the iterable union + * (type-mapper.ts iterableUnionType): an array, Set, Map or string keeps + * its own arm, so the value stays the same multi-pass object (identity, + * inspection and typeof are the source's own), and every iterator — a + * generator, an array, Map or Set iterator, a checked-dynamic iterator — + * enters the generator arm, single-pass as in JavaScript. + * + * Consumers open an iterator over the current arm the way for-of's + * GetIterator does: an array, Set, Map or string arm starts a fresh live + * walk (the sources' own iterator helpers), the generator arm is itself. + * for-of gives the array arm its own array loop, so iterating an Iterable + * that holds an array runs at array-loop speed. */ +import * as ts from "../ts7/adapter.js"; +import { locOf } from "../program.js"; +import { varRef, numLit } from "../../ir/build.js"; +import { + BOOL, + F64, + iterableArmsElem, + typeEquals, + type IrExpr, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; +import { typeKey } from "../type-mapper.js"; +import { keyedHelperName } from "./stable-names.js"; +import { PoisonError, type Lowerer } from "./lowerer.js"; +import { noLoweringDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js"; +import { generatorLoopBinding, lowerForOfGenerator, type GenType } from "./lower-generators.js"; +import { + iteratorSlotAdapter, + lowerArrayIteratorValue, + lowerCollectionIteratorValue, + lowerStringIteratorValue, +} from "./iterator-adapters.js"; +import { arrayValueRead, arrayValueType } from "./array-values.js"; + +/** An iterable union and the tags of its arms. */ +export interface IterableUnion { + unionId: string; + type: IrType & { kind: "union" }; + elem: IrType; + arms: readonly IrType[]; + /** The generator arm: a sync generator over `elem`. */ + gen: GenType; +} + +/** The iterable union `t` names, or null. */ +export function iterableUnionOf(lowerer: Lowerer, t: IrType): IterableUnion | null { + const info = iterableSlotUnionOf(lowerer, t); + // An optional Iterable (`Iterable | undefined`) is consumed only once + // narrowed to the iterable union itself. + if (info === null || info.arms.some((arm) => arm.kind === "undefinedT" || arm.kind === "nullT")) + return null; + return info; +} + +/** The iterable union `t` names, also an optional or nullable one + * (`Iterable | undefined`, whose unit arms sit beside the iterable + * arms), or null. */ +function iterableSlotUnionOf(lowerer: Lowerer, t: IrType): IterableUnion | null { + if (t.kind !== "union") return null; + const arms = lowerer.unions.get(t.unionId)?.arms; + if (!arms) return null; + const elem = iterableArmsElem(arms); + if (elem === null) return null; + const gen = arms.find((arm) => arm.kind === "generator") as GenType; + return { unionId: t.unionId, type: t, elem, arms, gen }; +} + +/** The generator arm of an iterable union (what a generator function + * declared to return Iterable returns), or `t` itself. */ +export function iterableGeneratorArm(lowerer: Lowerer, t: IrType): IrType { + return iterableUnionOf(lowerer, t)?.gen ?? t; +} + +/** A value flowing into an iterable-typed slot, optional or nullable + * ones included: a source arm wraps as itself; a generator over other + * channel types or a checked-dynamic iterator (a Map or Set iterator) + * adapts into the generator arm. Null when neither applies, leaving the + * ordinary conversion fences to refuse other sources (class iterables, + * typed arrays, other element types). */ +export function iterableSlotValue(lowerer: Lowerer, expr: IrExpr, expected: IrType): IrExpr | null { + const info = iterableSlotUnionOf(lowerer, expected); + if (info === null || expr.type.kind === "union") return null; + const wrap = (value: IrExpr): IrExpr => ({ + kind: "unionWrap", + unionId: info.unionId, + tag: lowerer.armTag(info.unionId, value.type), + value, + type: info.type, + loc: expr.loc, + }); + if (lowerer.armTag(info.unionId, expr.type) >= 0) return wrap(expr); + if (expr.type.kind === "generator" || expr.type.kind === "dyn") { + const adapted = iteratorSlotAdapter(lowerer, expr, info.gen); + if (adapted && typeEquals(adapted.type, info.gen)) return wrap(adapted); + } + // Class instances (their own [Symbol.iterator]) and typed arrays have no + // arm: refuse them here, by name. + if (expr.type.kind === "object" || expr.type.kind === "bytes") { + lowerer.pushDiag( + unsupportedDiag( + "SC1090", + expr.loc, + `an Iterable<${lowerer.fmt(info.elem)}> holding a '${lowerer.fmt(expr.type)}' (an Iterable value holds an array, Set, Map, string, or iterator)`, + ), + ); + throw new PoisonError(); + } + return null; +} + +/** GetIterator on an iterable union value: an interned helper answering a + * generator over the current arm — a fresh live walk of an array, Set, Map + * or string, or the generator arm itself. */ +export function iterableGenerator(lowerer: Lowerer, value: IrExpr, info: IterableUnion): IrExpr { + const loc = value.loc; + const key = `iterableOf:${typeKey(info.type)}`; + let name = lowerer.valueHelpers.get(key); + if (!name) { + const u = varRef("u.0", info.type, loc); + const body: IrStmt[] = []; + let genTag = -1; + info.arms.forEach((arm, tag) => { + if (arm.kind === "generator") { + genTag = tag; + return; + } + const narrowed: IrExpr = { + kind: "unionNarrow", + unionId: info.unionId, + tag, + value: u, + type: arm, + loc, + }; + const walk = + arm.kind === "array" + ? lowerArrayIteratorValue(lowerer, narrowed, "values", info.gen, loc) + : arm.kind === "set" + ? lowerCollectionIteratorValue(lowerer, narrowed, "values", info.gen, loc) + : arm.kind === "map" + ? lowerCollectionIteratorValue(lowerer, narrowed, "entries", info.gen, loc) + : arm.kind === "string" + ? lowerStringIteratorValue(lowerer, narrowed, info.gen, loc) + : null; + if (walk === null || !typeEquals(walk.type, info.gen)) { + lowerer.pushDiag( + noLoweringDiag( + `iterating an Iterable<${lowerer.fmt(info.elem)}> that holds a ${arm.kind}`, + loc, + ), + ); + throw new PoisonError(); + } + body.push({ + kind: "if", + cond: { + kind: "unionIsTag", + unionId: info.unionId, + tag, + negated: false, + value: u, + type: BOOL, + loc, + }, + then: [{ kind: "return", value: walk, loc }], + else_: null, + loc, + }); + }); + body.push({ + kind: "return", + value: { + kind: "unionNarrow", + unionId: info.unionId, + tag: genTag, + value: u, + type: info.gen, + loc, + }, + loc, + }); + name = keyedHelperName(lowerer, "%iterable.of", key); + lowerer.valueHelpers.set(key, name); + lowerer.liftedFns.push({ + name, + params: [{ localId: "u.0", name: "u", type: info.type }], + returnType: info.gen, + locals: [{ id: "u.0", name: "u", type: info.type, mutable: false }], + body, + loc, + }); + } + return { kind: "call", callee: name, args: [value], type: info.gen, loc }; +} + +/** Whether a loop body declares a class (a class has one lowering per + * declaration, so its body cannot lower twice). */ +function declaresClass(body: ts.Statement): boolean { + let found = false; + ts.walkPreorder(body, (node) => { + if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) { + found = true; + return "stop"; + } + return undefined; + }); + return found; +} + +/** `for (x of iterable)` over an iterable union: + * + * { const %src = ; + * if (%src is the array arm) { + * const %arr = ; + * for (let %cur = 0; %cur < %arr.length; %cur++) { const x = %arr[%cur]; } + * } else { + * for (const x of GetIterator(%src)) // the generator for-of + * } + * } + * + * The array arm walks exactly like an array for-of (live length, the + * backend's dense-array loop forms), so an Iterable holding an array + * iterates at array speed; every other arm drives its iterator with the + * generator for-of's IteratorClose rules. The body lowers once for each + * loop; a body that declares a class lowers only once, in the generator + * for-of over every arm. */ +export function lowerForOfIterable( + lowerer: Lowerer, + stmt: ts.ForOfStatement, + iterable: IrExpr, + info: IterableUnion, + labels?: string[], +): IrStmt { + const loc = locOf(stmt); + const generatorLoop = (source: IrExpr): IrStmt => + lowerForOfGenerator( + lowerer, + stmt, + iterableGenerator(lowerer, source, info) as IrExpr & { type: GenType }, + labels, + ); + const arrayTag = info.arms.findIndex((arm) => arm.kind === "array"); + // A union without an array arm (`Set | Generator`) has only the + // generator loop. + if (stmt.awaitModifier || declaresClass(stmt.statement) || arrayTag < 0) + return generatorLoop(iterable); + const arrT = info.arms[arrayTag]!; + lowerer.scopes.push(new Map()); + try { + const src = lowerer.declareHiddenLocal("%iterableSource", info.type); + const srcRef = (): IrExpr => varRef(src.id, info.type, loc); + const arr = lowerer.declareHiddenLocal("%arrayIterator", arrT); + const cursor = lowerer.declareHiddenLocal("%arrayCursor", F64); + cursor.mutable = true; + const arrRef = varRef(arr.id, arrT, loc); + const curRef = varRef(cursor.id, F64, loc); + // The element: a hole reads undefined, which an undefined-armed T + // holds; for other element types the read traps, as typed reads do. + const element: IrExpr = typeEquals(arrayValueType(lowerer, info.elem), info.elem) + ? arrayValueRead(lowerer, arrRef, curRef, info.elem, loc) + : { kind: "arrayGet", arr: arrRef, index: curRef, type: info.elem, loc }; + const arrayLoop = ((): IrStmt => { + lowerer.scopes.push(new Map()); + try { + const binding = generatorLoopBinding(lowerer, stmt, element, info.elem); + const body = lowerer.inCtl("loop", () => lowerer.lowerScopedBlock(stmt.statement), labels); + return { + kind: "for", + init: { kind: "varDecl", localId: cursor.id, init: numLit(0, loc), loc }, + cond: { + kind: "bin", + op: "<", + left: curRef, + right: { + kind: "arrIntrinsic", + method: "length", + receiver: arrRef, + args: [], + type: F64, + loc, + }, + type: BOOL, + loc, + }, + update: { + kind: "assign", + localId: cursor.id, + value: { kind: "bin", op: "+", left: curRef, right: numLit(1, loc), type: F64, loc }, + loc, + }, + body: [...binding, ...body], + ...(labels && { labels }), + loc, + }; + } finally { + lowerer.scopes.pop(); + } + })(); + return { + kind: "block", + body: [ + { kind: "varDecl", localId: src.id, init: iterable, loc }, + { + kind: "if", + cond: { + kind: "unionIsTag", + unionId: info.unionId, + tag: arrayTag, + negated: false, + value: srcRef(), + type: BOOL, + loc, + }, + then: [ + { + kind: "varDecl", + localId: arr.id, + init: { + kind: "unionNarrow", + unionId: info.unionId, + tag: arrayTag, + value: srcRef(), + type: arrT, + loc, + }, + loc, + }, + arrayLoop, + ], + else_: [generatorLoop(srcRef())], + loc, + }, + ], + loc, + }; + } finally { + lowerer.scopes.pop(); + } +} diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 6a80c551e..ac8b02b69 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -2095,6 +2095,7 @@ export function typeofAnswer(arm: IrType): string | null { case "array": case "map": case "set": + case "generator": case "regex": case "bytes": case "url": diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index d6a468260..cc0ea2148 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -242,6 +242,7 @@ import { npmStaticPackageOfPath } from "../npm-static.js"; import { fenceNodeModuleMutation } from "./lower-node-module.js"; import { fencePerfEntryKeyWalk } from "./builtins/performance.js"; import { classGeneratorIterable } from "./class-iterables.js"; +import { iterableUnionOf, lowerForOfIterable } from "./iterable-values.js"; interface UsingRegistration { acquire: IrStmt[]; @@ -9870,6 +9871,10 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { } iterSrc ??= stmt.expression; let iterable = lowerer.lowerExpr(iterSrc); + { + const iterableUnion = iterableUnionOf(lowerer, iterable.type); + if (iterableUnion) return lowerForOfIterable(lowerer, stmt, iterable, iterableUnion, labels); + } let tupleSource: { shapeId: string; fields: { name: string; type: IrType }[]; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 4add3193c..93c6246d9 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -67,6 +67,7 @@ import { valueStreamOf } from "./builtins/hash-stream.js"; import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js"; import { iteratorSlotAdapter } from "./iterator-adapters.js"; +import { iterableSlotValue } from "./iterable-values.js"; import type { FieldLift } from "./coercions/structural-plans.js"; import { lowerBuiltinCall } from "./builtin-calls.js"; import { CoercionState } from "./coercions/state.js"; @@ -9453,6 +9454,11 @@ export class Lowerer { const adapted = iteratorSlotAdapter(this, expr, expected); if (adapted) return adapted; } + // Iterable-typed slots: other iterators enter the generator arm. + if (expected.kind === "union" && !typeEquals(expr.type, expected)) { + const iterable = iterableSlotValue(this, expr, expected); + if (iterable) return iterable; + } // an any-typed slot marshals implicitly (tsc allows the assignment; // the marshal is where its semantics live). OUT: an 'any' value diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 4e8812437..78efe3d17 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -24,6 +24,7 @@ import { isSupportedMapKey, isSupportedMapValue, isSupportedSetElem, + iterableArmsElem, isUnitType, mapOf, NULL_T, @@ -3971,6 +3972,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ...channels, }; } + // Iterable: the iterable union (iterableUnionType) — the source value + // itself in its own arm, so an array stays the same multi-pass object and + // a generator stays single-pass. + if (isStdlibInterface("Iterable")) { + const arg = checker.getTypeArguments(widened as ts.TypeReference)[0]; + const elem = arg ? mapType(arg, ctx) : null; + return elem ? iterableUnionType(elem, ctx.shapes, unions) : null; + } // IteratorResult — the checker's type of `g.next()` (an // alias for IteratorYieldResult | IteratorReturnResult): // maps to the SAME interned record genResume answers, so `const r = @@ -5025,7 +5034,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { a.kind === "union" || a.kind === "date" || a.kind === "dyn" || - a.kind === "generator", + // A generator arm only comes from an Iterable (an optional + // one keeps its unit arms beside the iterable union's). + (a.kind === "generator" && iterableArmsElem(arms) === null), ) || !maybePromiseUnionOk(parts, arms, ctx) ) { @@ -6487,6 +6498,46 @@ export function unitOnlyUnion(unions: UnionRegistry): IrType { return { kind: "union", unionId: unions.intern(arms) }; } +/** `Iterable`: a union whose arms are the sources that keep their own + * representation — `T[]`, a `Set`, a `Map` (T the `[K, V]` + * tuple), a string (T string) — and a generator over T, which also holds + * every other iterator (Map/Set and array iterators adapt into one). The + * generator arm marks the union as iterable (iterableArmsElem): no other + * union has one. Null when T has no array or generator form. */ +export function iterableUnionType( + elem: IrType, + shapes: ShapeRegistry, + unions: UnionRegistry, +): IrType | null { + if (elem.kind === "void" || elem.kind === "generator" || !isSupportedArrayElem(elem)) return null; + if (!genResultRecord(elem, VOID, shapes, unions)) return null; + const arms: IrType[] = [ + { kind: "array", elem }, + { kind: "generator", yieldT: elem, retT: VOID, nextT: UNDEFINED_T }, + ]; + const elemArms = elem.kind === "union" ? unions.get(elem.unionId)?.arms : undefined; + if (isSupportedSetElem(elem, elemArms)) arms.push({ kind: "set", elem }); + if (elem.kind === "string") arms.push(STRING); + if (elem.kind === "record") { + const shape = shapes.get(elem.shapeId); + const [k, v] = shape?.fields ?? []; + if ( + shape?.tuple && + shape.fields.length === 2 && + k?.name === "0" && + v?.name === "1" && + isSupportedMapKey( + k.type, + k.type.kind === "union" ? unions.get(k.type.unionId)?.arms : undefined, + ) && + isSupportedMapValue(v.type) + ) + arms.push({ kind: "map", key: k.type, value: v.type }); + } + arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1)); + return { kind: "union", unionId: unions.intern(arms) }; +} + /** The IteratorResult record of a generator's value channels: `{ done: * boolean, value: V }` where V is dyn (an any/unknown channel) or the * canonical union of the yield arms, the return arms (when the return diff --git a/packages/compiler/src/ir/ir.test.ts b/packages/compiler/src/ir/ir.test.ts index 52ef9b9c1..d62c9c83c 100644 --- a/packages/compiler/src/ir/ir.test.ts +++ b/packages/compiler/src/ir/ir.test.ts @@ -10,6 +10,7 @@ import { canDynCheckTo, type IrBytesElem, isJsonSafeType, + iterableArmsElem, isJsonStringifySafeType, isJsonStringifyDynamicType, type IrRecordShape, @@ -22,6 +23,29 @@ import { typeKey, } from "./ir.js"; +describe("iterable unions", () => { + const gen = (yieldT: IrType): IrType => ({ + kind: "generator", + yieldT, + retT: { kind: "void" }, + nextT: { kind: "undefinedT" }, + }); + + test("names the element type of an Iterable union", () => { + expect(iterableArmsElem([arrayOf(F64), gen(F64), { kind: "set", elem: F64 }])).toEqual(F64); + expect(iterableArmsElem([arrayOf(STRING), gen(STRING), STRING])).toEqual(STRING); + expect(iterableArmsElem([arrayOf(F64), gen(F64), { kind: "undefinedT" }])).toEqual(F64); + }); + + test("rejects unions without exactly one plain generator arm over the same element", () => { + expect(iterableArmsElem([arrayOf(F64), STRING])).toBeNull(); + expect(iterableArmsElem([arrayOf(STRING), gen(F64)])).toBeNull(); + expect(iterableArmsElem([arrayOf(F64), gen(F64), gen(STRING)])).toBeNull(); + expect(iterableArmsElem([gen(F64), STRING])).toBeNull(); + expect(iterableArmsElem([arrayOf(F64), { ...gen(F64), nextT: F64 } as IrType])).toBeNull(); + }); +}); + describe("IR kind sets", () => { test("keeps procStream as the scalar handle exception", () => { expect(HANDLE_KINDS.has("procStream")).toBe(true); diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 7cd47c03c..cb0c7e496 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -730,6 +730,32 @@ function isCollectionValueKey(t: IrType): boolean { ); } +/** The element type of an ITERABLE union (`Iterable`, + * iterableUnionType): its one generator arm — a synchronous generator over + * T that returns nothing and takes no `.next()` values — beside source arms + * (T[], Set, a Map of [K, V] entries, a string), and the undefined and + * null arms of an optional Iterable. A generator arm is legal only in such + * a union. Null for every other union. */ +export function iterableArmsElem(arms: readonly IrType[]): IrType | null { + let elem: IrType | null = null; + for (const arm of arms) { + if (arm.kind !== "generator") continue; + if (elem !== null || arm.async || arm.retT.kind !== "void" || arm.nextT.kind !== "undefinedT") + return null; + elem = arm.yieldT; + } + if (elem === null) return null; + for (const arm of arms) { + if (arm.kind === "generator" || arm.kind === "undefinedT" || arm.kind === "nullT") continue; + if (arm.kind === "array" || arm.kind === "set") { + if (!typeEquals(arm.elem, elem)) return null; + } else if (arm.kind === "string") { + if (elem.kind !== "string") return null; + } else if (arm.kind !== "map") return null; + } + return elem; +} + /** Set elements and Map keys share storage, equality and ownership rules. */ export function isSupportedSetElem(t: IrType, unionArms?: IrType[]): boolean { return isSupportedMapKey(t, unionArms); diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 163c2a41c..217948641 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -18,6 +18,7 @@ import type { } from "./ir.js"; import { arrayOf, + iterableArmsElem, BYTES_ELEMENT_NAME, BOOL, BYTES_U8, @@ -801,7 +802,7 @@ export function validateModule(mod: IrModule): IrValidationError[] { arm.kind === "union" || arm.kind === "dyn" || arm.kind === "date" || - arm.kind === "generator" + (arm.kind === "generator" && iterableArmsElem(u.arms) === null) ) { errors.push({ message: `union ${u.id}: arm ${i} is ${arm.kind}`, loc: noLoc }); } diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-iterable-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-iterable-values.ts.json new file mode 100644 index 000000000..cf42c1888 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7380-iterable-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7380-iterable-values.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-iterable-forof-semantics.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-iterable-forof-semantics.ts.json new file mode 100644 index 000000000..824dfe11c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7381-iterable-forof-semantics.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7381-iterable-forof-semantics.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-iterable-generic-repository.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-iterable-generic-repository.ts.json new file mode 100644 index 000000000..7ac2b807f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7382-iterable-generic-repository.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7382-iterable-generic-repository.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-iterable-union-sources.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-iterable-union-sources.ts.json new file mode 100644 index 000000000..ea8476397 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7383-iterable-union-sources.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7383-iterable-union-sources.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/iterable-sources.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/iterable-sources.ts.json new file mode 100644 index 000000000..1b47e8dd2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/iterable-sources.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/iterable-sources.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7380-iterable-values.ts b/tests/corpus/7380-iterable-values.ts new file mode 100644 index 000000000..0c4284285 --- /dev/null +++ b/tests/corpus/7380-iterable-values.ts @@ -0,0 +1,122 @@ +// Iterable values: an array, Set, Map or string stays the same object +// (multi-pass, same identity, same inspection), and a generator or a +// Map/Set/array iterator stays single-pass. for-of, spread, Array.from and +// the Set/Map constructors consume every source. + +function sum(xs: Iterable): number { + let s = 0; + for (const x of xs) s += x; + return s; +} + +function* nums(): Generator { + yield 10; + yield 20; +} + +const arr = [1, 2, 3]; +const scores = new Map([ + ["a", 4], + ["b", 5], +]); +console.log(sum(arr), sum(new Set([1, 1, 2])), sum(scores.values()), sum(nums()), sum(arr.values())); + +// An array stays the same multi-pass object. +const it: Iterable = arr; +console.log(sum(it), sum(it), it === arr, typeof it, it); + +// A generator stays single-pass. +const once: Iterable = nums(); +console.log(sum(once), sum(once), typeof once); + +console.log([...it], Array.from(nums() as Iterable), new Set(it).size); + +// A function returning either a collection iterator or a fresh array. +function pick(rows: Map, ids: string[] | null): Iterable { + if (ids === null) return rows.values(); + const out: number[] = []; + for (const id of ids) out.push(rows.get(id) ?? 0); + return out; +} +let total = 0; +for (const v of pick(scores, null)) total += v; +for (const v of pick(scores, ["b", "b", "z"])) total += v; +console.log(total); + +// Strings iterate by code point; Maps iterate [key, value] entries. +function upper(s: Iterable): string[] { + const out: string[] = []; + for (const w of s) out.push(w.toUpperCase()); + return out; +} +console.log(upper("héllo😀"), upper(["a", "b"]), upper(new Set(["x"]))); +function pairs(p: Iterable<[string, number]>): string { + let out = ""; + for (const [k, v] of p) out += `${k}=${v};`; + return out; +} +console.log(pairs(scores), pairs([["x", 1]]), pairs(scores.entries()), pairs(new Set<[string, number]>([["s", 9]]))); + +// Constructors and Object.fromEntries take any Iterable. +function toMap(p: Iterable<[string, number]>): Map { + return new Map(p); +} +function toSet(xs: Iterable): Set { + return new Set(xs); +} +console.log(toMap([["a", 1], ["b", 2]]), toMap(scores), Object.fromEntries(toMap(new Set<[string, number]>([["c", 3]])))); +console.log(toSet([3, 3, 4]), toSet(new Set([9])), toSet(nums())); + +// Fields, generator functions declared to return Iterable, nested Iterables. +class Holder { + source: Iterable; + constructor(source: Iterable) { + this.source = source; + } + joined(): string { + let out = ""; + for (const s of this.source) out += s; + return out; + } +} +const holder = new Holder(["a", "b"]); +console.log(holder.joined(), holder.joined()); +holder.source = new Set(["x", "y"]); +console.log(holder.joined(), holder); + +function* range(n: number): Iterable { + for (let i = 0; i < n; i++) yield i; +} +console.log([...range(4)], sum(range(5))); + +const grid: Iterable[] = [[1, 2], new Set([3]), range(3)]; +let cells = 0; +for (const row of grid) for (const c of row) cells += c; +console.log(cells, grid.length); + +// A checked cast of an iterator value. +const entries = new Map([["k", 1]]).entries() as Iterable<[string, number]>; +console.log(toMap(entries)); + +// Optional Iterables narrow to the Iterable once proven present. +function totalOf(xs?: Iterable): number { + if (xs === undefined) return -1; + let s = 0; + for (const x of xs) s += x; + return s; +} +function orEmpty(xs: Iterable | null): number[] { + return xs === null ? [] : [...xs]; +} +console.log(totalOf(), totalOf([1, 2]), totalOf(new Set([5])), orEmpty(null), orEmpty(range(2))); +const byName = new Map>([ + ["a", [1, 2]], + ["b", new Set([3])], +]); +for (const k of ["a", "b", "c"]) { + const got = byName.get(k); + if (got !== undefined) console.log(k, [...got]); + else console.log(k, "none"); +} +const top = byName.get("a"); +console.log(top !== undefined ? [...top] : "none", byName); diff --git a/tests/corpus/7381-iterable-forof-semantics.ts b/tests/corpus/7381-iterable-forof-semantics.ts new file mode 100644 index 000000000..10f1d1d2f --- /dev/null +++ b/tests/corpus/7381-iterable-forof-semantics.ts @@ -0,0 +1,122 @@ +// for-of over Iterable keeps the semantics of the held source: an array +// is walked live, a generator is closed on break, return and throw (its +// finally blocks run) but not when it is exhausted or throws itself, and +// labels, closures, await and yield work in the loop body. + +function* noisy(tag: string): Generator { + try { + yield 1; + yield 2; + yield 3; + } finally { + console.log("closed", tag); + } +} + +const early: Iterable = noisy("break"); +for (const v of early) { + if (v === 2) break; + console.log("v", v); +} + +function firstOver(src: Iterable, n: number): number | undefined { + for (const x of src) if (x > n) return x; + return undefined; +} +console.log(firstOver([1, 5, 9], 4), firstOver(noisy("return"), 1), firstOver(noisy("exhausted"), 9)); + +try { + for (const v of noisy("throw") as Iterable) { + if (v === 1) throw new Error("boom"); + } +} catch (e) { + console.log("caught", (e as Error).message); +} + +function* failing(): Generator { + try { + yield 1; + throw new RangeError("generator failed"); + } finally { + console.log("failing finally"); + } +} +try { + for (const v of failing() as Iterable) console.log("got", v); +} catch (e) { + console.log(String(e)); +} + +// The array is read live: elements pushed during the walk are visited. +const grow: number[] = [1, 2]; +let visits = 0; +for (const v of grow as Iterable) { + if (grow.length < 5) grow.push(v * 10); + visits++; +} +console.log(visits, grow); + +// Holes read undefined when T holds it. +function show(xs: Iterable): string { + const out: string[] = []; + for (const x of xs) out.push(String(x)); + return out.join(","); +} +console.log(show([1, , 3]), show([undefined, 2])); + +// Labels, closures over the per-iteration binding, try/finally, a class +// declared in the body, and an existing binding as the loop target. +function run(src: Iterable, tag: string): void { + const fns: (() => number)[] = []; + let acc = 0; + outer: for (const x of src) { + fns.push(() => x * 10); + try { + if (x === 2) continue outer; + class Local { + v = x; + twice(): number { + return this.v * 2; + } + } + acc += new Local().twice(); + const inner = (y: number): number => y + x; + acc += inner(1); + } finally { + acc += 100; + } + } + console.log(tag, acc, fns.map((f) => f()).join(",")); +} +function* three(): Generator { + yield 1; + yield 2; + yield 3; +} +run([1, 2, 3], "array"); +run(three(), "generator"); +run(new Set([1, 2, 3]), "set"); + +let product = 0; +outer: for (const a of [1, 2, 3] as Iterable) { + for (const b of new Set([10, 20]) as Iterable) { + if (b === 20) continue outer; + if (a === 3) break outer; + product += a * b; + } +} +let target = 0; +for (target of [7, 8] as Iterable) { +} +console.log(product, target); + +// await and yield inside the loop body. +async function asyncSum(src: Iterable): Promise { + let s = 0; + for (const x of src) s += await Promise.resolve(x); + return s; +} +function* doubled(src: Iterable): Generator { + for (const x of src) yield x * 2; +} +asyncSum([4, 5]).then((v) => console.log("async", v, [...doubled([1, 2])], [...doubled(new Set([7]))])); diff --git a/tests/corpus/7382-iterable-generic-repository.ts b/tests/corpus/7382-iterable-generic-repository.ts new file mode 100644 index 000000000..3e9e62742 --- /dev/null +++ b/tests/corpus/7382-iterable-generic-repository.ts @@ -0,0 +1,57 @@ +// A generic repository whose query candidates are an Iterable: the +// live row map's values() iterator when nothing is indexed, or an array of +// index hits. Methods returning Iterable keep the class compilable. + +interface Row { + readonly id: string; + readonly tag: string; + readonly n: number; +} + +class Repository { + private readonly rows = new Map(); + private readonly index = new Map(); + + private readonly key: (row: T) => string; + + constructor(key: (row: T) => string) { + this.key = key; + } + + insert(row: T): void { + this.rows.set(row.id, row); + const k = this.key(row); + const ids = this.index.get(k); + if (ids === undefined) this.index.set(k, [row.id]); + else ids.push(row.id); + } + + all(): Iterable { + return this.rows.values(); + } + + candidates(key: string | null): Iterable { + if (key === null) return this.rows.values(); + const out: T[] = []; + for (const id of this.index.get(key) ?? []) { + const row = this.rows.get(id); + if (row !== undefined) out.push(row); + } + return out; + } + + query(key: string | null, where: (row: T) => boolean): T[] { + const matches: T[] = []; + for (const row of this.candidates(key)) if (where(row)) matches.push(row); + return matches; + } +} + +const repo = new Repository((row) => row.tag); +for (let i = 0; i < 12; i++) repo.insert({ id: `r${i}`, tag: i % 3 === 0 ? "red" : "blue", n: i * i }); +console.log(repo.query(null, (r) => r.n > 50).map((r) => r.id).join(",")); +console.log(repo.query("red", (r) => r.n % 2 === 0).map((r) => r.id).join(",")); +console.log(repo.query("green", () => true).length, [...repo.all()].length); +let sum = 0; +for (const row of repo.candidates("blue")) sum += row.n; +console.log(sum, Array.from(repo.candidates(null)).slice(0, 2)); diff --git a/tests/corpus/7383-iterable-union-sources.ts b/tests/corpus/7383-iterable-union-sources.ts new file mode 100644 index 000000000..3c393b256 --- /dev/null +++ b/tests/corpus/7383-iterable-union-sources.ts @@ -0,0 +1,51 @@ +// Iterable unions without an array arm, and optional or nullable Iterable +// slots that take a generator whose return channel is not void. + +function* evens(limit: number): Generator { + for (let i = 0; i <= limit; i += 2) yield i; +} + +// A union of a Set and a generator is an iterable union with no array arm: +// for-of walks every arm through the generator loop. +function total(src: Set | Generator): number { + let sum = 0; + for (const v of src) sum += v; + return sum; +} +console.log(total(new Set([1, 2, 3])), total(evens(6))); + +const labelled = (src: Set | Generator): string[] => { + const out: string[] = []; + outer: for (const v of src) { + if (v > 4) break outer; + out.push(`#${v}`); + } + return out; +}; +console.log(labelled(new Set([1, 3, 5])).join(" "), labelled(evens(10)).join(" ")); + +// A generator that returns a value: its return channel is dropped when it +// flows into an Iterable, required, optional or nullable alike. +function* countdown(from: number): Generator { + for (let i = from; i > 0; i--) yield i; + return "liftoff"; +} + +function joined(xs: Iterable): string { + return [...xs].join(","); +} + +function maybeJoined(xs?: Iterable): string { + return xs === undefined ? "none" : [...xs].join(","); +} + +function nullableJoined(xs: Iterable | null): string { + if (xs === null) return "null"; + let out = ""; + for (const x of xs) out += `${x};`; + return out; +} + +console.log(joined(countdown(3))); +console.log(maybeJoined(countdown(2)), maybeJoined(), maybeJoined(undefined), maybeJoined([7, 8])); +console.log(nullableJoined(countdown(4)), nullableJoined(null), nullableJoined(new Set([9]))); diff --git a/tests/diagnostics/iterable-sources.ts b/tests/diagnostics/iterable-sources.ts new file mode 100644 index 000000000..c93029cce --- /dev/null +++ b/tests/diagnostics/iterable-sources.ts @@ -0,0 +1,17 @@ +// An Iterable value holds an array, Set, Map, string or iterator. A +// class instance with its own [Symbol.iterator] and a typed array have no +// representation there, so the conversion site reports them by name. +class Countdown implements Iterable { + *[Symbol.iterator](): Iterator { + yield 2; + yield 1; + } +} +function sum(xs: Iterable): number { + let s = 0; + for (const x of xs) s += x; + return s; +} +console.log(sum(new Countdown())); +console.log(sum(new Uint8Array([1, 2]))); +export const marker: number = 1; diff --git a/tests/harness/__snapshots__/iterable-sources.ts.txt b/tests/harness/__snapshots__/iterable-sources.ts.txt new file mode 100644 index 000000000..cac17a49e --- /dev/null +++ b/tests/harness/__snapshots__/iterable-sources.ts.txt @@ -0,0 +1,13 @@ +iterable-sources.ts:15:17 - error SC1090: an Iterable holding a 'Countdown' (an Iterable value holds an array, Set, Map, string, or iterator) is not supported yet + + 14 | } + 15 | console.log(sum(new Countdown())); + | ^~~~~~~~~~~~~~~ + 16 | console.log(sum(new Uint8Array([1, 2]))); + +iterable-sources.ts:16:17 - error SC1090: an Iterable holding a 'Uint8Array' (an Iterable value holds an array, Set, Map, string, or iterator) is not supported yet + + 15 | console.log(sum(new Countdown())); + 16 | console.log(sum(new Uint8Array([1, 2]))); + | ^~~~~~~~~~~~~~~~~~~~~~ + 17 | export const marker: number = 1; \ No newline at end of file diff --git a/tests/harness/corpus-costs/7380-iterable-values.ts.json b/tests/harness/corpus-costs/7380-iterable-values.ts.json new file mode 100644 index 000000000..87569618d --- /dev/null +++ b/tests/harness/corpus-costs/7380-iterable-values.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6000 } diff --git a/tests/harness/corpus-costs/7381-iterable-forof-semantics.ts.json b/tests/harness/corpus-costs/7381-iterable-forof-semantics.ts.json new file mode 100644 index 000000000..87569618d --- /dev/null +++ b/tests/harness/corpus-costs/7381-iterable-forof-semantics.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6000 } diff --git a/tests/harness/corpus-costs/7382-iterable-generic-repository.ts.json b/tests/harness/corpus-costs/7382-iterable-generic-repository.ts.json new file mode 100644 index 000000000..87569618d --- /dev/null +++ b/tests/harness/corpus-costs/7382-iterable-generic-repository.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6000 } diff --git a/tests/harness/corpus-costs/7383-iterable-union-sources.ts.json b/tests/harness/corpus-costs/7383-iterable-union-sources.ts.json new file mode 100644 index 000000000..87569618d --- /dev/null +++ b/tests/harness/corpus-costs/7383-iterable-union-sources.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 6000 }