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.
| Reviewer | Grade | Reason |
|---|---|---|
| Sol | D | Call design rates F against the principle; a handler type crash, a name capture and snapshot exports reproduce. |
| Opus | D | About 30 call semantics on eight criteria; contradicts itself on evaluation order and cleanup. |
| Astra | D | Types and spelling pick evaluation strategy; an optional service brand lets a missing service type-check. |
| Fable | C | Part 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 today | Example | Decided by | Flagged |
|---|---|---|---|
| Run on this fiber; control operations | findUser(id) | callee type instantiated, Effect itself | F O S A |
| Return as data | Exit, Config, mixed unions | callee type subtype or union | F O S A |
| Plain direct call | double(4) | body purityannotation | F O S A |
| Call an Effect value | p() | callee type | O S A |
| Factory | (): Effect<A> => { …; return () => s } | annotationassignability | F O S A |
| Return boundary | return Exit.succeed(n) | assignabilityannotation | F O S |
| By-name argument | retry(f(x), p), [a(), b()] | expected typeoverload order | F O S A |
| Held initializer | const p: X!! = f() | annotation | F O S A |
| Pipe topic | f(id) |> retry(%, p) | expected type | F O S A |
| Lambda form | ids.map((id) => …) | overload orderbody purityrun-time value | F O S A |
| Callback contract | inline type vs alias | spelling | F O S A |
| Service and data methods | Users.find(id), stream.take(3) | callee typespelling same-named export, self | F O S A |
| Trusted foreign call | JSON.parse(t) | provenanceannotation | F O S A |
| Hatches, keyword forms, block params | sync(f), fork x, withSpan("x") { … } | syntaxrun-time value aliased hatch | F O S A |
Generators, using, TypeScript callers | function* | annotationcallee type | O S A |
| Runner and entry | runSync(…) | spelling | F O S A |
| Projector (planned) | $isRun(x) | run-time value | O |
Proposed designs
| Fable | Opus | Sol | Astra | |
|---|---|---|---|---|
| Semantics | 3: Run, Quote, Escape | 5: Run, Plain, Delay, Hatch, Continuation | 1 strict call, plus thunks, hatches, new | 2: Computational, Native |
f(x) runs when | Declared return type is Effect | Declared return type is the Effect interface, not a subtype, union or type parameter | Always, in a computation; foreign APIs adapted at import | Always, in a computational region; foreign via adapter |
| Delay | () =>, keyword forms, blocks | Same | () => | defer { … } |
| Effect value, service | Not addressed; S.m(a) | p(); S.m(a) | run(p); use(S).m(a) | run p; (use S).m(a) |
| Fallback | None | Keep by-name; evaluate arguments once | None | A full elaboration calculus |
Merged recommendation: three semantics
| Semantics | Syntax | Dispatch rule |
|---|---|---|
| Run | f(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, masks | Only these build a program. F O S A |
| Escape | sync, 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
| Today | After | Existing 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 first | Silent; 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 today | B suspend | B′ regions | C explicit run | |
|---|---|---|---|---|
| (a) No implicit lifting | No | No | Yes | Yes |
| (b) Types never change codegen | No | One rule: the callee's declared type picks the run | Yes | Yes |
| Call-site syntax only | No | Delay yes, run no | Yes, by region | Yes |
fetchUser(42) is a User | Yes | Yes | Yes | No |
| README example, 116 tokens | 116 | +0 | Not measured | +1 marker |
| Reviewers | None | Fable, Opus | Sol, Astra | None |
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?
| Finding | Under B | Under C |
|---|---|---|
By-name order and count: trace=b,a,run, i=3, capture(input) | Gone | Gone |
| Annotations run or hold: held const, factory, return boundary | Gone | Gone |
| Overload order picks evaluation (D44, D56, D257) | Gone for arguments; lambdas need one form | Gone |
Parameter unions and generic T | Gone | Gone |
Return-type unions and subtypes: Sol's Exit probe prints [Success(42), 42] | Stays | Gone |
| A forgotten run silently drops work | Cannot happen | New: 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.awaitand its unused-future warning,yield*inEffect.gen. B: Kotlinsuspend. 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
() => Ainside VibeLang andEffect<A, E, R>at the boundary, plain-annotated functions and factories excepted O A. Inside.vibetoo, 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
selfparameter. Prior art: Scheme hygiene, ECMAScript ResolveBinding. - Breaks
- Silent, wrong programs only:
mainstarts; a localforkstops capturingfork 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
finallyoff the plain path O. Prior art: Java 7addSuppressed. 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
usingadds 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:matchwithoutdefaultover 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
matchthat omits one; Opus and Astra agree. Fable's fails on a concreteBase.
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
moduleF; a call's failures depend only on the callee O.
Recommend B. All four back it; compile errors.
Smaller decisions
| # | Today | Options | Recommend |
|---|---|---|---|
| 11 | A structural type relabels service Alpha as Beta, then "Service not found" D45 | Optional brand, or a nominal identifier | Nominal A only |
| 12 | An aliased sync passes an AbortSignal the direct call does not D232 | Same behavior A, or aliasing is an error O A | Aliasing is an error |
| 13 | A!E infers R on a function with a body, never elsewhere D309 | Keep, or never everywhere F O | never everywhere |
| 14 | A planned renderer picks run or data from a run-time TypeId D248 | Drop it, or renderers match the type-directed compiler or reject | Opus's rule O only |
| 15 | A returned block-owned fiber is already interrupted; a throw in a parameter default never runs F O | Keep, or compile errors | Compile errors |
Spec fixes that change no correct program
| Finding | Where | Rule | By |
|---|---|---|---|
No hygiene rule; Sol's probe reads a source $match1 = 99 as 1. | ch 26 | Introduced 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 26 | Desugaring 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 13 | Numbered 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, 30 | Same 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 17 | State D256 beside the rule. | S A |
| D10 stated three ways; two plain-call ABIs (D65, D268). | ch 15, 27 | Plain 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, 27 | Declaration 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 29 | Ch 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 B | Reconcile to the normative rule. | F O S A |
| Implementation limits stated as rules: G19, D210, D294, D307. | ch 29–30 | Label 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
matchmust not die at run time; all reproduced it. usingreleases 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 ordeferA. Returned Effects: implicit F or explicit O S A. - Service lookup after arguments F or before A O.
- "Language change" labels for the spelling,
finallyand module fixes. - Single-reviewer: service brand, hatch alias A; emit switch O; handler crash,
$match1capture S.
What the spec gets right
- The runtime machine (ch 20–23): transition rules, atomicity, evidence tags. F O S A
- One-shot continuations with ownership; abandonment runs finalizers. F O S A
- Cause algebra matches Effect 4.0.6; EK2006 keeps control from leaving
finally. F O S A - Snapshot Context; DI separates build-time from call-time requirements. F O S A
- The compiler passes the order, lexical-context and hoisting probes. F O S A