v0.1.11
Added
-
[E130]lists the bindings in scope at the error position (#558). An unresolved slot reference reported only how many same-typed bindings existed, so recovering the right index meant tracing pattern pushes andlets by hand, or writing a typed hole and re-running —vera check --explain-slotsstops at the signature and is no help several levels into amatcharm. The fix text now ends with the sameAvailable bindings: @T.n: Type; …table theW001typed-hole warning already emits, rendered from the scope at the reference itself, so the read-time diagnostic carries what the write-time one always did — the same set from the same helper, zero-size bindings included, because the index range in the description counts them and hiding them would make one diagnostic describe two different scopes (@Unit.1against(@Unit, @Int)reporting "valid indices: 0..0" above a table with noUnitrow). A zero-size read staysE182's to explain. Nothing is appended when no binding is in scope. The table renders at most twelve rows and then; … and K more, because the scope it reports is unbounded and the language server concatenates the fix into the hover message, so a wide function would turn one diagnostic into a wall of rows nobody reads — thirty same-typed parameters rendered a 492-character fix. Twelve sits above the measured corpus: across the 2,080 slot-reference positions intests/**/*.veraandexamples/the table is 7 rows at the 95th percentile and 11 at the 99th, so every position through the 99th renders complete and 12 of the 2,080 elide.W001renders through the same capped helper, so the two diagnostics still agree row for row; the LSP's typed-hole completion consumes the binding list itself and keeps every row, since there a dropped row is a missing completion item. This is the issue's option (a); the positional query (option (b),--explain-slots-at <line>:<col>) stays open on the roadmap. -
The browser runtime's untested host imports are now exercised, and the two divergences that fell out are pinned rather than papered over.
tests/test_browser.pygains a battery covering the Map, Set,Decimal,readJson/json_stringifyandResult.Errbindings thatvera/browser/runtime.mjsregistered but nothing ever invoked — several closure bodies had zero hits — taking the file from 81.74% to 86.86% lines under theVERA_JS_COVERAGE=1 pytest tests/test_browser.pycommand TESTING.md documents (#349). Each case compiles one.wasmand runs it under both wasmtime and Node, so a failure isolates the host import rather than codegen. Writing them surfaced two browser↔native divergences of the kind spec §12.9.3 forbids:json_stringifydiffers on separator padding and on integral-number rendering, and traps natively on a NaN number where the browser silently emitsnull(#1293); andmd_renderbreaks the round-trip property §9.7.6 states for it on any multi-line paragraph — not just the list lazy continuation first observed — is not stable under re-render, and destroys a nested blockquote outright the second time round (#1294). Neither is fixed here, because fixing means editingruntime.mjsand this change is tests and documentation only; instead both runtimes' exact current strings are asserted separately, including the destructive blockquote case, so a fix on either side goes red until the pins are updated deliberately. Alongside them,TESTING.mddrops a "91% combined" coverage figure that no documented command produces (the two collectors measure different line populations, so the blend needs a line-weighted total neither report emits), corrects a blanket claim that no action ref is pinned to a commit SHA —pypa/gh-action-pypi-publishandcodecov/codecov-actionboth are — refreshes its~109,000-line test-corpus estimate to the measured~152,000, and repopulates its Open CI/Tooling Issues table, which had been emptied to "No open CI/tooling issues" while six remained open.vera/README.mdandFAQ.mdlose an unqualified "identical results" parity claim that the two divergences above contradict, along with a stale hand-count of parity tests and a runtime-family count that disagreed with itself in three places against the fourteenregister_<effect>entry pointsvera/runtime/actually defines. -
The editor grammars are gated against the effect registry. The three grammars under
editors/(vscode, TextMate, Vim) enumerate the built-in effect names by hand and nothing checked them, so they drifted:HttpServer,InferenceandRandomnever reached the vscode and TextMate grammars, andDBreached none of the three. The drift is silent — an unknown capitalised identifier falls through to the generic type-reference rule, soDB.query(...)still highlights as something, just not as an effect — so four accumulated unnoticed.scripts/check_editor_grammars.pyreadsvera.introspect.effects_payload()from the checkout it is checking and requires a word-boundary occurrence of every registered name in each grammar, and in the two extension READMEs that repeat the list in prose and had gone stale in exactly the same way. Absence is conclusive, presence is optimistic, which is the right way round when the failure is omission and keeps the check immune to the formats involved (JSON, plist XML, Vim regex, Markdown). The checked list is explicit, since not every file undereditors/is a grammar, so it is paired with a completeness guard: a file in a syntax directory, or carrying a grammar extension — the tree-sitter.scmquery sets and a.tmLanguage.jsonfiled anywhere butsyntaxes/among them — that the list does not name fails the gate rather than passing unchecked. It runs as a pre-commit hook and a CI step, triggered byeditors/and by the registry files themselves, since adding an effect is what puts the grammars out of date. Registering that CI step exposed the same defect one layer out — TESTING.md's CI-pipeline table hand-enumerates the scripts the lint job runs and nothing held the two in step, so the new step left the table describing 22 of the 23 scripts the job invokes;scripts/check_doc_counts.pynow compares that row against the workflow as an ordered list, treating a reworded row or a renamed job as an error rather than a skip. Abilities (Eq/Hash/Ord/Show) stay out of scope: whether they should highlight distinctly from ordinary types is an open design question, tracked separately as #1295. -
xAI (Grok) provider for the Inference effect, and one flagship model per provider (#425) —
Inference.completenow supports Grok models. SetVERA_XAI_API_KEYto use. xAI's endpoint is OpenAI-compatible (bearer auth,choices[0].message.content), so this is one row in the_PROVIDERSregistry with no new dispatch code. Appended last in the registry, which leaves the existing auto-detect precedence unchanged. (The issue asks forgrok-3-mini-fast-beta, which xAI documents nowhere any more — neither in its model list nor in the retirement table that names the redirect target for each slug it withdrew — so the default is a currently-listed model instead.) Alongside it, every row'sdefault_modelis now that provider's flagship general-chat model rather than its cheap/fast tier:claude-opus-5,gpt-5.6-sol,kimi-k3,mistral-large-latest, andgrok-4.6for the new row. Each was verified against the vendor's own live documentation at the time of the change, which also retired two IDs their vendors no longer list (gpt-4o-mini,kimi-k2-0905-preview). A program's contracts are written against what the default model can do, so capability — not price — is what the default owes the caller; the cheap tier stays oneVERA_INFERENCE_MODELaway. This raises the per-call cost of any program that relies on the default, on every provider. The provider tests now pin each model ID as a literal:test_openai_providerasserted against_PROVIDERS["openai"].default_model, which pinned the value to itself and would have stayed green through the flip, andtest_anthropic_providerasserted no model at all.test_xai_provideralso asserts the request'sContent-Type, which its OpenAI sibling checked and it did not, so the new row's headers are pinned as completely as the row it was modelled on. The auto-detect tests assert the API key that reaches_call_inference_provider, not only the provider name it selected: with two keys set, naming the right row while reading another row's environment variable satisfies a name-only assertion, and the four-case precedence test now fails on exactly that.SKILL.mdalso states the detection rule the tests pin — the registry is walked in insertion order (anthropic,openai,moonshot,mistral,xai) and the first provider whose key is set wins — where it previously said only "auto-detected from whichever key is set", which answers nothing when several are. Detecting IDs that rot at the vendor still needs network access and is tracked in #1263. Closes #425.
Changed
-
resumeis reserved as a function name, as Chapter 1, Section 1.4 already required. The MUST was unenforced:resumeis not a keyword token —vera/grammar.larkhas noRESUMEterminal and lexes the name as an ordinaryLOWER_IDENTeverywhere — soprivate fn resume(@Int -> @Int)parsed, type-checked, and ran, and a bareresume(7)outside a handler resolved to it. The declaration is not merely dead weight: inside every handler clause body the checker bindsresumeto the effect-resumption operator, and with a top-level declaration present the clause bodies resolved against its signature instead — an otherwise validhandle[State<Int>]was rejected with[E202]onput(@Int) -> { resume(()) }("has type Unit, expected Int"), and deleting the declaration made the identical handler check clean. Declaring the name broke working code elsewhere in the file. Such a declaration is now rejected atvera checkwith E153, on the same rail asold/new(#1181) and the grammar keywords (#1187), in a fourth named piece (_HANDLER_OPERATOR_FN_NAMES) with its own rationale — the keyword branch's reason, that no unqualified call site can reach the declaration, is false here and would have misinformed the reader. The rejection is also the only diagnostic the program gets. A bare call resolves lexically — the enclosing where-helpers, then the top-level function of that name, then the flat registry — and the clause binding lives in that last tier, so a declaredresumeshadowed it and drew a second error out of clause bodies that were correct, at both top level and where-helper depth.resumenow resolves against the flat registry alone, which is the only place its binding can live; in a valid program nothing changes, because the name is reserved and no declaration of it exists to be found. The reservation is on declarations only:resume(...)inside a handler clause is bound by the handler rather than declared and is untouched, as isresumedor any other longer name. Awhere-helper namedresumeis rejected one scope deeper, like every other reserved name. Covered bytests/conformance/ch05_reserved_resume_fn_rejected.vera. The neighbouring statement in Chapter 10 — thatresumeis aLOWER_IDENTandresume(expr)parses as an ordinaryfn_call— remains true, and now says which phase enforces the reservation instead. -
vera/addEffectbounds its propagation at handlers (#725). The transitive-caller closure was handler-unaware: a caller that wrapped its call inhandle[E]hadEappended to its owneffects(...)row even though the handler discharges it there, so the workflow wrote rows the program does not need and dragged that caller's own callers in behind it. A call site inside ahandle[E]body now contributes no edge, so propagation stops at the function that discharges the effect. Three cases deliberately still propagate, because the effect really does escape them: a caller that reaches the callee on any unhandled path as well (the row is needed for that path), a call in a handler clause body, which runs outside its own handler, and a call in the handler's state initialiser, which escapes for a different reason — the initialiser is evaluated in the enclosing scope, before the handler is installed, so an effectful call there is anE125against the caller's own row. A handler bounds the propagation only when itshandle[...]head is spelled the way the request is, type arguments included and compared as written rather than as resolved. That is required in one direction: the checker discharges againstEffectInstanceequality, sohandle[State<Nat>]leavesState<Int>escaping and a caller around it still needs the row — pruning that edge on a base-name match would leave the callerpureand fail the whole candidate onE125. In the other direction it under-prunes:handle[State<MyAlias>]withtype MyAlias = Intis discharged by the checker but is not spelled the way aState<Int>request is, so that caller keeps a row it does not need — documented behaviour until #1292 keys the bound on the resolved instance. Every non-match keeps the edge, which is the safe direction: a row the program does not strictly need still type-checks. (Row identity, a separate question, remains the base name —State<Int>is not appended beside an existingState<Bool>.) Containment is structural (the handled sub-tree), not span arithmetic, so the handler bound applies inside awhere-helper body exactly as it does at the top level, and a helper's bare call still attributes to its containing top-level function as before. Row rewriting is unchanged and still top-level-only: awherehelper that needs the new effect does not get it, and the gate refuses the resulting candidate rather than applying a broken one. TheKNOWN_ISSUES.mdlimitation row is retired. TheLSP_SERVER.mdrow stated two things, only one of which is fixed, so it is replaced by a by-design row for the other: propagation still stops at the file boundary. -
DeepSeek provider for the Inference effect (#450) —
Inference.completenow supports DeepSeek. SetVERA_DEEPSEEK_API_KEYto use. DeepSeek's endpoint is OpenAI-compatible (bearer auth,choices[0].message.content), so this is one row in the_PROVIDERSregistry with no new dispatch code — the issue's proposed env-var fetch inhost_inference_completewould have been dead code, since that function already iterates the registry. Appended last, afterxai, leaving the existing auto-detect precedence unchanged. The default isdeepseek-v4-pro, following the one-flagship-model-per-provider convention the other five rows adopted in #425: DeepSeek's own API reference lists exactly two currentmodelvalues,deepseek-v4-flashanddeepseek-v4-pro, and positions the latter as the flagship — its change log records "The GA release of DeepSeek-V4-Pro has been rolled out on the APP, Web, and API" (2026-08-13) againstdeepseek-v4-flash's public beta (2026-07-31), and every code sample in the documentation's first API call sendsdeepseek-v4-pro. Capability, not price, is what a default owes the caller, because a program's contracts are written against what the default can do;deepseek-v4-flashis oneVERA_INFERENCE_MODELaway. (The issue asks fordeepseek-chatwithdeepseek-reasonerfor the reasoning model. DeepSeek's change log announced both discontinued as of 2026-07-24, a date now past, and documents neither; the default is a currently-documented model instead. There is also no reasoning model to pointVERA_INFERENCE_MODELat any more: on v4, thinking mode is a request-body field, which the registry does not send, so no model override reaches it.) The provider tests meet the #425 bar — the model ID and the endpoint as literals rather than registry-derived expressions that would pin each value to itself, theAuthorizationandContent-Typeheaders, the absence of Anthropic'sx-api-keyandmax_tokens, the exact turn list, and the API key that reaches_call_inference_providerrather than only the provider name it selected — and the precedence battery runs one case per provider ahead ofdeepseek, all five, so relocating the row fails exactly the providers it jumped. Detecting IDs that rot at the vendor still needs network access and is tracked in #1263. Closes #450.
Fixed
-
Chapter 10's EBNF names the rules the parser actually has (#683). The published grammar still called the assertion forms
assert_stmt/assume_stmtafter they became expressions (assert_expr/assume_exprinvera/grammar.lark), and three Lark rules appeared in no EBNF block at all:pure_effectandeffect_setwere inlined intoeffect_row, andwith_clausewas missing entirely, leaving thewithform of a handler clause undocumented as a production.scripts/check_grammar_alignment.pynow holds the two files together in pre-commit and CI — every rule header in one must exist in the other, with a six-entry allowlist for the pairs that differ on purpose (start/program, and the four spec headers Lark expresses as-> aliasnames or folds into a more general rule). Rule names only; bodies are not compared. Each allowlist entry records the side its name lives on and, where its reason rests on a Lark-> alias, both that alias and the production it must be an alternative of; those facts are checked rather than asserted, against comment-stripped text, so neither deleting the alternative nor commenting it out nor moving it to another rule can hold a waiver up, and deleting the waived spec production fails too instead of passing forever. What the alias premise does not establish is that the Lark alternative still spells the same construct as the spec production — a body-level fact a header-only gate cannot see;tuple_literalandtuple_typerest onconstructor_callandnamed_type, general forms that would outlive tuples leaving the language, and the script says so at both the entry and the module docstring rather than claiming more. A name gone from both files is reported as a broken premise rather than as "the sides now agree" (the old symmetric-difference arithmetic pointed the reader at the deletion that turns the gate green with the construct documented nowhere), and each name yields at most one report, so a spent waiver whose premise also broke no longer asks for the waiver's deletion and its restoration in the same run. The header extractor also tolerates Lark's template parameters and rule priorities (sep{item}.2:), which previously dropped a rule from the Lark set entirely. Two further Chapter 10 defects are fixed:statementcarriedassert_expr SEMICOLON | assume_expr SEMICOLONalongsideexpr SEMICOLON, soassert(p);derived two ways in the published grammar while the reference parser has one production (expr ";"); and aRESUME: "resume"terminal was declared but referenced by no production, contradicting the neighbouring note thatresume(expr)is an ordinaryfn_call— under the spec's own lexer a keyword terminal shadowsLOWER_IDENT, andvera/grammar.larkhas no such terminal. That criterion, applied to the rest of §10.2 rather than toRESUMEalone, clears five more phantom declarations and one omission.SOME/NONE/OK/ERRwere declared as keyword terminals and referenced by no production:Some,None,OkandErrare ordinary prelude ADT constructors (data Option<T> { None, Some(T) },data Result<T, E> { Ok(T), Err(E) }), so under the spec's own lexer those four declarations would shadowUPPER_IDENTand stopSome(1)parsing as a constructor call at all — the identical defectRESUMEhad againstLOWER_IDENT.COLONwas declared for a:that no production in either file uses. All five are deleted. The converse case is fixed with them:module_callreferenced aDOUBLE_COLONthat §10.2 never declared, now added asDOUBLE_COLON: "::", the spellingvera/grammar.larkcarries as an inline literal. The whitespace and comment terminals stay — they are labelled "(skipped)" and%ignored, so no production referencing them is correct. This fixes the six instances; auditing declared-versus-referenced terminals as a gate is #1290's scope, and the rule-name gate here does not compare terminals. The issue also asked for aqualified_call→module_callrename in the Lark grammar, which is not done and should not be: those are two different constructs (Effect.op()andmod::fn()), both already present under both names, and merging them would be a regression. The gate has a test pinning that it never reports them. -
Two documentation statements corrected in review. The FAQ's contract-testing walkthrough read
requires(@Int.1 != 0)as constraining the second parameter, inverting the De Bruijn rule the same document links out to:@Int.0is the most recent binding, so@Int.1is the leftmost — which is exactly whatexamples/safe_divide.vera, the example that answer tells the reader to run, guards as its divisor. Separately, the compiler-architecture README's cross-cutting summary creditederrors.pywith theE-codes alone, where the registry described further down the same file also holds theW001/W002warning codes. -
TESTING.md's conformance section counts the programs the manifest actually holds. The section states each non-
runlevel twice — as a number in prose and as a hand-written list of program names — andch05_reserved_resume_fn_rejectedreached neither, so thechecklevel read thirty-eight against a manifest holding thirty-nine and the negative-test subset read thirty-one against thirty-two. The parenthesised E-code sequence is aligned to that subset "respectively", so it was short by the same one entry — the thirty-one codes present did pair correctly with the thirty-one names present, which is why the omission left nothing visibly wrong to notice. The same section's parametrized-suite count read 1,035 where the runner collects 1,070 — five checks over each of the 214 programs — a number that had drifted seven programs back, before the fixture above existed. None of the four is gated:scripts/check_doc_counts.pypins the suite total, the per-file table and the skipped-stage rows, but reads neither prose list nor the count in the run instructions, which is why a fixture could land with its skipped-stage rows added and its two list entries missed. All four are corrected against the manifest, which is the oracle for every one of them. -
Three more documentation statements corrected in review. The FAQ's browser-parity answer claimed every operation but
json_stringifyandmd_renderbehaves identically across the two hosts, which is not true of two whole effects:InferenceandDBreturn an explanatoryErrfrom every operation in the browser, because the API key or database credential they need would be readable from page source. That refusal is now stated as what it is — a deliberate platform boundary, distinct from the two divergences, which are unintended and tracked — alongside the fact thatHttpgenuinely does work there, backed byXMLHttpRequestrather than a stub, so a server-side proxy is a real escape route rather than advice. Separately, TESTING.md describedcheck_doc_counts.pyas verifying that "counts cited in the docs match the live codebase", a blanket claim the entry above disproves; it now enumerates the citations the script actually reads, and says plainly that a count it does not name is not read at all and that hand-written program lists are outside its scope. Finally, the compiler-architecture README and itshost-familiesdiagram both counted two stateful family adapters, Decimal and State, where there are three:async_http.py'sregister_asynctakes afuture_storethatexecute()creates up-front, publishes it ashost_store_refs["future"]so its size reachesExecuteResult.host_store_sizes, and has entries evicted by the samehost_decref_handle— kind 4, which also cancels a future that never started (#841). Prose, diagram label, legend and accessible title are corrected together. -
Spec §1.4's reserved-keyword list now matches the checker:
existswas missing andhandle's host-invoked carve-out was unstated.