VIP-19: for concurrent
Status: accepted by Will, 2026-10-28.
Decision
for concurrent (const id of ids) { try syncOne(id) } // every item in its own fiber, all at once
for concurrent(4) (const id of ids) { … } // at most 4 at a time
// exactly this Effect TS
yield* Effect.forEach(ids, (id) => Effect.gen(function* () { yield* syncOne(id) }),
{ concurrency: "unbounded", discard: true }) // or concurrency: 4-
All items join before the loop ends, and one failure interrupts the rest. It is a statement inside an
effectfunction or block. -
continueends that one item.breakandreturnare compile errors, because no loop runs on the body's fiber. -
Results are discarded;
try.all ids.map(…)(VIP-18) keeps them. -
Ambiguity: none. JavaScript's
forcan only be followed by(orawait. -
Amended 2026-10-29: the limit. In
for concurrent(n),nis any EffectConcurrency, passed straight toconcurrency. In effect 4.1.1,Concurrencyis a number or"unbounded"(Types.d.ts); the decision as sent also listed"inherit", which 4.1.1 doesn't have. Sent to Will on Telegram 2026-10-29 11:28 under his standing rule 'assume I accept'; no objection by 12:00. -
Amended 2026-10-30: the full numeric limit domain. Keep the limit assignable to pinned
Types.Concurrencyand pass its evaluated value unchanged toEffect.forEach, without positive/integer/finite validation, clamping or rounding in VibeLang. Evaluate it once before the operand's head environment. On Effect4.1.1, numeric limits below1still visit every item sequentially as1does; positive integers bound concurrent items, while other numbers retain Effect's own behavior. A zero limit does not skip the iterable. See Running Effects. -
Amended 2026-10-29: bindings and operands. The head takes every
ForBindingthatfor…oftakes (const,let, destructuring), minusvar. The operand is anIterableonly; a Stream is a compile error that points tofor try. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 48).
Corrected 2026-10-29: the example calls syncOne(id), a user function, because sync is the VIP-6 global Effect.sync, which takes a thunk (found by the round-4 review).
-
Amended 2026-10-30: source head scope and item checks. Evaluate the limit before entering the right-hand operand's separate lexical environment with uninitialized head bindings, as ECMAScript's
ForIn/OfHeadEvaluationrequires. RHS closures retain those TDZ cells; each item receives fresh initialized bindings and the body keeps its own block. Apply the loop's outward-jump restrictions to head binding initialization and body, including defaults and computed keys, while nested source functions keep their own targets. The limit and RHS retain the enclosing activation's permitted outward completions. These are source behavior requirements, independent of generated callbacks; see Running Effects. In Effect4.1.1, concurrency1runs items sequentially on the enclosing fiber; higher limits and"unbounded"fork per item. -
Amended 2026-10-30: iterable pulling and closing follow pinned Effect. Effective concurrency
1pulls incrementally without calling iteratorreturn()merely on item failure. The parallel branch obtains its items throughArray.fromIterablebefore starting item Effects: a non-array iterable is materialized, while an existing array is reused by reference without consulting its custom iterator. A concurrency limit bounds active work, not pulling; an endless non-array input cannot finish parallel materialization. No extra closing, prefetch, snapshot or finiteness check is added. The omitted limit stays"unbounded"; numeric limits keep native Effect behavior. See Running Effects and the finite witness.
History
One finding, in round 5.
Prior art
JavaScript's for await (…), which puts a keyword between for and (; Rayon's par_iter().for_each.
Will's words
"recomendation is good"