You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Fixed
vera fmt no longer deletes inline comments (#1123). A comment with code before it on the same line -- --, {- -} and /* */ alike -- was classified, filed into _Attached.inline, and then dropped, because nothing ever read that store; only comments occupying a whole line survived. vera fmt --check compounded it by reporting such a file as non-canonical, so the remedy it prescribed (vera fmt --write) was what performed the deletion, in place and with nothing on stderr. Placement is now decided at emission time by a single rule -- a comment is claimed by the innermost construct whose span contains it -- with claim points after each statement, block result, signature, contract clause and effects clause, and a declaration-level backstop so a comment that fits no inner construct (on a brace or where line, say) is relocated rather than discarded. Claims compare full (line, column) positions and stop at the next construct's start, since two statements may share a line and a line-granular claim would hand both their trailing comments to whichever is emitted first. Because nested constructs emit first, claiming greedily yields innermost-wins, and the result is a fixed point: re-formatting finds each comment already trailing its construct. Annotation labels on a parameter or return slot are exempt -- they come from the AST -- and the signature claim skips them so they are not emitted twice. A claimed comment is collapsed onto one physical line when its text spans several — a {- -} can be multi-line and still inline, and appending it verbatim would leave the continuation carrying its original source indentation through the final join, against spec 1.8's two-spaces-per-level rule. Spec 1.8 gains the preservation rule it never stated, which is why the deletion went unnoticed.
vera fmt preserves comments in every declaration form, not just functions (#1123). The claim points and the declaration-level backstop all lived inside _emit_fn_decl, so data, type, effect, ability, module and import had none: a trailing comment in any of them was filed into the write-only inline store and dropped, exactly as before the fix, while the release note claimed otherwise. Every declaration now claims, with per-item points on constructors and effect/ability operations so a comment trails the item it belongs to rather than the declaration's last line. A comment written above a module or import was likewise filed into before[line] and never read — examples/vera/math.vera and collections.vera were losing their header lines — so those buckets are read too. Two guards keep it honest: a sweep asserting no corpus file loses a comment when formatted, and a fixture carrying one in every position the emitter has.
Match-arm comments stay on their arm, and formatting is a fixed point for every comment shape. An arm is not a Block, so the block-body claim never reached it and every arm comment fell to the declaration backstop. Separately, overflow comments were spilled onto a new line, which turned an inline comment into an own-line one — and own-line comments attach to the nearest anchor after them, so a comment following the last statement in a block walked out of its function on each pass until it landed at top level. Everything claimed at one point now stays on that one line, with line comments last and any non-final one re-delimited as {- ... -} so it survives as a distinct comment instead of being absorbed. Guarded by two invariants over every comment shape — comment count preserved, and fmt(fmt(x)) == fmt(x) — because deletion and merging show up only in the first, drift and relocation only in the second.
A -- comment no longer swallows the comments after it. Comments claimed at one point were joined onto a single physical line, and since -- runs to end of line, anything following it became part of its text: four distinct comments re-read as one, irreversibly and idempotently. At most one line comment may share a physical line now, and it must be last; the rest go on their own lines. A multi-line annotation label is collapsed on the AST-driven emission path too, which previously spliced raw text and left a continuation carrying its original source indentation.
{- unambiguously opens a block comment.{-1} parsed before the nesting work and is now [E020], since {- is a comment opener per spec 1.3 whatever follows it. The diagnostic's fix text names the real remedy — write { -1 } with a separating space — and spec 1.9 now states the precedence rather than leaving it implicit.
Annotation comments are preserved in the AST, as the specification has always said (#1112). Spec 1.3 describes /* ... */ as "optional human-readable labels for bindings" that are "preserved in the AST"; they were in fact %ignored by the grammar — absent from the AST entirely, and silently deleted by vera fmt. A label on a function parameter or on the return slot is now carried on the FnDecl as a tuple aligned with params, so it belongs to a slot index rather than to a source line: an unlabelled slot holds None instead of being omitted, since collapsing the gaps would shift every later label onto the wrong parameter. That positional storage is what De Bruijn addressing requires and what the formatter's line-keyed inline store could never have provided — fn area(@Int /* width */, @Int /* height */ -> @Int) puts two labels on one line, and a dict[int, Comment] holds one. vera fmt re-emits labels from the AST, so formatting is now a fixed point over them rather than a deletion. Because annotation comments are %ignored and never reach the parse tree, parse() carries the scanned labels on the tree for transform() to attach, which keeps every AST-building path — check, verify, fmt, the LSP — consistent without threading a source argument through every transform() call site. A label is recognised only inside the signature's parentheses — the shared scanner now records each comment's parenthesis depth, so a comment written after the closing paren stays an ordinary trailing comment instead of being adopted as the return label and re-emitted inside the parens. Only the annotations the label walk actually consumes are retired from the comment stream, so a leading /* ... */ before a slot, or a second one behind an already-labelled slot, survives as an ordinary comment rather than being deleted. Labels in other positions remain accepted and ignored; spec 1.3 now states precisely which positions retain.
A dedicated E02x block for malformed comments (#1112). All three cases previously surfaced as a token-level complaint naming the wrong culprit, because the grammar only ever sees the wreckage a malformed comment leaves behind: an unterminated {- was reported as an unexpected {, an unterminated /* as an unexpected /, and a nested /* a /* b */ */ as an unexpected * at the trailing delimiter. They are now detected during the shared pre-lex scan that already knows where every comment begins and ends — [E020] unterminated block comment, [E021] unterminated annotation comment, and [E023] annotation comments do not nest, the last pointing at the inner /* the author expected to nest rather than at the wreckage.
Block comments nest, as the specification has always said (#1112). Spec 1.3 documents {- outer {- inner -} still outer -} as a single comment, but the grammar ignored block comments with a non-greedy regex that closes at the first-} — and a regular expression cannot match balanced delimiters at all, so nesting was impossible by construction. The identical text was therefore one comment to vera fmt, whose extractor counts depth (and whose suite asserted it), and a syntax error to vera check; the spec's own nested example did not parse, and SKILL.md's comment-syntax block carried a vera:skip-parse annotation for the same reason (now removed -- the gate flags it as stale once the example parses). Nesting is now resolved before the grammar sees the source by a single scanner in the new vera/lexical.py, which the formatter's extract_comments is re-based on so the two cannot drift apart again. Block comments are blanked to spaces with newlines preserved, so every line, column, and propagate_positions offset — and with them the formatter's span-based comment attachment — stays byte-faithful; an unterminated {- now reports at the opening delimiter instead of at end of input.
vera fmt keeps comments inside data, effect, and ability declarations (#1113). Comments immediately preceding constructors or operations now stay attached to those items instead of being hoisted after the enclosing declaration.
The concurrent await lowering resolves type aliases in its fused-handle classification (#1109, found probing #1095). A future bound through an alias-typed let (type F = Future<Result<String, String>>; let @F = async(Http.get(url)); await(@F.0)) or returned from a helper declared -> @F compiled and verified clean, fused its async (call-shape fusion ignores the binding type), but identity-lowered the await: the classifier matched the literal type Future<Result<String, String>> only, so the kind-4 handle wrapper was read as the Result ADT and every two-arm match took Err on a successful request — a silent wrong answer, no trap, exit 0. The [E602] skip spec §9.5.4 documented as the guard against exactly this mis-lower no longer fired once the v0.1.5 alias-payload work let the alias-typed let compile. The classifier now resolves aliases transitively (param-substituting, cycle-guarded) before the literal check — a new shared resolve_type_alias walk in vera/monomorphize.py that resolve_fn_type_alias is re-based on, so the fn-type and Future classifications cannot drift — covering the await slot arm, both declared-return registries (bare and module-qualified), and the #843apply_fn closure-return arm. Review (PR #1110) found the same literal-matching gap one level down — an alias INSIDE the future's payload (Future<R> with type R = Result<String, String>) — so the terminal check now canonicalizes type arguments recursively too, with a path-local cycle guard whose probe unwraps refinement layers (a cyclic payload alias like type A = Future<A> or type A = Future<{ @A | true }> conservatively fails classification rather than spinning; the checker rejects such cycles upstream with [E132], #1059); payload discrimination is unchanged (only the exact terminal Future<Result<String, String>> classifies). The spec §9.5.4 limitation sentence, the KNOWN_ISSUES row, and the ROADMAP Stage 20 row are removed; regression tests pin the async_await import and byte-exact Ok payloads for the alias-let, alias-fn-return, alias-chain, and payload-alias shapes, plus a genuinely-concurrent two-request overlap test through an alias chain.
The install section's language-server note names both routes — the source .[dev] install already includes the server, while the PyPI route needs the veralang[lsp] extra — instead of an ambiguous "the [lsp] extra shown above."
The install section's source-route explanation spans the full column on veralang.dev. The paragraph carried a max-width:56ch cap that left a two-sentence block crammed into the left half beside the full-width code blocks; the cap is removed so it fills the column like the blocks it sits between.
The release workflow's tag-existence probe treats a 404 as "tag absent" instead of capturing the error body. gh api prints the 404 JSON on stdout, so the first production run of the tag/Release job compared {"message":"Not Found"...} against the merge SHA, tripped the immutability guard, and failed after PyPI had already accepted the v0.1.5 files; the probe result is now shape-validated to a 40-hex SHA. The v0.1.5 tag and GitHub Release were completed manually from the run's registry-verified artifact per the RELEASING.md recovery runbook.