Skip to content
LogoLogo

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 effect function or block.

  • continue ends that one item. break and return are 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 for can only be followed by ( or await.

  • Amended 2026-10-29: the limit. In for concurrent(n), n is any Effect Concurrency, passed straight to concurrency. In effect 4.1.1, Concurrency is 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.Concurrency and pass its evaluated value unchanged to Effect.forEach, without positive/integer/finite validation, clamping or rounding in VibeLang. Evaluate it once before the operand's head environment. On Effect 4.1.1, numeric limits below 1 still visit every item sequentially as 1 does; 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 ForBinding that for…of takes (const, let, destructuring), minus var. The operand is an Iterable only; a Stream is a compile error that points to for 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/OfHeadEvaluation requires. 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 Effect 4.1.1, concurrency 1 runs 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 1 pulls incrementally without calling iterator return() merely on item failure. The parallel branch obtains its items through Array.fromIterable before 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"