VIP-2: effect { } blocks, and do { } as TC39 defines it
Status: accepted by Will, 2026-10-28.
Decision
- Amended 2026-10-30: call references. Expressions retain ECMAScript's native call-reference and argument evaluation behavior, including receiver binding and non-callable errors. Compiler invocation machinery introduces no observable helper lookup; see Evaluation order.
- Corrected 2026-10-31: the first example logs with
log. It wroteconsole.log("hi"), a host global that VIP-33 bans in every.vibecontext, aneffectblock included.try log("hi")runs Effect'slogglobal (VIP-6) once per run ofp, so the example still shows a cold block that reruns. Loop round 5 ASTRA-3.
const p = effect { try log("hi"); return 1 } // an Effect, not yet run
try p // logs "hi", gives 1
try p // logs "hi" again: each run reruns the block
try retry(effect { try save(try fetchUser(id)) }, policy)
Layer.effect(Users, effect { … })
const x = do { const a = 1; a + 1 } // TC39 do expression: runs now, x = 2-
effect { … }is an expression. It builds an Effect from the block without running it, and each time the Effect runs, the block runs again.- It is Effect TS's
Effect.gen(function* () { … }), and the same as(effect () => { … })(). - Inside it,
tryruns Effects, as in aneffectfunction (VIP-5).
- It is Effect TS's
-
effectbefore a function makes a function that returns an Effect (VIP-1).effectbefore a block makes the Effect itself. -
do { … }is exactly TC39's do expression, pinned to a dated snapshot of the proposal (moved deliberately, as VIP-7 does formatch), everywhere, with no exceptions. It runs now and gives its last statement's value, like an IIFE. As TC39 specifies,awaitinsidedobelongs to the enclosing function, and so doestry. -
No other syntax builds a program. A function literal never turns into a program because the parameter it's passed to expects an Effect.
-
Amended 2026-10-29:
dofollows TC39 exactly. VibeLang adds no limits onvar,breakorcontinueinside adoblock (the old EK1019 is deleted). Sent to Will on Telegram 2026-10-29 12:28 under his standing rule 'assume I accept'; no objection by 13:00. -
Amended 2026-10-29: the pinned snapshot.
dois pinned totc39/proposal-do-expressionscommit3ef6d463b8163cbf222846b40d17178cca36e9e1, the head on 2026-10-28 (UTC), vendored intc39-do-expressions/, as pattern matching is pinned. 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 52). -
Amended 2026-10-30: complete the pinned static predicates. For
EndsInIterationOrBareIfOrDeclaration,CaseBlock : { }returnsfalsefor either value ofisLastand anylabelSet, including when the switch is nonfinal. EveryForTryandForConcurrentproduction, with or without a concurrency limit, uses the native iteration rule:isLastequal totruegivestrue; otherwise a body passingIsBreakwith that label set givestrue; otherwise apply the predicate to the body with the samelabelSetandisLast. The loop statement itself hasIsEmptyandIsBreakequal tofalse. Thus nonfinal loop bodies are still visited, and a final added loop is rejected like a final native loop. Ordinary switch completion and loop jump restrictions stay unchanged. The snapshot receipt records the effective reconciliation without changing the vendored draft; Running Effects gives the completion witnesses and their Exits. -
Amended 2026-10-30:
returnin adoin a parameter default. A parameter list whosedocontains areturnlowers by moving every default from that parameter on into the body, so thereturnreturns from the function;lengthis unchanged. The one divergence from TC39 is a default that reads a later parameter in its TDZ. Decided by the agent on 2026-10-30 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 68). -
Amended 2026-10-30: defaults that suspend or jump stay in the enclosing activation. A parameter or destructuring default containing
try,await,yield, or adowhosereturn,breakorcontinueleaves the default, lowers into guarded statements in the enclosing function at the default's place, never into an arrow, keeping the pattern's evaluation order and its other bindings.effect function f() { const { x = do { return 9 } } = {}; return 0 }succeeds with 9. Decided by the agent on 2026-10-30 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 68a). -
Amended 2026-10-30: defaults stay native where they can; expanded ones close iterators exactly. A destructuring default whose suspending
do(ortry/await) lowers to one expression stays native destructuring in the generator:const [x = do { try succeed(7); bad.value }] = xsisconst [x = (yield* Effect.succeed(7), bad.value)] = xs, so ECMAScript's own evaluation order and IteratorClose apply. Only a default with a statement that is not an expression, or areturn,breakorcontinueleaving it, uses the expanded statements, which close the iterator by ECMA-262 IteratorClose:returnis called on the iterator, a pending throw wins over an error from closing, and otherwise a close error propagates and a non-object result is aTypeError. Decided by the agent on 2026-10-30 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 68b). -
Amended 2026-10-30: the spec states destructuring-default behavior, not a lowering. This replaces the lowering recipes of the three amendments above; their behavior stands. In effect code a destructuring pattern behaves exactly as ECMAScript destructuring (ECMA-262 BindingInitialization, GetIterator, IteratorClose): evaluation order, getters, the iterator protocol,
IteratorCloseonly when the native algorithm requires it, with a pending throw winning over a close error, and native error kinds and messages. A default'stryandyieldrun in the enclosing effect function; adodefault'sreturn,breakandcontinueact on the enclosing function or loop. How the compiler achieves it is its choice. The witnesses are normative examples with their Exits:effect function f() { const { x = do { return 9 } } = {}; return 0 }succeeds with 9,effect function g() { const { x = do { try succeed(7) } } = {}; return x }succeeds with 7, andconst [x = do { try succeed(7); bad.value }] = xsdies with"body"whenbad.valuethrows"body"andreturnthrows, and with aTypeErrorwhenbad.valuecompletes normally andreturnreturns1; round 20 adds a replaced globalSymbol(success) and malformed iterator methods, results,nextandreturn(each the nativeTypeError) (Running Effects). Decided by the agent on 2026-10-30 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 68c). -
Amended 2026-10-30: jumps in defaults, and no recipes. A
return,breakorcontinuein a destructuring default leaves the pattern as the same jump leaves afor…ofbody: an array pattern closes its iterator unless it is done, and an error from closing replaces the jump. Areturnin a parameter default stays legal, as the pinned proposal says, and acts asreturnat the top of the body: a plain function returns the value, anasyncfunction fulfills with it, aneffectfunction's call gives an Effect that succeeds with it, and a generator's call gives a generator that finishes with it without starting the body (Will, 2026-10-30: "everything about the do expreession should work liike that spec"). A throw there follows the kind of function, so aneffectfunction's call gives an Effect that dies and never throws. The behavior replaces the moved-default lowering of the amendments above, including its temporal-dead-zone exception: JavaScript has none, so VibeLang has none. Adoormatchinside a larger expression runs where JavaScript evaluates that expression, and how the compiler keeps that is its choice. Decided by the agent on 2026-10-30 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later". -
Amended 2026-10-30: source completion types.
douses the source block's normal completion values andUpdateEmptyflow, including locally consumed jumps. Only a final empty completion becomesundefined; paths leaving outward contribute no result, and no normal path meansnever. Its fresh literals and contextual typing follow ordinary expressions:const x = do { 1 }keeps1,let x = do { 1 }widens tonumber, and annotations/const assertions follow TypeScript. Context reaches result alternatives, while discarded expressions and outward returns retain their own contexts. Generated functions and temporaries never choose source overloads or declaration types. -
Amended 2026-10-31: jumps out of an
effectblock. Abreakorcontinuewhose target is outside itseffect { }block isEK0202. A jump inside the block and the block'sreturnstay allowed. Decided by the agent under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 247).
Clarification: overlapping var diagnostics
VIP-34 requires: "Use an already assigned specific diagnostic for the same rejection; preserve independent native errors." The do early errors use their assigned codes instead of the general legacy-JavaScript EK0107 for the same var: EK0201 for a final declaration, EK0203 for a declaration in a parameter list, and both when both independent early errors apply. A nonfinal var outside a parameter list remains EK0107 under VIP-4's amended ban. This clarifies diagnostic overlap without admitting var anywhere in .vibe code. Running Effects gives the three witnesses.
Amendment: mixed block modifier recovery
The mixed heads effect async { B } and async effect { B } parse for recovery and report EK0105 (VIP-1). Remove async to recover the ordinary cold Effect block. A bare async { B } is not a language form; the recovery adds no accepted syntax or runtime counterpart.
Prior art
- Rust:
async fnis a function, andasync { }is the value. - F#'s
async { }, Kotlin'ssuspend { }, TC39'sasync do { }proposal. - Effect's
Effect.gen. - TC39 do expressions (stage 1).
Will's words
"No no exceptions do should just be shorthand for an iiffe and have exact same spec as tc39 proposal"
"Ok I approve this one"
Updated by VIP-5 (2026-10-28): examples use try to run Effects. Updated by VIP-7: the proposal is pinned to a dated snapshot.
Footnote: old VibeLang
Old VibeLang is a reference only; nothing in it is part of the language unless a VIP adds it. For implementers, this VIP replaces:
- D322's exception, under which a
do { }element of an await operation became a program. - D551's conversion of a function literal at an
Effectparameter. This follows from VIP-1's "syntax decides". - The function-literal conversion of D480, where the parameter's type decided.