mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Compile Iterable<T> values (#1875)
This commit is contained in:
@@ -24,6 +24,8 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/language-features/async-iteration.mdx</include>
|
||||
|
||||
<include>../limitations/language-features/iterable-values.mdx</include>
|
||||
|
||||
<include>../limitations/language-features/generics.mdx</include>
|
||||
|
||||
<include>../limitations/language-features/function-values.mdx</include>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
- **Iterable values:** A value typed `Iterable<T>` can hold an array, a Set, a Map (for `Iterable<[K, V]>`), a string (for `Iterable<string>`), 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<T>` and are reported at the conversion.
|
||||
@@ -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<T> 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<T> 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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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<T>`: 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(
|
||||
|
||||
@@ -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<T> returns a generator over T.
|
||||
if (mappedReturn && decl.asteriskToken)
|
||||
mappedReturn = iterableGeneratorArm(lowerer, mappedReturn);
|
||||
if (
|
||||
mappedReturn === null &&
|
||||
decl.asteriskToken &&
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
/* `Iterable<T>` 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<T> | 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<T> | 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<T> 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 = <iterable>;
|
||||
* if (%src is the array arm) {
|
||||
* const %arr = <array arm>;
|
||||
* for (let %cur = 0; %cur < %arr.length; %cur++) { const x = %arr[%cur]; <body> }
|
||||
* } else {
|
||||
* for (const x of GetIterator(%src)) <body> // 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<T> | Generator<T>`) 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();
|
||||
}
|
||||
}
|
||||
@@ -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":
|
||||
|
||||
@@ -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 }[];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<T>: 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<T, TReturn> — the checker's type of `g.next()` (an
|
||||
// alias for IteratorYieldResult<T> | IteratorReturnResult<TReturn>):
|
||||
// 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<T> (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<T>`: a union whose arms are the sources that keep their own
|
||||
* representation — `T[]`, a `Set<T>`, a `Map<K, V>` (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
|
||||
|
||||
@@ -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<T> 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);
|
||||
|
||||
@@ -730,6 +730,32 @@ function isCollectionValueKey(t: IrType): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** The element type of an ITERABLE union (`Iterable<T>`,
|
||||
* iterableUnionType): its one generator arm — a synchronous generator over
|
||||
* T that returns nothing and takes no `.next()` values — beside source arms
|
||||
* (T[], Set<T>, 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);
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7380-iterable-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7381-iterable-forof-semantics.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7382-iterable-generic-repository.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7383-iterable-union-sources.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/iterable-sources.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Iterable<T> 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>): number {
|
||||
let s = 0;
|
||||
for (const x of xs) s += x;
|
||||
return s;
|
||||
}
|
||||
|
||||
function* nums(): Generator<number, void, undefined> {
|
||||
yield 10;
|
||||
yield 20;
|
||||
}
|
||||
|
||||
const arr = [1, 2, 3];
|
||||
const scores = new Map<string, number>([
|
||||
["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<number> = arr;
|
||||
console.log(sum(it), sum(it), it === arr, typeof it, it);
|
||||
|
||||
// A generator stays single-pass.
|
||||
const once: Iterable<number> = nums();
|
||||
console.log(sum(once), sum(once), typeof once);
|
||||
|
||||
console.log([...it], Array.from(nums() as Iterable<number>), new Set(it).size);
|
||||
|
||||
// A function returning either a collection iterator or a fresh array.
|
||||
function pick(rows: Map<string, number>, ids: string[] | null): Iterable<number> {
|
||||
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>): 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<string, number> {
|
||||
return new Map(p);
|
||||
}
|
||||
function toSet(xs: Iterable<number>): Set<number> {
|
||||
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<string>;
|
||||
constructor(source: Iterable<string>) {
|
||||
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<number> {
|
||||
for (let i = 0; i < n; i++) yield i;
|
||||
}
|
||||
console.log([...range(4)], sum(range(5)));
|
||||
|
||||
const grid: Iterable<number>[] = [[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>): number {
|
||||
if (xs === undefined) return -1;
|
||||
let s = 0;
|
||||
for (const x of xs) s += x;
|
||||
return s;
|
||||
}
|
||||
function orEmpty(xs: Iterable<number> | 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<string, Iterable<number>>([
|
||||
["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);
|
||||
@@ -0,0 +1,122 @@
|
||||
// for-of over Iterable<T> 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<number, void, undefined> {
|
||||
try {
|
||||
yield 1;
|
||||
yield 2;
|
||||
yield 3;
|
||||
} finally {
|
||||
console.log("closed", tag);
|
||||
}
|
||||
}
|
||||
|
||||
const early: Iterable<number> = noisy("break");
|
||||
for (const v of early) {
|
||||
if (v === 2) break;
|
||||
console.log("v", v);
|
||||
}
|
||||
|
||||
function firstOver(src: Iterable<number>, 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<number>) {
|
||||
if (v === 1) throw new Error("boom");
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("caught", (e as Error).message);
|
||||
}
|
||||
|
||||
function* failing(): Generator<number, void, undefined> {
|
||||
try {
|
||||
yield 1;
|
||||
throw new RangeError("generator failed");
|
||||
} finally {
|
||||
console.log("failing finally");
|
||||
}
|
||||
}
|
||||
try {
|
||||
for (const v of failing() as Iterable<number>) 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<number>) {
|
||||
if (grow.length < 5) grow.push(v * 10);
|
||||
visits++;
|
||||
}
|
||||
console.log(visits, grow);
|
||||
|
||||
// Holes read undefined when T holds it.
|
||||
function show(xs: Iterable<number | undefined>): 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<number>, 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<number, void, undefined> {
|
||||
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<number>) {
|
||||
for (const b of new Set([10, 20]) as Iterable<number>) {
|
||||
if (b === 20) continue outer;
|
||||
if (a === 3) break outer;
|
||||
product += a * b;
|
||||
}
|
||||
}
|
||||
let target = 0;
|
||||
for (target of [7, 8] as Iterable<number>) {
|
||||
}
|
||||
console.log(product, target);
|
||||
|
||||
// await and yield inside the loop body.
|
||||
async function asyncSum(src: Iterable<number>): Promise<number> {
|
||||
let s = 0;
|
||||
for (const x of src) s += await Promise.resolve(x);
|
||||
return s;
|
||||
}
|
||||
function* doubled(src: Iterable<number>): Generator<number, void, undefined> {
|
||||
for (const x of src) yield x * 2;
|
||||
}
|
||||
asyncSum([4, 5]).then((v) => console.log("async", v, [...doubled([1, 2])], [...doubled(new Set([7]))]));
|
||||
@@ -0,0 +1,57 @@
|
||||
// A generic repository whose query candidates are an Iterable<T>: the
|
||||
// live row map's values() iterator when nothing is indexed, or an array of
|
||||
// index hits. Methods returning Iterable<T> keep the class compilable.
|
||||
|
||||
interface Row {
|
||||
readonly id: string;
|
||||
readonly tag: string;
|
||||
readonly n: number;
|
||||
}
|
||||
|
||||
class Repository<T extends { readonly id: string }> {
|
||||
private readonly rows = new Map<string, T>();
|
||||
private readonly index = new Map<string, string[]>();
|
||||
|
||||
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<T> {
|
||||
return this.rows.values();
|
||||
}
|
||||
|
||||
candidates(key: string | null): Iterable<T> {
|
||||
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) => 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));
|
||||
@@ -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<number, void, undefined> {
|
||||
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<number> | Generator<number, void, undefined>): 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<number> | Generator<number, void, undefined>): 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<number, string, unknown> {
|
||||
for (let i = from; i > 0; i--) yield i;
|
||||
return "liftoff";
|
||||
}
|
||||
|
||||
function joined(xs: Iterable<number>): string {
|
||||
return [...xs].join(",");
|
||||
}
|
||||
|
||||
function maybeJoined(xs?: Iterable<number>): string {
|
||||
return xs === undefined ? "none" : [...xs].join(",");
|
||||
}
|
||||
|
||||
function nullableJoined(xs: Iterable<number> | 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])));
|
||||
@@ -0,0 +1,17 @@
|
||||
// An Iterable<T> 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<number> {
|
||||
*[Symbol.iterator](): Iterator<number> {
|
||||
yield 2;
|
||||
yield 1;
|
||||
}
|
||||
}
|
||||
function sum(xs: Iterable<number>): 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;
|
||||
@@ -0,0 +1,13 @@
|
||||
iterable-sources.ts:15:17 - error SC1090: an Iterable<number> 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<number> 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;
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4000, "san": 6000 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4000, "san": 6000 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4000, "san": 6000 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4000, "san": 6000 }
|
||||
Reference in New Issue
Block a user