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 }