Wado

Research: Language Survey — vibe

Survey against the rubric.

mizchi/vibe-lang — 5395 commits / first 2026-02-03 / surveyed at f7cb4d068 2026-08-31

Arbiter: "When they conflict, the order is never be silently wrong > honesty of representation > pleasantness of the surface syntax." — opens the design policy in both README.md and AGENTS.md.

An effect-typed functional language in the Rust / MoonBit / Koka / Verse lineage, compiled to wasm, with a selfhost-only compiler: parser, checker, and codegen are written in vibe and built from a sha256-pinned seed. The original MoonBit host was retired outright.

The arbiter is not a maximized metric but a lexicographic order over three values, and it is carried into operations: issue triage ranks P0 = silently wrong above P1 = crashes.

Examined: README.md, AGENTS.md, docs/vibe.md, roughly a fifth of the 2,646-line cheatsheet, docs/adr.md, docs/spec/decisions.md, docs/spec/stable-surface.md, docs/spec/memory-contract.md, docs/module-system-oracle.md, docs/effect-evidence-passing.md, docs/pl-survey-2026-07.md, eval/lang-review/rubric.md and its latest findings, eval/msr/README.md, formal/README.md, docs/cli-commands.md, scripts/check_cheatsheet_signatures.sh, the lib/ and scripts/ trees by listing and count, and lib/@vibe/builtin/ by reading.

Not examined: docs/concurrency.md and the ADRs behind A5; docs/perceus-reuse.md; the incremental build and result-cache design, which the module oracle touches only at the hash closure; four fifths of the cheatsheet; the Vibe Book.

A. The language

Axis Claim Reality Holds in self-application Unimplemented / Rejected
A1 Canonicity "One concept, one spelling" Mechanized, not asserted: a gate verifies documented signatures against the compiler, another enforces the canonical call form, doctest compiles every vibe block in the docs. A block that must not compile is marked vibe skip and carries a doctest-skip: comment giving the reason, so a "do not write this" example stays in the document without exempting the gate. AGENTS.md marks decided-but-not-landed rules inline and warns not to read them as today's behaviour Yes Landing: structural == in every context, constructor-polymorphic pipeline combinators, Exception over Error at the 1.0 freeze
A2 Type vocabulary Own surface, representations chosen to match wasm and WIT without friction A value is a tagged i64; String is a byte string indexed by byte offset (ADR-0098, "semantics that match what the memory actually is"); what may cross a WIT boundary is decided by nominal rules (ADR-0089) Yes —
A3 Effects Row-polymorphic effect types with handlers; capabilities ride the row fn main with Console, with Exception + Fs, handle { … } with Exception[String] { … }. Deno-style permissions cross Koka-style effects: authority is fixed once in the earliest phase and immutable while running; --allow-* const-folds and dead-code-eliminates ungranted capabilities, and the emitted binary declares the wasm feature level it needs. The handler lowering is replay and it corrupts values — see below Partly: the wasm-gc backend implements no handler at all beyond a Throw stub —
A4 Errors Failure travels in the effect row, not in a return-type wrapper fn safe_div(a: Int, b: Int) -> Int with Exception[String] — the success value flows straight through, and handle discharges the row at one site instead of unwrapping per call. The checked Error policy is the adopted static rule and is formalized in Lean Yes Rejected: ambient Error, kept in the Lean model as a negative witness
A5 Concurrency Structured, shared-nothing TaskGroup plus Send/region checks. Async syntax exists behind --unstable-async. Continuations designed against wasm-gc typed reference lanes, stack switching today, JSPI as an alternate backend Yes Real threads deferred, but the representation is chosen for them
A6 Boundary mechanisms No macros None a user can invoke: no generation command, no plugin, no build hook among the 25 user-facing CLI commands. derive is the only generation and its set is fixed — Eq, Ord, Show, Hash, Default, none of them serialization. Values cross the boundary through a dynamic Json:: API (stringify: (Any) -> String), with no typed mapping either way. The compiler generates WIT from declarations — —
A7 Hidden operations Not claimed either way Perceus reference counting is inserted, monomorphization runs, ungranted capabilities are eliminated. Each is documented in its own file; nothing under docs/ names an inventory of them. The cheatsheet's measured-pitfalls section covers part of the ground from the other side, recording observed behaviour rather than compiler steps — Gap, not a decision

The self-application cross-check

There are two implementations of the semantics here too, and nothing holds them to one answer.

The linear backend is the production default, and the gc backend cannot be reached from the compile CLI at all. So the disagreement between them does not hurt yet — which is also why the bill has gone unpaid. Array::push is in-place and growable on one backend and fixed-size on the other, and the memory contract writes the debt down instead of gating against it. In the other surveyed language, two backends of equal standing produced a contract ledger, a fixture corpus and a three-way oracle.

On the axis that does carry weight, vibe applies the strongest form of this check there is: the compiler is written in the language.

Measure Value
vibe under lib/ 974 files, 360,831 lines
the compiler itself (lib/@vibe/compiler/) 268,463 lines, of which 55,388 are _test.vibe
Rust in the tree (runtime/, bootstrap/) 3 files, 5,562 lines — the wasmtime runner, not the compiler
stdlib files touching raw memory (builtin, core, json, http, fs) 0 of 132

The builtin floor is typed and generic rather than address-based — Array::get: (Array[T], Int) -> T, ArrayBuilder::push, Map::set — and docs/builtin_contract_table.generated.md records each builtin's signature and effect requirement. Library code above it reads as ordinary vibe:

export fn zip[A, B](xs: Array[A], ys: Array[B]) -> Array[(A, B)] { … }

Generics reach the implementation, not just the signature. A lexer, parser, CST, desugarer, type checker, monomorphizer, Perceus pass, two codegen backends, an optimizer, an incremental build layer, a formatter, and a WIT generator are all written this way.

B. The practice

Axis Present Scope
B1 Arbiter Yes A lexicographic tiebreak order rather than a maximized metric, stated at the top of the design policy and encoded in triage priorities
B2 Accept/reject criteria Partial The tiebreak order settles conflicts; what it cannot settle becomes an issue with three-axis triage labels. Not numeric
B3 The "why" axis Refused, with a doctrine docs/adr.md records only what was decided; rationale goes to git log and the issue thread. The same stance as Wado's WEP rule, reached independently
B4 Falsifier Partial, inverted Not "what would retract this" but "proposed must say what would make it accepted" — and a proposed entry that has not moved in a release cycle is deleted as "a decision nobody made"
B5 Rejection record Refused, with a doctrine "Documents rot; delete them." A status banner is "a document admitting it already failed at its job". A superseded row survives only while still cited as the road not taken; otherwise it is folded into the successor and deleted. An ADR whose subject no longer exists in the tree "does not become history by sitting there; it becomes a false statement about the current build"
B6 Sync gate Yes, the widest of the three surveyed 49 check_*.sh gates, many with _test.sh companions, plus check_gate_registry.sh and check_gate_self_tests.sh — gates over the gates. Scope reaches prose-adjacent surfaces: doctest over every vibe block in the docs, cheatsheet signatures against the checker's own builtin table, cited file paths, example typechecking, book link and ordering checks, and translation output parity. The gates are placed where the previous gate is blind — the signature one exists because "doctest only compiles its ```vibe blocks, it is blind to a table", and it also holds one prose paragraph to naming exactly the documented non-builtins, no more and no less
B7 Self-reported violations Yes docs/known-issues.md, plus decided-but-not-landed rules marked inline in AGENTS.md where a reader would otherwise take them for current behaviour
B8 Who decides No rule Not stated

B3 and B5 are the interesting rows, because they are refusals with reasons rather than things nobody got round to.

vibe diagnoses exactly the failure this survey found in Almide — a gate that covers code and is blind to prose — and answers it by deleting the redundant surface instead of building a gate for prose. docs/language-tour/ was removed for precisely that reason. Doctest compile-checked its code blocks and could not see that the prose around them still called String a UTF-16 string, long after ADR-0098 made it a byte string, and still named index.vibei as the package boundary after ADR-0070 replaced it.

docs/spec/stable-surface.md records the stable surface, and is honest about not yet being in force. The freeze takes effect at the 0.1.0 tag, and until then, in its own words, "this document describes the surface being frozen, not a promise already made".

The specification opens by declaring its own authority instead of assuming it. It says which document is canonical for surface syntax, which for the behaviour outside syntax, and which for package boundaries and pinning. It also says that anything marked future, proposal or draft is non-normative.

Then it splits the language a second way: what the standard tutorial teaches, against what is documented but deliberately left out of it. That is a statement about which subset a reader should write from, and it is a different subset from the one that is frozen.

The handler lowering, and what it costs

handle is compiled by replay. The body is wrapped in a wasm loop. A perform unwinds to the handler, recording resolved resume values into a fixed 128 KB region on the way. Then resume(v) appends to that memo and re-runs the body from the top, skipping the performs it has already resolved.

Only a perform's return value gets memoized. Everything else in the body runs again on every resume.

Their own review round measured what that does. A body that performs twice prints its first line three times. A let mut accumulator reads 23 where 11 was expected. The function returns 29 where 17 was expected. The value itself is wrong.

What shipped as the fix was a sentence in the cheatsheet, telling authors to keep a handle body pure until the last perform. No fixture pins the defect. There is also a hard ceiling. The memo region is 128 KB of 8-byte slots, which their own note puts at 16,382 performs per handle — two short of the round figure, and it does not say what the other two slots hold. The constant is duplicated across three files, and shrinking it once corrupted the heap.

ADR-0076 is the plan to replace all of it with Koka's generalized evidence passing. It has not landed: the ADR puts itself at phase 0, design only, no code changes. The design's first move is reuse — vibe already passes trait dictionaries as implicit witness parameters for generic method calls, so the evidence vector is built on that same mechanism instead of a second one.

Two things in the write-up are worth as much as the decision itself.

First, it corrects the premise of the issue that asked for it. The issue assumed there was a tail-resumptive fast path to generalize; that path only ever existed in the retired MoonBit host. So this is introducing a mechanism, not generalizing one.

Second, phase 1 is to pin the existing corruption as a regression fixture before fixing anything, so that "fixed" can later be proved rather than asserted.

The plan then shrank once they investigated. A CPS runtime was scheduled for phase 3 and turned out to be unnecessary: the checker already rejects non-tail and multi-shot resume, which by the Xie and Leijen definition makes every handler tail-resumptive already. So the remaining work is just widening how much the evidence pass covers statically. The document records that the phase evaporated.

Backends, and where wasm-gc actually is

The gc backend is not what its name suggests. It turns on the Wasm-GC feature set, but it does not emit struct.new or array.new for user values. Tuples, structs, constructors and closures all live in guest linear memory behind a bump allocator, so there is nothing for a tracing collector to reclaim.

It is also unreachable from the compile CLI: compile --wasm-gc throws. The only way in is through test and bench environment variables, for cases with no host imports.

The production default is linear memory with Perceus reference counting. Reclamation is eager and deterministic. Cycles leak permanently. The compiler's own self-build is pinned away from RC and back to bump, for performance — RC costs it roughly 1.7× in wall clock and 2.9× in output size.

The two backends do not agree on what the language means. Array::push is in-place and growable on linear, and fixed-size with a rebound local on gc. The memory contract records that this "must be reconciled or documented before unifying defaults".

The same document says what happens once the gc lane emits real GC allocations. Eager RC against lazy GC is a fundamental difference, and the plan for keeping observable behaviour identical across it is a differential test rather than an argument.

Oracle documents

The module system is written up in a genre that neither Wado nor this rubric's axes account for.

docs/module-system-oracle.md declares itself the executable canon of its ADR. A Lean model under formal/VibeFormal/Module/ decides package boundary, ownership, visibility and hash input. The compiler's loader then refines that judgement onto a real filesystem. The prose is the readable form of the model, and it says so: every other document touching the subject "links here and holds no explanation of its own", and where documents from different generations disagree, this section and the Lean model win.

Its table is the shape worth stealing. One row per observable rule, and three columns: the Lean definition that decides it, the implementation function that refines it, and the test that pins the pair together. So Boundary and Workspace.ownerOf sit against nearest_vpkg_path_fs, AllowedImport against enforce_incoming_boundary_fs, and automaticallyIncludedInPackageHash against collect_package_hash_source_closure_fs.

It closes with an Epistemic status section. That section says what the proof does not cover — filesystem path normalization, the parser, the cache, hash bytes and wasm codegen are none of them extracted from Lean. It says what holds those instead: regression tests and a fixpoint gate. And it says what would close the gap: a differential oracle that turns fixture graphs into Lean inputs, and an inductive model of hash-closure reachability.

One decision inside the model is worth taking on its own. Transparent types declared in a package contract are ambient inside that package. Rather than special-case how a sibling module resolves an enum constructor, the loader writes those types out as a real source module under _build/, content-keyed and ownerless. The facade re-exports it, and siblings import it through the ordinary mechanism.

That turns a special case into an instance of the general rule. What it prevents is a second definition site, because two same-named enums would become separate identities and trap at run time.

C. What outlives it

Kind What Externality Contender Wins
C1 Artifact None outside the toolchain, across examples/, tools/, clients/ and integrations/: editor integrations (tree-sitter, VS Code, Zed), a JS/wasm embedding client, and the LSP all serve vibe itself — — —
C2 Method docs/pl-survey-2026-07.md — a PL survey that reads primary sources, tabulates implications per topic, and turns them into prioritized adoption proposals carrying landed status High Reading papers without a decision trail Yes
C2 Method eval/ — five evaluation loops: msr, lang-review (rubric-based design review), lang-bench, call-style, book-review High — —
C2 Method "Documents rot; delete them", the ADR log rules, and the gate registry with per-gate self-tests High Status banners and an append-only decision log Yes
C2 Method The executable book: 40 *.vibe.md chapters whose code blocks are compiled and run with the output embedded, and whose translations are held to identical output by a CI gate High Prose documentation with untested examples Yes
C2 Method The cheatsheet's measured-pitfalls section: every rule carries the date it was measured against the compiler and the fixture that pins it, and the section records where it was previously wrong High A "common mistakes" list written from memory Yes
C3 Proof formal/ — Lean 4 models of the effect row, the checked Error policy, capability and resource contracts, parallel scheduling, and the module system, driven by oracle scripts. Includes negative witnesses: a deliberately broken checker is modeled and shown to admit an undeclared Fs operation High — —
C4 Corpus 1,266 fixture and test files, 42,467 lines Medium — —

C is thick in method and proof and empty in artifacts. That is the same shape as Almide, arrived at from a different arbiter, and limited the same way by A6. The only generation here is a fixed derive set, and the only way values cross the wire is an untyped Json:: API. A foreign grammar or format has no route in, so what you can build with the language is what the language is made of.

The self-hosted compiler is deliberately not counted here. It would be worth nothing if the language stopped, which is exactly what makes it an A cross-check rather than spin-off value.

The measured-pitfalls section says its purpose is to stop anyone investigating the same question twice. Two of its entries record the section itself having been wrong.

One entry had quoted a diagnostic's own list of rejected shapes and used it as the rule. Measuring showed that four of the shapes the message called rejected actually compile, and that the two which do fail, fail for reasons the message never mentioned. The message was rewritten and a measured table replaced the list. The other entry notes that the cheatsheet had the declaration separator backwards, in the very place that explains it.

The section's lead entry shows what the format is for. If a library defines fn String::index_of, that definition replaces the builtin of the same name across the entire linked program. A file that imports only some unrelated function from that library still gets the replacement. Nothing is reported either way, so you cannot tell the two readings of one source apart without running it.

The measured cost is 0.8 us with the builtin against 174 us with the shadowing definition — and the answers differ too, not only the speed. They then built a lexical scanner to catch the hazard, and it missed three ways: a raw identifier, a declaration attribute in front, and a declaration split across lines. The entry concludes that working out what a declaration binds is the compiler's job.

docs/pl-survey-2026-07.md is this document's own genre, done first. It surveys Almide specifically, down to naming almide-mir/src/alias_safety.rs as ahead of vibe's own Perceus work. It also records that Almide's binary-size benchmark method and its modification survival rate were both adopted, with the issue numbers where they landed. The eval/msr/ README says where it came from outright.

For Wado

Learned:

Take:

Refuse:

Hold: