VibeLang spec review

Four independent reviews of the language spec, merged 2026-10-27. Codes: F Fable, O Opus, S Sol, A Astra.

Verdict

All four reviewers find a sound runtime machine under call rules that let types, annotations, overload order and spelling decide when code runs, and all four want only syntax to delay a call.

ReviewerGradeReason
SolDCall design rates F against the principle; a handler type crash, a name capture and snapshot exports reproduce.
OpusDAbout 30 call semantics on eight criteria; contradicts itself on evaluation order and cleanup.
AstraDTypes and spelling pick evaluation strategy; an optional service brand lets a missing service type-check.
FableCPart III is first-rate; the compiler passed six probes the bug list predicted it would fail.

Fable's falsifiable claim: at least half of the ~50 audit bugs sit in the context-dependent rows below. Sol and Astra: ch 13 already forbids reordering or repeating operands, so those bugs break today's spec.

Call semantics

Principle: few call semantics, dispatched only on call-site syntax. Fable counts 17 behaviors (11 context-dependent), Opus 30 (4 on syntax), Sol 29, Astra 27.

syntax the principle. callee type Kotlin suspend style. amber the call's context. red spelling or a run-time check.

Behavior todayExampleDecided byFlagged
Run on this fiber; control operationsfindUser(id)callee type instantiated, Effect itselfF O S A
Return as dataExit, Config, mixed unionscallee type subtype or unionF O S A
Plain direct calldouble(4)body purityannotationF O S A
Call an Effect valuep()callee typeO S A
Factory(): Effect<A> => { …; return () => s }annotationassignabilityF O S A
Return boundaryreturn Exit.succeed(n)assignabilityannotationF O S
By-name argumentretry(f(x), p), [a(), b()]expected typeoverload orderF O S A
Held initializerconst p: X!! = f()annotationF O S A
Pipe topicf(id) |> retry(%, p)expected typeF O S A
Lambda formids.map((id) => …)overload orderbody purityrun-time valueF O S A
Callback contractinline type vs aliasspellingF O S A
Service and data methodsUsers.find(id), stream.take(3)callee typespelling same-named export, selfF O S A
Trusted foreign callJSON.parse(t)provenanceannotationF O S A
Hatches, keyword forms, block paramssync(f), fork x, withSpan("x") { … }syntaxrun-time value aliased hatchF O S A
Generators, using, TypeScript callersfunction*annotationcallee typeO S A
Runner and entryrunSync(…)spellingF O S A
Projector (planned)$isRun(x)run-time valueO

Proposed designs

FableOpusSolAstra
Semantics3: Run, Quote, Escape5: Run, Plain, Delay, Hatch, Continuation1 strict call, plus thunks, hatches, new2: Computational, Native
f(x) runs whenDeclared return type is EffectDeclared return type is the Effect interface, not a subtype, union or type parameterAlways, in a computation; foreign APIs adapted at importAlways, in a computational region; foreign via adapter
Delay() =>, keyword forms, blocksSame() =>defer { … }
Effect value, serviceNot addressed; S.m(a)p(); S.m(a)run(p); use(S).m(a)run p; (use S).m(a)
FallbackNoneKeep by-name; evaluate arguments onceNoneA full elaboration calculus

Merged recommendation: three semantics

SemanticsSyntaxDispatch rule
Runf(a), S.m(a), p()Callee, then arguments, left to right, once. Runs when the callee's declared return type is Effect itself, or p is an Effect. F O
Delay() => e, block param, fork, await.*, provide, masksOnly these build a program. F O S A
Escapesync, try, callback, module { }Resolved prelude symbol; the first argument is TypeScript. F O S A

Services and data methods desugar by symbol; trust is a static check F O. Decision 1 weighs this against explicit runs.

What users feel under the merged design

TodayAfterExisting code
retry(fetchUser(id), p)retry(() => fetchUser(id), p)Compile error, quick fix
Effect.all([a(), b()])await.all [a(), b()]Compile error
const p: X!! = make()const p = () => make()Compile error
release () => cleanupProgram() => cleanupProgram()Compile error once EK4802 is one
Effect.withErrorReporting(work())Same text; work() runs firstSilent; Opus: one-release migration diagnostic

Cost: thunks were 2.6% of tokens in D147's census F O; the return-boundary change touches four returns in 2 of 468 acceptance cases F. Silent only at parameters typed unknown, any, A | Effect or an unfixed T O.

Decisions for Will

1. When does a call run?

ch 13 lines 118 and 217; D24, D44, D56, D147, D257, D262, D269, D270

Vladimir Kalnitsky, the outside compiler engineer who also contributes fixes, calls ch 13's call rule "cursed": it promises JavaScript's eager, once-only, left-to-right arguments, then passes an argument in an Effect-typed parameter by name, re-run on each run. His principles: (a) no implicit lifting between Effect and plain JavaScript; (b) types may reject code, never change what it does.

// A. Today: the callee's type runs a call; the parameter's type delays it
const user = fetchUser(42)              // runs
Console.log(user.name)                  // runs
take(job())                             // run (T = number) or delay (T = Effect<number>)? each decides the other
takeEffect(job())                       // delayed by name
acceptEither(job())                     // runs: the parameter is not Effect alone
retry(capture(input), policy)           // reads input on every attempt

// B. Kotlin suspend: the callee's declared type runs a call; only syntax delays
const user = fetchUser(42)              // runs
Console.log(user.name)
take(job())                             // runs: T = number
takeEffect(() => job())                 // the delay is written
acceptEither(job())                     // runs
retry(() => capture(input), policy)     // per-attempt read is visible

// C. Every call returns an Effect; running is written
const user = run fetchUser(42)
run Console.log(user.name)
take(job())                             // passes the Effect: T = Effect<number>
takeEffect(job())                       // passes the Effect: T = number
acceptEither(job())                     // passes the Effect
run retry(capture(input), policy)       // reads input once; retries the Effect
A todayB suspendB′ regionsC explicit run
(a) No implicit liftingNoNoYesYes
(b) Types never change codegenNoOne rule: the callee's declared type picks the runYesYes
Call-site syntax onlyNoDelay yes, run noYes, by regionYes
fetchUser(42) is a UserYesYesYesNo
README example, 116 tokens116+0Not measured+1 marker
ReviewersNoneFable, OpusSol, AstraNone

B′, Sol's and Astra's design: calls inside a computation run; foreign functions need declared adapters; raw Effects need run; services need use.

Do the findings disappear?

FindingUnder BUnder C
By-name order and count: trace=b,a,run, i=3, capture(input)GoneGone
Annotations run or hold: held const, factory, return boundaryGoneGone
Overload order picks evaluation (D44, D56, D257)Gone for arguments; lambdas need one formGone
Parameter unions and generic TGoneGone
Return-type unions and subtypes: Sol's Exit probe prints [Success(42), 42]StaysGone
A forgotten run silently drops workCannot happenNew: a discarded Effect must be an error

C's spelling and cost

provide UsersLive {                       // README example under C
  const [name, fallback] = await.all [Users.find("42"), Users.find("7") catch "Guest"]
  run Console.log(name, fallback)         // the one added marker
}
run Console.log(run FileSystem.readFileString(path))   // direct style: +2 markers

Spellings: run f(x), one token, Sol's and Astra's word. yield* f(x), Effect.gen's own, two tokens. await f(x), one token, but reverses D226, which keeps await for promises. Hand counts. C needs no thunks: retry(fetchUser(42), policy) passes the Effect. No census of running calls exists, so C's full cost is unmeasured.

Prior art
C: JavaScript await, Rust .await and its unused-future warning, yield* in Effect.gen. B: Kotlin suspend. Koka and Unison run effectful calls implicitly and track effects in types.
Ambiguity
"All functions return Effect" holds today at the TypeScript boundary: ch 13 types a program as () => A inside VibeLang and Effect<A, E, R> at the boundary, plain-annotated functions and factories excepted O A. Inside .vibe too, it is C, which contradicts the README's "Why VibeLang" pitch: "a call returns its value". No reviewer proposes C.

Recommend B. It removes every argument-side finding, keeps direct style and costs 2.6% of tokens. It breaks principle (b) at one point, as Kotlin's suspend does, and Opus calls that break unavoidable for direct style. If you meant "all functions return Effect" inside .vibe, pick C, spell it run, and make a discarded Effect an error.

2. Details of B

Skip if decision 1 picks C. D7, D24, D241, D269, D271

Rule text, from Opus: "A type annotation may reject a program; it never changes when, whether, or how often an expression is evaluated or run." Open choices: delay as () => F O S or defer { } A; factories marked O S A or plain F; a returned Effect runs through p() O S A or implicitly F; generator kind by syntax O A.

Recommend () =>, a factory marker, explicit p() and EK4802 as an error F O.

3. Names by spelling or by resolved symbol

D135, D148, D152, D208, D295, D311

function runSync(x: number): number { return x }
runSync(1)            // top level: main never runs, exit 0     F O S
x is Number           // a local Number predicate is ignored: false
x is StringMatcher    // same body, another name: true          A
Option B
Resolved symbols for runners, matchers, hatches and exports; data methods from a versioned table, not a self parameter. Prior art: Scheme hygiene, ECMAScript ResolveBinding.
Breaks
Silent, wrong programs only: main starts; a local fork stops capturing fork x.

Recommend B. All four back it.

4. finally in plain functions

ch 15 against ch 17

try { o.a.b } finally { o.c.d }
// plain path: 1 reason ('d'); framed path: 2 reasons ('b', 'd')     Opus's probe
Options
A: keep JavaScript's replacement. B: combine causes as the frame path does O S A. C: keep finally off the plain path O. Prior art: Java 7 addSuppressed. Fable's probe kept both failures.
Breaks
Silent: callers also see the body's failure.

Recommend B, plus a rule that every optimization yields the frame path's Exit O S A.

5. using and resource release

D25, D94

using r = openDisposable()  // its acquireRelease already disposes: ["dispose","dispose"]   Sol
using h = Effect.acquireRelease(() => "h", () => cleanupProgram)
                            // the release never runs; warning EK4802 only               Fable
Option B
An Effect acquisition owns its release and using adds none; a plain Disposable registers one; an unregistered one goes through an adapter that transfers ownership atomically S A. Prior art: TC39 Explicit Resource Management, C# using.
Breaks
Silent: one disposal instead of two.

Recommend B, with EK4802 an error F O. D25 and D94 are still open.

6. Inline module exports

D283, D303

// import { count, bump } from an inline module
bump()
count     // today 0, a snapshot; ECMAScript gives 1          S A
Options
A: snapshots, documented in ch 10 F. B: live bindings, source initialization order, case-sensitive names S A O. Prior art: ECMAScript module records.
Breaks
Silent: importers see current values.

Recommend B.

7. Open error families and exhaustive match

D209, D304; queue item decision:vibelang-sealed-error-families

error Base { _tag: string; code: string }
error Sub extends Base {}
match (e) { when Sub: "sub"; }   // compiles; a Base dies: "No match arm matched Base"
Options
1: seal a family to its file (the queue item; Scala 3, Kotlin sealed). 2: match without default over a non-finite subject is EK4701 F O S A; Rust #[non_exhaustive]. 3: document the gap.
New fact
Sealing alone does not fix it: Sol's probe declares every subclass in one file and still compiles a match that omits one; Opus and Astra agree. Fable's fails on a concrete Base.

Recommend 1, with a checker that counts every subclass and a concrete base, and 2 for every other subject. Compile errors.

8. Handler answer types

D97, D50, EK2119

// clause: const n = resume(); return n.toFixed(1)
// compiles with warning EK2119; runs: TypeError: n.toFixed is not a function     Sol
Options
A: keep the warning. B: one answer type per handler from D50's fixpoint, written when inference cycles; EK2119 an error S A F. Prior art: Eff's handler typing, OCaml 5.

Recommend B.

9. Callback contracts through type aliases

ch 16, D12

function inline(f: (a: number) => number) {}   // accepts an effectful lambda
type Fn = (a: number) => number
function viaAlias(f: Fn) {}                     // EK4301 on the same lambda     O
Options
B: alias or not, an uncontracted function parameter is effect-polymorphic; plain-only is written => A!! O or tagged F. C: explicit effect rows, as in Koka S A.

Recommend B. Aliased callbacks gain channels.

10. Untrusted calls inside plain functions

D101, D263, D282

function viaPlain(n: number): number { return risky(n) }   // compiles
function viaEffect(n) { sleep(…); return risky(n) }         // EK4401     F
Option B
EK4401 in every function; plain code reaches untrusted TypeScript only through a hatch or module F; a call's failures depend only on the callee O.

Recommend B. All four back it; compile errors.

Smaller decisions

#TodayOptionsRecommend
11A structural type relabels service Alpha as Beta, then "Service not found" D45Optional brand, or a nominal identifierNominal A only
12An aliased sync passes an AbortSignal the direct call does not D232Same behavior A, or aliasing is an error O AAliasing is an error
13A!E infers R on a function with a body, never elsewhere D309Keep, or never everywhere F Onever everywhere
14A planned renderer picks run or data from a run-time TypeId D248Drop it, or renderers match the type-directed compiler or rejectOpus's rule O only
15A returned block-owned fiber is already interrupted; a throw in a parameter default never runs F OKeep, or compile errorsCompile errors

Spec fixes that change no correct program

FindingWhereRuleBy
No hygiene rule; Sol's probe reads a source $match1 = 99 as 1.ch 26Introduced bindings are fresh; intrinsics come through an import no source name shadows; a conformance case shadows every generated name.F O S A
No lexical-context rule; today's probes pass.ch 26Desugaring preserves this, arguments, new.target, super and self-binding, or is an error.F O S A
Evaluation order is four sentences; "run" means call and execute.ch 13Numbered rules with two verbs, evaluate and run. Service lookup before arguments, as emitted A O; Fable's E3 says after.F O S A
No equivalence rule for optimizations and safe fixes.ch 15, 28, 30Same Exit, reason order, side-effect count, bindings and lifetimes as the frame lowering.O S A
"Cleanup failure is never dropped" omits timeout and race.ch 17State D256 beside the rule.S A
D10 stated three ways; two plain-call ABIs (D65, D268).ch 15, 27Plain code recurses on the host stack, frames on the heap; keep D268's property only.F O
.d.ts carries less meaning than source; trust assumptions unstated.ch 10, 27Declaration consumers behave as source consumers; name the trusted base.S A
No normative source: "every rule is decided" with D25, D94 open; ch 29 repeats D13; D257, D270, D303 and others never reach the chapters.Intro, ch 29Ch 13–17 are normative; a decision updates every rule in one revision.F O S A
Contradictions: TC39 table against ch 3 and 11; ch 3 against ch 17 on Effect.catch; ch 4 against D222; three entry stories; "every function is an Effect".Intro, ch 1–4, 12, App BReconcile to the normative rule.F O S A
Implementation limits stated as rules: G19, D210, D294, D307.ch 29–30Label them restrictions.A

Agreement

All four agree

  • Part III's machine is the strongest part.
  • Types, annotations and overload order must not decide evaluation; only syntax delays.
  • Direct style stays; none proposes explicit runs.
  • Names resolve by symbol, not spelling.
  • Open-family match must not die at run time; all reproduced it.
  • using releases once; aliases mean what inline types mean; plain code cannot bypass trust.
  • The spec needs hygiene, lexical-context and evaluation rules, and one normative source.

They disagree

  • Grade: Fable C, the rest D.
  • Design: B F O or B′ S A.
  • Delay: () => F O S or defer A. Returned Effects: implicit F or explicit O S A.
  • Service lookup after arguments F or before A O.
  • "Language change" labels for the spelling, finally and module fixes.
  • Single-reviewer: service brand, hatch alias A; emit switch O; handler crash, $match1 capture S.

Full reports