Diagnostics reference
Diagnostics reference
Every compiler diagnostic is a stable, named code. Each one has a calm headline, a known set of triggers, a verified reproducer the compiler actually rejects, and a path forward. This is the second layer of the error message — the part the short compiler output could not always reach.
Every rule below describes the current Vision compiler. The vision why <code> command prints the same teaching paragraph from the command line, and the --explain flag emits it under every error in the default stderr output. This page is the same data in narrative form, with a fixture for each documented diagnostic so you can confirm the wording against the compiler you have on disk. The Effects & failure reference, Construction & absence reference, and Generics reference each cover the language surface that the corresponding diagnostic guards.
How to read a diagnostic
When the compiler rejects a program, the message has one fixed shape: a code word, a near-line pointer, and the calm headline. The line looks like vision: near line N — <headline>. The diagnostic code is not printed by default; the headline is the only thing a reader sees on stderr. To recover the code, the reader runs vision why <code> and the compiler prints the teaching paragraph and the small worked example.
Each entry below lists the code, the verbatim headline the compiler emits, when the diagnostic fires, a verified reproducer (a tiny program the real compiler rejects with exactly this message), how to fix the program, and the cross-references into the Tier 1/2 reference pages that describe the surrounding language surface.
The page documents ten diagnostics that span the floor of the surface: the E1 optional-read-* pair, the E2 built-from-nothing-literal, two generic-verb rejections, one C-library discovery rejection, the monomorphized-host-exposure rejection, the nested-server ruling, the reserved-name ruling, and the generic unhandled-failure ruling. The compiler registers 294 codes in total; the full registry lives in src/diag.c and is discoverable through vision why <code> for any one of them. The diagnostics gate (scripts/check-diagnostics.mjs) reads that registry directly and refuses any fixture whose declared code is not in it, so the page cannot drift toward invented IDs.
Status: stable diagnostic IDs are present
The campaign brief asked us to flag the absence of stable IDs as a prerequisite gap. That gap does not exist in the current compiler. Every diagnostic has a stable, beginner-readable code word, registered in src/diag.c's DIAGS[] table and reachable through vision why <code>. The code is the lookup key; the headline is the message; the teach paragraph is the second layer. This page is keyed by the code.
built-from-nothing-literal
Headline: a bare `nothing` has no field name in `built from (...)`; omit the optional field, or use block construction with `<field> is nothing`.
Triggers. A built from (...) clause contains a bare nothing token. The builtfrom_args production in the grammar accepts onlywordrun alternatives (parser.y); theNOTHING_TOK productions route to reject_builtfrom_nothing_literal() and emit this diagnostic. The names-only field-wiring rule is structural — nothing has no name, so accepting it would force positional matching and break the declaration-order field walk.
// diagnostic: built-from-nothing-literal
//
// A bare `nothing` in `built from (…)` has no field name. The E2 ruling
// rejects this at the parser so the names-only field wiring stays stable.
kind card:
a title is text
a subtitle is text or nothing
to begin:
the title is "Hello"
the note is a card built from (title, nothing)
say note.title// diagnostic: built-from-nothing-literal
//
// A bare `nothing` in `built from (…)` has no field name. The E2 ruling
// rejects this at the parser so the names-only field wiring stays stable.
kind card:
a title is text
a subtitle is text or nothing
to begin:
the title is "Hello"
the note is a card built from (title, nothing)
say note.titlevision: near line 11 — a bare `nothing` has no field name in `built from (...)`; omit the optional field, or use block construction with `<field> is nothing`How to fix. Either omit the optional field — built from (title) leaves subtitle at its declared default, which for an or nothing field is nothing — or use block construction to name the field explicitly: the note is a card: title is "Hello"; subtitle is nothing. The E2 ruling is documented in the Construction & absence reference.
optional-read-needs-presence-proof
Headline: <value> might be nothing — prove it is present with `when there is` or `is something` before reading it.
Triggers. The compiler cannot see a presence fact for this value at the current control-flow path. The presence fact from the is something branch is local to its block; a later line that reads the same value, even after no assignment, cannot inherit the proof. The E1 facts live in branch-local state (src/flow.c's flow_state_meet) and are invalidated by any write (FLOW_INVALIDATION_WRITE) or possibly-mutating alias (FLOW_INVALIDATION_MUTATING_ALIAS). The check at this site assumes an outside-the-block caller could have rewritten the field, so the proof is not portable.
// diagnostic: optional-read-needs-presence-proof
//
// The presence fact from the `is something` branch is not in scope on the line
// that follows the block — the assignment could in principle have mutated the
// value. Re-prove presence before reading.
kind profile:
name is text
note is text or nothing
to begin:
the person is a profile
person.note is "hi"
when person.note is something:
say "got greeting"
say person.note// diagnostic: optional-read-needs-presence-proof
//
// The presence fact from the `is something` branch is not in scope on the line
// that follows the block — the assignment could in principle have mutated the
// value. Re-prove presence before reading.
kind profile:
name is text
note is text or nothing
to begin:
the person is a profile
person.note is "hi"
when person.note is something:
say "got greeting"
say person.notevision: near line 15 — person.note might be nothing — prove it is present with `when there is` or `is something` before reading itHow to fix. Move the read inside the is something branch, or add a fresh when there is / is something guard at the read site. The long-form E1 reasoning is documented in the Effects & failure reference.
optional-read-known-absent
Headline: <value> is known to be nothing on this path — do not read it here.
Triggers. The read sits inside the is nothing branch of a presence test, where the compiler already knows the value is absent. Reading a known-absent value is an unconditional dereference of the absent state, which the compiler cannot permit because the runtime cannot produce a value to render. The fix is structural, not stylistic — the absent branch should emit nothing for the absent field.
// diagnostic: optional-read-known-absent
//
// Inside the `is nothing` branch the checker already knows this value is
// absent. Reading it here is an unconditional dereference of the absent
// state, which the compiler cannot permit.
kind profile:
name is text
note is text or nothing
to begin:
the person is a profile
person.note is "hi"
when person.note is nothing:
say person.note
else:
say "got it"// diagnostic: optional-read-known-absent
//
// Inside the `is nothing` branch the checker already knows this value is
// absent. Reading it here is an unconditional dereference of the absent
// state, which the compiler cannot permit.
kind profile:
name is text
note is text or nothing
to begin:
the person is a profile
person.note is "hi"
when person.note is nothing:
say person.note
else:
say "got it"vision: near line 14 — person.note is known to be nothing on this path — do not read it hereHow to fix. Read the value inside the else branch (the present edge of the test), or remove the read entirely from the absent edge. Presence tests are structural; the absence proof is real, not a soft hint.
unknown-typesig-type
Headline: "<word>" isn't a known type; <verb>_<param> can't work on it.
Triggers. A which works on or returns clause names a type word Vision does not know. The closed list belongs to the verb's or kind's own declaration, and every word in it has to be a type Vision already knows. The accepted words are: numbers (or number, whole numbers, whole number, whole), decimals (or decimal), text, bytes (or byte), secret, yes/no, and the name of any described thing, enum, or tagged union the program has declared.
// diagnostic: unknown-typesig-type
//
// `pickles` is not a Vision type word, so `which works on` cannot accept it.
// The closed list belongs to the verb's own declaration, and every word in it
// has to be a type Vision already knows.
to wrap(item), which works on numbers or pickles:
answer item
to begin:
say wrap(42)// diagnostic: unknown-typesig-type
//
// `pickles` is not a Vision type word, so `which works on` cannot accept it.
// The closed list belongs to the verb's own declaration, and every word in it
// has to be a type Vision already knows.
to wrap(item), which works on numbers or pickles:
answer item
to begin:
say wrap(42)vision: near line 6 — "pickles" isn't a known type; wrap_item can't work on itHow to fix. Pick a type from the closed list, or declare the type as a described thing in the program and use that name. The exact accepted-words list is documented in the Generics reference.
which-works-on-mismatch
Headline: <verb>_<param> works on <list>; you passed <type>.
Triggers. A call site passes an argument whose concrete type is not on the verb's closed which works on list. The selection is unambiguous because the argument already has one settled type when the call is checked; the compiler does not silently add the off-list type. The emitted message names the verb, the actual argument type, and the declared list so the reader can fix the call by picking a verb that fits.
// diagnostic: which-works-on-mismatch
//
// The call site passes `yes/no`, which is not on the verb's `which works on`
// list. The closed list does not silently accept new types — the verb's
// monomorphizations only exist for the types it declared.
to wrap(item), which works on numbers or text:
answer item
to begin:
the result is wrap(yes)// diagnostic: which-works-on-mismatch
//
// The call site passes `yes/no`, which is not on the verb's `which works on`
// list. The closed list does not silently accept new types — the verb's
// monomorphizations only exist for the types it declared.
to wrap(item), which works on numbers or text:
answer item
to begin:
the result is wrap(yes)vision: near line 10 — wrap_item works on numbers or text; you passed yes/noHow to fix. Pass an argument of one of the listed types, or add the type to the verb's which works on clause. Adding the type means another monomorphization will be emitted; a body that fails for the new type will surface as which-works-on-body-mismatch at the verb's own declaration. The full rejection family is documented in the Generics reference.
no-library-for-c-function
Headline: none of the libraries this program understands has a C function called '<name>'.
Triggers. A C face names a C function none of the understood libraries defines. The compiler parses the installed header of each this program understands <lib> library with the system clang (src/cbind.c's cbind_discover); the discovered function set is the only source of truth for which names a C face can bind. The compiler does not invent or guess — it asks the reader to add the library or correct the function name.
// diagnostic: no-library-for-c-function
//
// `foo` is not a function the understood library `zlib` exposes. The compiler
// names the libraries it does know and asks for the library or function name
// to be corrected rather than guessing.
this program understands zlib
to compress some bytes is foo
it takes some bytes
it gives back some bytes
to begin:
say "hi"// diagnostic: no-library-for-c-function
//
// `foo` is not a function the understood library `zlib` exposes. The compiler
// names the libraries it does know and asks for the library or function name
// to be corrected rather than guessing.
this program understands zlib
to compress some bytes is foo
it takes some bytes
it gives back some bytes
to begin:
say "hi"vision: near line 11 — none of the libraries this program understands has a C function called 'foo'How to fix. Add the library that does define the function with this program understands <lib>, or correct the function name to match one of the libraries the program already understands. The vision explain <lib> command prints every function the library offers, read from the library itself, so the reader never has to guess. The full this program understands surface is documented in the Host & C interop reference.
host-expose-invalid
Headline: <name> is monomorphized with `which works on`; expose a concrete wrapper verb for now.
Triggers. A this program exposes <name> to the host declaration names a monomorphized verb. The host ABI needs a single C symbol it can call by name; a monomorphized verb emits one C symbol per declared type. The same code is reused for fallible verbs, undefined verbs, verbs whose answer type is not a Layer 0 ABI shape, and begin (which is Vision's own standalone entry point). Each emission site composes a different message into the same diagnostic.
// diagnostic: host-expose-invalid
//
// `wrap` is monomorphized — it has no single C symbol for the host ABI to
// call. Expose a concrete wrapper verb that calls the monomorphized verb
// with one fixed type instead.
to wrap(item), which works on numbers or text:
answer item
to begin:
say wrap(1)
this program exposes wrap item to the host// diagnostic: host-expose-invalid
//
// `wrap` is monomorphized — it has no single C symbol for the host ABI to
// call. Expose a concrete wrapper verb that calls the monomorphized verb
// with one fixed type instead.
to wrap(item), which works on numbers or text:
answer item
to begin:
say wrap(1)
this program exposes wrap item to the hostvision: near line 12 — wrap_item is monomorphized with `which works on`; expose a concrete wrapper verb for nowHow to fix. Write a non-generic wrapper verb that calls the monomorphized verb with one fixed type, and expose the wrapper. For fallible verbs, restructure the verb to remove the failure clause before exposing; for non-ABI return types, structure the verb so it answers a Layer 0 shape. The full this program exposes surface is documented in the Host & C interop reference. The callable-ref-monomorphized rejection on the callable side has the same root cause.
nested-server
Headline: a server can't run inside another server; one `serve … for each connection:` is the program's main loop.
Triggers. A serve on port N for each connection: block appears inside another such block. The outer block is the program's main loop; nesting an inner server would either never return control to the outer accept loop or emit invalid C. The compiler enforces "one server is the program's main loop" at parse-tree shape, not at runtime.
// diagnostic: nested-server
//
// A `serve … for each connection:` block is the program's main loop. Nesting
// another server inside it would never return control to the outer loop, so
// the compiler rejects the inner `serve` outright.
>> to begin:
>> serve on port 9001 for each connection:
>> serve on port 9002 for each connection:
say "x"// diagnostic: nested-server
//
// A `serve … for each connection:` block is the program's main loop. Nesting
// another server inside it would never return control to the outer loop, so
// the compiler rejects the inner `serve` outright.
>> to begin:
>> serve on port 9001 for each connection:
>> serve on port 9002 for each connection:
say "x"vision: near line 9 — a server can't run inside another server; one `serve … for each connection:` is the program's main loopHow to fix. Move the inner work into a verb the outer connection body calls, or move the second server to its own program. The serve shape is documented in the Network & process reference.
reserved-runtime-name
Headline: "<name>" is reserved — it folds to "<c-spelling>", the exact C spelling of a symbol the compiler's own generated C relies on (either Vision's own runtime, or an external system/library function, type, or macro it calls by fixed name); pick a different name.
Triggers. A verb's or kind's name folds (spaces become underscores) into the exact C identifier of a fixed symbol the compiler's generated C relies on. The reserved set lives in src/ident.c (RESERVED_PLAIN_NAMES) and covers the prelude runtime functions, the VisFailure enum constants, the process_result / directory_entry / entry_kind / variable prelude types, and a sweep of every other C symbol the compiler emits by fixed name. The guard runs at declaration time so the collision can never reach the generated C.
// diagnostic: reserved-runtime-name
//
// `process result` folds to the C identifier `process_result`, which is the
// exact symbol the compiler's own generated C relies on for the `process
// result` thing. Pick a different name to avoid the silent collision.
to process result:
say "hi"
to begin:
say "ok"// diagnostic: reserved-runtime-name
//
// `process result` folds to the C identifier `process_result`, which is the
// exact symbol the compiler's own generated C relies on for the `process
// result` thing. Pick a different name to avoid the silent collision.
to process result:
say "hi"
to begin:
say "ok"vision: near line 8 — "process_result" is reserved — it folds to "process_result", the exact C spelling of a symbol the compiler's own generated C relies on (either Vision's own runtime, or an external system/library function, type, or macro it calls by fixed name); pick a dHow to fix. Pick a different name. The exact reserved set lives in src/ident.c; the teaching paragraph for this code points at the same set. The campaign continuity also records the precise spellings that are reserved.
unhandled-failure
Headline: this can fail; please say what to do with a `|`.
Triggers. A fallible call (a verb whose signature names one or more failure kinds) is invoked without a failure clause. Every fallible call must either name each failure kind it can experience (with a | <tag>: clause) or accept the failure and pass it on (with | pass). The failure is part of the call's signature, not an exceptional case the runtime might or might not hit; the compiler refuses the call the moment it sees the missing clause.
// diagnostic: unhandled-failure
//
// `write` can fail (the disk is full, the path is missing, the bytes could
// not be written). The body of `to begin:` never says what to do on failure,
// so the compiler rejects the call. Add a `| tag:` clause or pass the failure
// on.
>> to begin:
the payload are a list of bytes
add 65 to the payload
>> write the payload to the file at "/tmp/x.txt"// diagnostic: unhandled-failure
//
// `write` can fail (the disk is full, the path is missing, the bytes could
// not be written). The body of `to begin:` never says what to do on failure,
// so the compiler rejects the call. Add a `| tag:` clause or pass the failure
// on.
>> to begin:
the payload are a list of bytes
add 65 to the payload
>> write the payload to the file at "/tmp/x.txt"vision: near line 10 — this can fail; please say what to do with a `|`How to fix. Add a | <tag>: clause for each failure kind the operation can produce, or pass the failure on with | pass. The failure-kind table and the pass resolution are documented in the Effects & failure reference.
Floor coverage
The ten diagnostics above satisfy the floor the campaign brief required: the bare-nothing E2 ruling, the optional-read-* pair, three generic / type-mismatch codes (unknown-typesig-type, which-works-on-mismatch, host-expose-invalid on the monomorphized side), two host / C-interop codes with full sections (no-library-for-c-function, the monomorphized host-expose-invalid) — plus the related callable-side callable-ref-monomorphized cross-referenced from the host-expose-invalid entry, one signal/network code (nested-server), one construction-site code (built-from-nothing-literal), one reserved-word identifier code (reserved-runtime-name), and the generic unhandled-failure. The compiler registers 294 codes in total; the page covers a representative floor and the rest are reachable through vision why <code> and the same second-layer pattern.
Current limitations
The compiler's diagnostics surface is deliberately small but real. The following should be treated as gaps, not as supported behavior:
- The default sink does not print the code in stderr. The
vision: near line N — <headline>format is the only thing a reader sees by default. To recover the code, a reader runsvision why <code>or compiles with--explain, which appends the teach paragraph under each error. A built-in editor / LSP integration that surfaces the code directly is not part of the command-line surface. - Some call sites compose the message. The
host-expose-invaliddiagnostic covers monomorphized verbs, fallible verbs, undefined verbs, non-ABI return types, and thebeginedge case — each emission site composes its own message into the same code. The teach paragraph names the monomorphized case because it is the common one; the other shapes share the same fix shape (wrap or restructure the verb). - The reserved-name buffer truncates at 256 bytes. The
reserved-runtime-nameemission site composes the call-site message into a 256-byte buffer (src/check.c'scheck_reserved_name_collision). The default stderr therefore ends with a cut-off word (typically "pick a d"); the full reasoning is in the teach paragraph and insrc/ident.c'sRESERVED_PLAIN_NAMES. The compiler does not lose the diagnostic — it just surfaces the short form on stderr. - The fixture gate is byte-for-byte. The diagnostics gate (
scripts/check-diagnostics.mjs) compares stderr against the pinned.stderrfile byte-for-byte. Any change to the compiler's headline wording — even a punctuation tweak — fails the gate until the pin is updated. This is deliberate: the brief says "verified by running the compiler, not written from description," and a byte-exact check is the only way to keep the page honest. - The page is a floor, not a registry. The compiler registers 294 distinct codes; the page documents ten. The remaining 284 codes are reachable through
vision why <code>and through the same .vis + .stderr fixture pattern the diagnostics gate enforces. Future versions of this page can extend the per-diagnostic sections without changing the harness shape.