Skip to content
LogoLogo

Runtime guarantees

Source: VIP-31.

  • No independent Effect runtime semantics. Every accepted construct is sugar for its specified counterpart. Failures, defects, interruption, finalizer order and the combination of causes are those of the pinned effect 4.1.1 (Configuration). Pattern matching follows VIP-7’s pinned TC39 snapshot and its explicit Effect-pattern adaptations, including its class tests.
  • Module boundaries. The source module classification is fixed before checking and survives JavaScript and declaration emission; a generated prelude import cannot change binding visibility or select different script-versus-module behavior. Execution still follows the configured output format and its ordinary host rules.
  • The guarantee. Compiled output gives the same Exit as the accepted VIP’s Effect TypeScript counterpart: success, failure, defect or interruption, with finalizers run in the same order. This specification spells out that counterpart. Running Effects specifies operand order: temporaries preserve source order wherever Effect's argument order differs, fork.in(scope) e included. Compare the canonical Exit and the finalizer trace. The canonical form is CanonicalVIPExit in typescript/tsc/internal/vibelangfuzz/vip.go: object keys are ordered; Cause.reasons retain their order and each reason’s _tag and error, defect or fiberId; payloads recursively encode class name ($class), own enumerable string and symbol fields (symbol identity) and an error’s name and message, dropping stack; fiber IDs are renamed by first appearance. The encoded object’s fields dictionary treats source property names as data: they cannot select runtime tags or invoke inherited setters. Its encoded values still validate and normalize actual nested runtime values. Object identity is not compared: independently allocated errors with equal encoded fields agree. A fixture names any extra comparison it needs.
  • Interruptor identifier equality. Renaming preserves the native equality classes of Cause.interruptors: repeated NaN identifies one interruptor, Infinity and -Infinity identify different interruptors, and 0 and -0 identify the same interruptor. For an interrupt-only failure Exit, identifiers [NaN, Infinity, -Infinity, NaN] have the canonical identity sequence [0, 1, 2, 0]; identifiers [NaN, NaN, NaN, NaN] have [0, 0, 0, 0].
  • Symbol identity in comparison. Under VIP-31's own-enumerable-field rule, generic payloads select keys with Reflect.ownKeys and their own enumerable descriptors. String fields are sorted, omitting the string stack; symbol entries keep native own-key order, each key before its recursively encoded value. A symbol described as stack is retained. Keys and symbol values share identity tokens throughout one Exit, including nested values and container entries: actual well-known symbols use their names, registry symbols their exact registry keys, and local symbols first-appearance ordinals and exact descriptions (undefined differs from ""). Repeated local symbols reuse their ordinal; distinct symbols with equal descriptions get different ordinals. Each run starts a fresh table; hidden or omitted fields allocate no ordinals. Source property names remain data, and object allocation identity remains ignored.
  • How it is checked. Each VIP’s VibeLang and Effect TypeScript pair is a differential test: it compiles the VibeLang, runs both, and compares their Exits and finalizer traces. The fuzz suite generates more pairs. It also compares recorded events at the function call, before running its returned Effect, and the complete ordered event trace after the run (VIPCompare in typescript/tsc/internal/vibelangfuzz/vip.go). Equal Exits alone cannot distinguish an eager body from a cold one. Call-time events, including parameter-default evaluation, must match the counterpart; running the same returned Effect again reruns its body, without repeating the call or its defaults (VIP-1, VIP-2). Checking only the Effect TypeScript validates the counterpart, not compiler conformance; skipped pairs establish no conformance.
  • Encoded reason tags. An encoded cause reason's _tag must be the string Fail, Die or Interrupt, including in nested encodings; any other tag value fails the comparison with an encoding error.
  • Runtime helpers do not patch Effect (VIP-7). They implement only the specified adaptations: defining Symbol.customMatcher where absent (below); generated error matchers; the class tests below and the pinned snapshot's extraction rules, without installing matcher methods on built-in globals; and the retained fixed checks for Effect's Option, Exit, Result and tagged errors (VIP-7). These checks follow resolved bindings, including aliases, not identifier spelling. The forty fixed tests and extracted lists are listed once in Retained fixed patterns. VIP-7 keeps today’s fixed checks for Effect’s tagged errors until Effect ships matchers; neither inherited instance matching nor a new universal _tag test replaces them.
  • Matcher symbol initialization. When the host lacks Symbol.customMatcher, runtime initialization defines an own property on the global Symbol with value Symbol.for("Symbol.customMatcher"), non-writable, non-enumerable and non-configurable, like a well-known symbol. A pre-existing host symbol is preserved with its native identity. The runtime's customMatcher export and every generated or user-written matcher use that actual key; no runtime copy creates a fresh fallback symbol. Emitted .vibe and .vibex modules that use the key request the runtime initialization dependency first, before every other import or re-export dependency, independently of the Effect prelude switches. Initialization is synchronous and does not depend on application modules; under CommonJS it runs before any other emitted dependency load. This initializes the key before dependencies first reached through that module and before its body; it cannot undo evaluation through an earlier entry or import path. Source dependencies retain their relative order. A declaration type reference performs no initialization. TypeScript modules (.ts, .tsx and VIP-40 bodies) retain ordinary dependency order and arrange initialization before using the key, normally with import "vibelang-runtime" first. Without prior host or runtime initialization, Symbol.customMatcher can be undefined even when its ambient type is visible. Cycles and top-level await keep their ordinary TypeScript meanings. Registry symbols retain the host's normal realm and agent boundaries; this adds no symbol transport across agents. See source and declaration types.
  • Class patterns have one definition, in Class patterns and Retained fixed patterns: a retained guard, an own Symbol.customMatcher, a built-in test, instanceof, or a call, chosen in that order. This deliberately differs from the pinned draft's [[ConstructedBy]] and internal-slot tests, which JavaScript cannot read for untouched .ts and .js objects. No helper patches Function.prototype or records construction.
  • Source bindings and control flow survive lowering. Bindings and jump targets are resolved on the source before any callback is introduced. Function bodies follow Functions (this, super, new.target); region bodies follow Region bodies (source completions kept separately from primitive Effect success payloads); expressions follow the evaluation-order rule (call references, receivers, short-circuits); destructuring and parameter defaults behave as JavaScript with the do proposal. The lowering is the implementation's choice, checked by those sections' examples and the fuzz suite. A nested source function keeps its own bindings. The legacy JavaScript uses listed in Legacy JavaScript are compile errors throughout .vibe, so no lowering has to preserve them. Type references such as Function and property names such as o.arguments keep TypeScript’s meaning.
  • Source names survive captures. Generated pipe, do/match and completion-record temporaries preserve source naming context, including what class static initializers observe; a generated binding cannot add an anonymous name, erase an intended name, or repair it after creation.
  • Match cache and cleanup follow Pattern matching, including its explicit snapshot reconciliations and cleanup rule.
  • Region observers. provide and interruption regions preserve the original generator payload at every Effect/Scope boundary: normal completion and outward break/continue succeed with undefined, while source return value succeeds with that value, preserving identity. Source completion values and targets are carried separately and transferred only after successful finalization. Nested regions apply the rule independently; compiler control records never become finalizer payloads. See Region bodies.
  • Pattern binding lifetimes follow Pattern bindings; helper generation preserves the source lexical cells and the closures that capture them.
  • Abandonment. A rejected await resumes the async function with a throw. A failed try never resumes the generator. try e is yield* e, and Effect.gen calls only the generator's next, only on success. So when a yield* in effect code fails, dies or is interrupted, no JavaScript after it runs: no using disposal, no destructuring IteratorClose, no FinishMatch. throw e in effect code is return yield* Effect.fail(e), so every throw abandons the generator, wherever it stands: in an arm, a guard, or a do in either. A JavaScript exception is different: a getter that throws, the no-match TypeError or a call that throws unwinds the generator and then becomes a defect. Cleanup follows the applicable JavaScript or matching algorithm; iterator acquisition and advancement failures do not necessarily invoke IteratorClose. Use ensuring, scoped or acquireRelease for cleanup on every exit.
  • Effect handlers would need a continuation runtime. They are an open VIP (VIP-35), not part of the language.

The matcher-symbol shim has this TypeScript initialization counterpart. It runs in the synchronous initialization dependency described above, before other dependencies first reached through the importing VibeLang module:

Effect
const host = Symbol as SymbolConstructor & { readonly customMatcher?: symbol }
if (host.customMatcher === undefined) {
  Object.defineProperty(host, "customMatcher", { value: Symbol.for("Symbol.customMatcher") })
}
const actualMatcherKey = host.customMatcher!

The following comparator regression cases are pending VIP-31. In each row, a and b are distinct Symbol("same") values; each side is the success payload of an independent run. Symbol fields must be retained, and aliases must survive encoding.

Left payloadRight payloadCanonical comparison
{ [Symbol("a")]: 1 }{ [Symbol("a")]: 2 }different
{ first: a, second: a }{ first: a, second: b }different
{ [a]: a }{ [b]: b }equal
{ [a]: a }{ [a]: b }different

In plain functions, for…of keeps stock TypeScript's emitted behavior, including thrown-value identity. If the body throws, IteratorClose preserves that exact value even when the iterator's return throws. If the body leaves normally through break or return, an error from closing replaces that completion. Nested loops apply this rule at each close; lowering preserves every source label on its loop.

Native iterator completion determines cleanup. In this example all three runs end with Die(problem): an exception from next or a default after exhaustion has trace [], while a default exception before exhaustion closes the iterator, records ["close"] and wins over closeProblem. See the destructuring rule.

Effect
// Native iterator completion witness: VIP-2, VIP-31
import { Effect, Exit } from "effect"
 
const problem = { label: "source" }, closeProblem = { label: "close" }
const box = { get value(): number { throw problem } }
for (const phase of ["advance", "default", "exhausted"] as const) {
  const trace: string[] = []
  const input: Iterable<number | undefined> = {
    [Symbol.iterator]() {
      return {
        next(): IteratorResult<number | undefined> {
          if (phase === "advance") throw problem
          if (phase === "exhausted") return { done: true, value: undefined }
          return { done: false, value: undefined }
        },
        return(): IteratorResult<number | undefined> {
          trace.push("close")
          throw closeProblem
        },
      }
    },
  }
  const exit = Effect.runSyncExit(Effect.gen(function* () {
    const [value = box.value] = input
    return value
  }))
  if (!Exit.isFailure(exit) || exit.cause.reasons.length !== 1 ||
      exit.cause.reasons[0]._tag !== "Die" || exit.cause.reasons[0].defect !== problem ||
      trace.join() !== (phase === "default" ? "close" : "")) throw new Error("native iterator completion changed")
}

The call below evaluates its default once. Each run then succeeds with 7 and appends one body event:

Effect
// Coldness witness: VIP-1, VIP-2, VIP-31
import { Effect, Exit } from "effect"
 
const trace: string[] = []
function parameter() { trace.push("parameter"); return 7 }
function program(value = parameter()) {
  return Effect.gen(function* () { trace.push("body"); return value })
}
const p = program()
const called = trace.slice()
if (called.includes("body")) throw new Error("body ran during construction")
if (called.join() !== "parameter") throw new Error("call-time default changed")
const first = Effect.runSyncExit(p)
const second = Effect.runSyncExit(p)
if (!Exit.isSuccess(first) || first.value !== 7 ||
    !Exit.isSuccess(second) || second.value !== 7) throw new Error("Exit changed")
if (trace.join() !== "parameter,body,body") throw new Error("body did not rerun")
VibeLang
// compiler-gap: pending VIP-7
const timeout = new Cause.TimeoutError()
const matched = match (timeout) {
  when Cause.TimeoutError(let e): e;
  default: undefined;
}
Effect
import { Cause } from "effect"
 
const timeout = new Cause.TimeoutError()
const matched = Cause.isTimeoutError(timeout) ? timeout : undefined
if (matched !== timeout) throw new Error("extractor changed")
// A matching _tag alone does not satisfy the retained module guard.
if (Cause.isTimeoutError({ _tag: "TimeoutError" })) throw new Error("tag-only match")
VibeLang
// compiler-gap: pending VIP-7
const isText = (x: unknown): x is string => typeof x === "string"
const textMatch = match ("ok" as unknown) { when isText: true; default: false; }
Effect
// The pinned matcher call supplies the hint; a predicate may ignore it.
const isText = (x: unknown, _hint?: { matchType: "boolean" }): x is string => typeof x === "string"
const subject: unknown = "ok"
const textMatch = isText(subject, { matchType: "boolean" }) ? true : false
if (!textMatch) throw new Error("predicate match lost")

A prototype change exposes the ordinary-class divergence (the shared fixture is foreign.ts):

VibeLang
// compiler-gap: pending VIP-7
import { Foreign, real, fake } from "./foreign.ts"
effect function classify(x: unknown) {
  return match (x) { when Foreign: "yes"; default: "no"; }
}
// classify(real) succeeds with "no"; classify(fake) succeeds with "yes".
Effect
import { Effect } from "effect"
 
// foreign.ts: untouched TypeScript objects, without construction instrumentation.
export class Foreign {}
export const real = new Foreign()
Object.setPrototypeOf(real, null)
export const fake: unknown = Object.create(Foreign.prototype)
 
function classify(x: unknown) {
  return Effect.gen(function* () { return x instanceof Foreign ? "yes" : "no" })
}
if (Effect.runSync(classify(real)) !== "no") throw new Error("prototype change ignored")
if (Effect.runSync(classify(fake)) !== "yes") throw new Error("prototype match lost")
VibeLang
// compiler-gap: pending VIP-7
const arrayMatch = match ([]) { when Array: true; default: false; }
const errorMatch = match (new TypeError("bad")) { when TypeError: true; default: false; }
Effect
import { Effect } from "effect"
 
const arrayMatch = Array.isArray([])
const errorMatch = new TypeError("bad") instanceof TypeError
if (!Effect.runSync(Effect.succeed(arrayMatch && errorMatch))) throw new Error("built-in match lost")
VibeLang
// compiler-gap: pending VIP-22
effect function binding() {
  let x = 0
  const read = () => x
  uninterruptible { x = 1 }
  return read()                     // 1: the block assigns the function's x
}
Effect
import { Effect } from "effect"
 
function binding() {
  return Effect.gen(function* () {
    let x = 0
    const read = () => x
    yield* Effect.uninterruptibleMask(($restore) => Effect.gen(function* () { x = 1 }))
    return read()
  })
}
if (Effect.runSync(binding()) !== 1) throw new Error("binding changed")
VibeLang
// compiler-gap: pending VIP-7
class Point { constructor(readonly x: number) {} }
const point: unknown = new Point(7)
// when Point(let x): is EK0704; use the class test and object pattern:
const coordinate = match (point) { when Point and { x: let x }: x; default: 0; }
Effect
class Point { constructor(readonly x: number) {} }
const point: unknown = new Point(7)
const coordinate = point instanceof Point && "x" in point ? point.x : 0
if (coordinate !== 7) throw new Error("class test lost")

A default's suspensions and jumps stay in the enclosing function:

VibeLang
// compiler-gap: pending VIP-2
effect function defaults() {
  const { x = do { try succeed(7) } } = {}
  const { y = do { return x } } = {}
  return 0
}
Effect
import { Effect } from "effect"
 
function defaults() {
  return Effect.gen(function* () {
    const { x = yield* Effect.succeed(7) } = {} as { x?: number }
    const { y } = {} as { y?: number }
    if (y === undefined) return x   // the source's default returns from the function
    return 0
  })
}
if (Effect.runSync(defaults()) !== 7) throw new Error("default left its activation")
VibeLang
// compiler-gap: pending VIP-2
effect function eager(x = do { return null }) { return 1 }
// eager() is an Effect that succeeds with null, built when eager() is called
Effect
import { Effect } from "effect"
 
let built = 0
function eager(x: unknown = undefined): Effect.Effect<number | null> {
  if (x === undefined) { built++; return Effect.succeed(null) }   // runs at the call, before the generator
  return Effect.gen(function* () { return 1 })
}
const e = eager()
if (built !== 1) throw new Error("parameter default became cold")
if (Effect.runSync(e) !== null) throw new Error("default return lost")
if (Effect.runSync(eager(0)) !== 1) throw new Error("body changed")

A one-element producer buffer bounds queued elements, not concurrent emitter work. The producer bound of two elements applies to one sequential emitter. Each concurrent item can evaluate its value before waiting in Queue.offer; an unbounded loop has no constant global lead bound. This Effect TypeScript witness succeeds with { started: 8, consumed: 1 }, so seven values have been evaluated ahead of the consumer:

Effect
// Concurrent producer witness: VIP-19, VIP-21, VIP-31
import { Deferred, Effect, Exit, Queue, Stream } from "effect"
 
const inspect = Effect.gen(function* () {
  const started: number[] = []
  const ready = yield* Deferred.make<void>()
  const stream = Stream.callback<number>(queue => Effect.gen(function* () {
    yield* Effect.forEach([1, 2, 3, 4, 5, 6, 7, 8], n => Effect.gen(function* () {
      started.push(n)
      if (started.length === 8) yield* Deferred.succeed(ready, undefined)
      yield* Queue.offer(queue, n)
    }), { concurrency: "unbounded", discard: true })
  }).pipe(Effect.andThen(Queue.end(queue))), { bufferSize: 1, strategy: "suspend" })
  let consumed = 0
  yield* Stream.runForEachWhile(stream, () => Effect.gen(function* () {
    consumed++
    yield* Deferred.await(ready)
    return false
  }))
  return { started: started.length, consumed }
})
const outcome = await Effect.runPromiseExit(Effect.timeout(inspect, "2 seconds"))
if (!Exit.isSuccess(outcome)) throw new Error("concurrent producer stalled")
if (outcome.value.started !== 8 || outcome.value.consumed !== 1) {
  throw new Error("concurrent producer lead changed")
}

using keeps TypeScript’s meaning (VIP-8, denied). It disposes when JavaScript control leaves its scope, including by return or a JavaScript throw; on a success Exit, the disposer runs as in TypeScript. A non-success Exit of an Effect run by try abandons the generator without unwinding that scope. Use Effect’s scoped, acquireRelease, acquireDisposable or addFinalizer for cleanup on every Effect exit. using in effect code gets a warning (EK0801), with a quick fix to acquireDisposable inside scoped; the warning does not change disposal semantics.

VibeLang
// compiler-gap: pending VIP-34
// openFile is the plain helper in the shared TypeScript fixture below.
import { openFile } from "./resource.ts"
effect function unsafe(defect = false) {
  using f = openFile()               // warning: use acquireDisposable inside scoped
  if (defect) try die("boom")         // defect: disposer is also skipped
  throw "bad"                        // typed failure: disposer is skipped
}
effect function safe() {
  return try scoped(effect {
    const f = try acquireDisposable(sync(() => openFile()))
    throw "bad"                      // typed failure: scope disposes f
  })
}

Effect TypeScript (the exported fixture is resource.ts):

Effect
import { Effect, Exit } from "effect"
 
export const trace: string[] = []
/** @throws {never} */
export function openFile(): Disposable {
  return { [Symbol.dispose]() { trace.push("dispose") } }
}
function unsafe(defect = false) {
  return Effect.gen(function* () {
    using f = openFile()
    if (defect) yield* Effect.die("boom")
    return yield* Effect.fail("bad")
  })
}
function safe() {
  return Effect.gen(function* () {
    return yield* Effect.scoped(Effect.gen(function* () {
      const f = yield* Effect.acquireDisposable(Effect.sync(() => openFile()))
      return yield* Effect.fail("bad")
    }))
  })
}
// Run separately: both fail with "bad"; unsafe leaves trace empty, safe appends "dispose".
const unsafeExit = Effect.runSyncExit(unsafe())
const unsafeTrace = trace.slice()     // []
trace.length = 0
const safeExit = Effect.runSyncExit(safe())
const safeTrace = trace.slice()       // ["dispose"]
trace.length = 0
const defectExit = Effect.runSyncExit(unsafe(true))
const defectTrace = trace.slice()     // []
if (!Exit.isFailure(unsafeExit) || !Exit.isFailure(safeExit)) throw new Error("expected failures")
if (!Exit.isFailure(defectExit) || defectTrace.length !== 0) throw new Error("defect disposal changed")
if (unsafeTrace.length !== 0 || safeTrace.join() !== "dispose") throw new Error("disposal changed")

Region finalizer observations

VibeLang
// compiler-gap: pending VIP-2, VIP-12
effect function normalObservation() { return do { provide observed { 7 } } }
effect function returnObservation() { provide observed { return 7 } }
Effect
import { Effect, Layer } from "effect"
 
const released: unknown[] = []
const observed = Layer.effectDiscard(Effect.addFinalizer(exit => Effect.sync(() => { released.push(exit) })))
function normalObservation() {
  return Effect.gen(function* () {
    let value = 0
    yield* Effect.provide(Effect.gen(function* () { value = 7 }), observed)
    return value
  })
}
function returnObservation() {
  return Effect.provide(Effect.gen(function* () { return 7 }), observed)
}
// Both outer Exits are Success(7); their layer-finalizer Exits are
// Success(undefined) and Success(7), respectively.

A source field name that resembles a comparison tag remains data. These Effects succeed with distinct records; each computed __proto__ key is an ordinary own property:

Effect
// Source-field witness: VIP-31
import { Effect, Exit } from "effect"
 
const first = Effect.runSyncExit(Effect.succeed({ $fiber: 1, ["__proto__"]: 7 }))
const second = Effect.runSyncExit(Effect.succeed({ $fiber: 2, ["__proto__"]: 8 }))
if (!Exit.isSuccess(first) || !Exit.isSuccess(second)) throw new Error("expected successful records")
if (first.value.$fiber !== 1 || second.value.$fiber !== 2 ||
    first.value["__proto__"] !== 7 || second.value["__proto__"] !== 8 ||
    !Object.hasOwn(first.value, "__proto__") || !Object.hasOwn(second.value, "__proto__")) {
  throw new Error("source fields changed")
}
// Success({ $fiber: 1, ["__proto__"]: 7 }) differs from Success({ $fiber: 2, ["__proto__"]: 8 }).

Unchecked try operands keep ECMA-262's delegated yield evaluation inside the accepted Effect.gen counterpart, without a runtime Effect guard (try). A noniterable defects with its native TypeError; an unchecked iterable can complete normally. Native iterator getters, calls, results and errors keep their reached order and identity. This does not change static operand admission or Effect's generator abandonment.

VibeLang
// compiler-gap: pending VIP-5
effect function unchecked(value: any) { return try value }
// unchecked(5) defects with TypeError; unchecked([]) succeeds with undefined.
Effect
// Unchecked delegation witness: VIP-5, VIP-31
import { Effect, Exit } from "effect"
 
function unchecked(value: any): Effect.Effect<unknown> {
  return Effect.gen(function* (): Generator<Effect.Effect<unknown>, unknown, unknown> { return yield* value })
}
for (const value of [5, undefined, Promise.resolve(5)]) {
  const result = Effect.runSyncExit(unchecked(value))
  if (!Exit.isFailure(result) || result.cause.reasons.length !== 1) throw new Error("expected one defect")
  const reason = result.cause.reasons[0]
  if (reason._tag !== "Die" || !(reason.defect instanceof TypeError)) throw new Error("native delegation error lost")
}
for (const [value, expected] of [
  [[], undefined],
  [(function* () { return 7 })(), 7],
  [(function* () { return yield* Effect.succeed(8) })(), 8]
]) {
  const result = Effect.runSyncExit(unchecked(value))
  if (!Exit.isSuccess(result) || result.value !== expected) throw new Error("unchecked iterable completion changed")
}
if (await (5 as any) !== 5) throw new Error("native await identity changed")

The for concurrent iterable's lifecycle is pinned Effect.forEach behavior (Running Effects), not native for…of backpressure or closing. This finite witness records pulling before explicitly closing its remaining test iterator. The endless-input consequence follows from synchronous materialization; it does not require running an endless probe.

VibeLang
// compiler-gap: pending VIP-19
effect function visit(items: Iterable<number>) {
  for concurrent(2) (const item of items) {
    if (item === 1) throw "bad"
  }
}
// A finite non-array input is fully pulled before item Effects start; the Exit fails with "bad".
Effect
// Iterable lifecycle witness: VIP-19, VIP-31, VIP-32
import { Effect, Exit } from "effect"
 
function observe(concurrency: number | "unbounded") {
  const trace: string[] = []
  function* items() {
    try {
      for (let item = 0; item < 5; item++) { trace.push("pull:" + item); yield item }
    } finally { trace.push("close") }
  }
  const iterator = items()
  const result = Effect.runSyncExit(Effect.forEach(iterator, item => Effect.gen(function* () {
    trace.push("item:" + item)
    if (item === 1) return yield* Effect.fail("bad")
  }), { concurrency, discard: true }))
  const observed = trace.slice()
  iterator.return() // test-owned cleanup, after recording the native traversal
  if (!Exit.isFailure(result) || result.cause.reasons[0]?._tag !== "Fail" ||
      result.cause.reasons[0].error !== "bad") throw new Error("item failure changed")
  return observed
}
for (const limit of [0, -1, 0.5, 1]) {
  if (observe(limit).join() !== "pull:0,item:0,pull:1,item:1") throw new Error("incremental pulling or closing changed")
}
for (const limit of [2, 2.5, "unbounded"] as const) {
  const trace = observe(limit)
  if (trace.slice(0, 6).join() !== "pull:0,pull:1,pull:2,pull:3,pull:4,close") throw new Error("parallel materialization changed")
  if (trace[6] !== "item:0") throw new Error("item started before materialization")
}
let iteratorCalls = 0
const array = [1, 2]
Object.defineProperty(array, Symbol.iterator, { value: function* () { iteratorCalls++; yield 99 } })
function visit(concurrency: number) {
  const seen: number[] = []
  Effect.runSync(Effect.forEach(array, item => Effect.sync(() => { seen.push(item) }), { concurrency, discard: true }))
  return seen.join()
}
if (visit(1) !== "99" || iteratorCalls !== 1) throw new Error("sequential array iterator changed")
iteratorCalls = 0
if (visit(2) !== "1,2" || iteratorCalls !== 0) throw new Error("parallel array reuse changed")

A plain fork in a for try item belongs to the current consuming fiber (Running Effects), not to the item. This witness waits for the child to start, lets the item and loop finish, then releases and joins it from the continuing owner. The gates establish this trace without promising scheduling order for unrelated fibers.

VibeLang
// compiler-gap: pending VIP-20
effect function consume(items: Stream.Stream<number>) {
  for try (const item of items) { fork work(item) }
  try parentWork() // plain children may still run while this consuming fiber continues
}
Effect
// Stream consuming-fiber witness: VIP-17, VIP-20, VIP-31
import { Deferred, Effect, Exit, Fiber, Stream } from "effect"
 
const trace: string[] = []
const result = await Effect.runPromiseExit(Effect.gen(function* () {
  const started = yield* Deferred.make<void>()
  const release = yield* Deferred.make<void>()
  const children: Array<Fiber.Fiber<void>> = []
  yield* Stream.runForEachWhile(Stream.make(1), () => Effect.gen(function* () {
    children.push(yield* Effect.forkChild(Effect.gen(function* () {
      yield* Deferred.succeed(started, undefined)
      yield* Deferred.await(release)
      trace.push("child")
    })))
    yield* Deferred.await(started)
    trace.push("item")
    return true
  }))
  trace.push("loop")
  yield* Deferred.succeed(release, undefined)
  yield* Fiber.join(children[0])
  trace.push("joined")
}))
if (!Exit.isSuccess(result) || trace.join() !== "item,loop,child,joined") throw new Error("consuming-fiber ownership changed")

The producer callback supplies Scope for its body, so its inferred Stream environment excludes that Scope and keeps other services (Producers). This witness's body needs Scope.Scope, while each consumption succeeds with [1] without an external Scope. Construction is cold; two consumptions start two bodies and close two callback scopes. Values of type Scope.Scope and explicit annotation checks are unchanged, and the witness adds no ordering promise for unrelated fibers.

Effect
// Producer Scope witness: VIP-21, VIP-31, VIP-32
import { Effect, Exit, Queue, Scope, Stream } from "effect"
 
let starts = 0, closes = 0
const body: Effect.Effect<void, never, Scope.Scope> = Effect.gen(function* () {
  starts++
  yield* Effect.addFinalizer(() => Effect.sync(() => { closes++ }))
  yield* Effect.forkScoped(Effect.never)
})
const values: Stream.Stream<number> = Stream.callback<number>(queue => body.pipe(
  Effect.andThen(Queue.offer(queue, 1)),
  Effect.andThen(Queue.end(queue))
), { bufferSize: 1, strategy: "suspend" })
if (starts !== 0 || closes !== 0) throw new Error("producer started at construction")
for (let run = 1; run <= 2; run++) {
  const result = await Effect.runPromiseExit(Stream.runCollect(values))
  if (!Exit.isSuccess(result) || result.value.join() !== "1") throw new Error("producer Exit changed")
  if (starts !== run || closes !== run) throw new Error("callback Scope was not fresh or finalized")
}