Skip to content

fix: fail on //@ annotations that lsc does not read - #239

Open
Sequela02 wants to merge 1 commit into
midspiral:mainfrom
Sequela02:fix/unread-annotations
Open

Sequela02 wants to merge 1 commit into
midspiral:mainfrom
Sequela02:fix/unread-annotations

Conversation

@Sequela02

Copy link
Copy Markdown
Contributor

Problem

A //@ comment that lsc doesn't read is dropped without a word (#238). When it states a postcondition, a false contract passes: lsc check exits 0 with 0 verified, 0 errors. It happens when the line is malformed (//@ensures, extra spaces, a tab, no expression, a misspelled keyword) and when a well-formed annotation sits where no reader looks (above an arrow with a block body, after an unrelated statement, a requires inside a loop body).

Fix

lsc now fails on a //@ comment it ignores, as discussed in the issue, with the limits under "Not covered":

ERROR: f.ts:2: lsc does not read this annotation: //@ensures \result > x
  • Contract annotations (requires, ensures, contract, invariant, decreases, done_with, type) are recorded where their value is used, not where they are parsed, so an annotation that is parsed and then dropped also counts as unread: a block-body arrow's parent annotations, a loop's requires, a second decreases. Two helpers, annotExprs and firstAnnotExpr, replace the filter(...).map(a => a.expr) lines at those sites and record the read.
  • Every //@ comment in the file must be a contract annotation that was read, or one of the other directives in SPEC §2.1 followed by a space or the end of the line. Option lines are recognized by config.ts's own grammar through a new isOptionDirective, so //@ option … and //@<tab>safe-slice keep working.
  • Left alone: declarations lsc leaves out on purpose (functions outside the //@ verify selection, extern functions, skipped statements, consts, classes and methods) are not checked for placement, so a contract in an unselected function stays fine; a typo there, like //@verify, is still caught. TypeScript's //@ts-… comments are not ours.

The check runs at the end of extractModule and reuses fileCommentLines from config.ts, which now also yields each comment's text. All failures are collected into one error, so softening it to a warning is a change in one place. SPEC §2.1 gets one paragraph describing the rule.

Not covered

Directives other than contract annotations are checked by keyword only. A correctly spelled bare directive in a place nothing reads (//@ pure above a statement), or one with extra text (//@ pure extra), still passes. Covering those would mean every reader recording what it matched, which could be a follow-up. The list of directive keywords next to KEYWORDS has to be kept in step with the readers.

Reading annotations above an arrow with a block body, as for an expression body, would change behavior, so it isn't part of this PR. That case is now an error instead of a silent pass.

What breaks

The check found annotations that were already being ignored. In this repository, four example lines: //@ type i nat and //@ type j nat inside loop bodies in nullableArray.ts, //@ type qHead nat in toposort.ts, and //@ type T with no type in readonlyArray.ts. They are removed; the generated .dfy.gen, .dfy and Lean files are byte-identical.

I ran lsc extract on the 82 LemmaScript-files.txt entries of the 30 case-studies-dafny matrix entries, each cloned at the branch ci.yml uses. On f245e98 none fails. With this change four files do, and in each the generated Dafny confirms the annotation is ignored today:

Case study Line Today
xyflow-lemmascript (lemmascript) packages/system/src/utils/general.ts 150: //@ ensures \result >= 0 above export const getOverlappingArea = (…) => { … } No getOverlappingArea_ensures lemma is generated; the postcondition is never checked
github-star-checker-lemmascript src/domain.ts 113, 155, 184, 238: //@ type idx nat and //@ type i nat in loop bodies var idx := 0;
rallly-lemmascript (lemmascript) apps/web/src/features/poll/scoring.ts 69: //@ type i nat in a loop body var i := 0;
collab-todo-lemmascript src/domain.ts 537 and 1134: //@ __mapFromArray Not a directive any reader matches

The case-studies-dafny jobs for these four will fail on this PR until those lines are fixed. I can open the case-study changes with the staging branches ci.yml describes.

//@ type inside a loop body shows up in three examples and two case studies, so people seem to expect it to work there. Should loops read it? That would be a separate change.

Regression

tools/tests/unread-annotations.test.ts: twelve unread forms and placements fail extraction with their line, including the brownfield //@verify typo and two arrows sharing one const statement; seven controls still extract, two of them asserting the contract arrives. On f245e98 the twelve fail and the seven pass.

Validation

On Ubuntu 24.04 (WSL), Node 24.15, Dafny 4.11.0, against f245e98:

  • npm run typecheck, (cd tools && npm run typecheck) and npm run typecheck:examples: pass.
  • tools/test-fixtures.sh: pass. The unit suite is 333 passed, 0 failed (314 on f245e98, plus the 19 new tests).
  • ./regen-dafny.sh --no-verify and ./regen-lean.sh: pass, no generated-file drift.
  • lsc check --backend=dafny on all 72 examples/*.ts: 0 failures.
  • The issue's ten cases: the seven silent ones now fail with their line; the three that already worked still reach Dafny and fail on the false postcondition.
  • Not run: lake build. The Lean inputs are byte-identical to f245e98, whose CI passed.

Compatibility

No new option, syntax or dependency. A file whose //@ comments are all read extracts exactly as before. A file with an ignored one now fails instead of passing silently, which is the point of the change.

Closes #238.

A //@ comment that lsc didn't read was dropped without a word, so a
false postcondition that was misspelled or misplaced passed with
"0 verified, 0 errors" (midspiral#238).

Extraction now records where each contract annotation's value is used
and ends with one check over the file's comments: a malformed or unknown
//@ comment, or a contract annotation that nothing used, is an error
naming the file, line and text. Declarations left out on purpose
(unselected, extern or skip) are exempt from the placement check, and
TypeScript's //@ts- comments are left alone. Option lines are recognized
by config's own grammar.

Four example lines that were never read are removed; the generated
Dafny and Lean files are unchanged.
@Sequela02

Copy link
Copy Markdown
Contributor Author

CI as expected: the four case-studies-dafny failures are the ones listed above.

case-studies-lean (pi-lemmascript) failed in setup-lean with "No space left on device", before lsc ran. Locally, lsc gen --backend=lean over each Lean case study's manifest (colorwheel, node-casbin, clear-split, pi), at the branches ci.yml uses, writes byte-identical files with this branch and with f245e98, matching the committed ones. The job passed on main at f245e98, so a rerun should clear it.

namin added a commit to midspiral/xyflow-lemmascript that referenced this pull request Oct 31, 2026
namin added a commit to midspiral/github-star-checker-lemmascript that referenced this pull request Oct 31, 2026
namin added a commit to midspiral/rallly-lemmascript that referenced this pull request Oct 31, 2026
namin added a commit to midspiral/collab-todo-lemmascript that referenced this pull request Oct 31, 2026
@namin
namin self-requested a review October 31, 2026 02:11

@namin namin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great. Cool that it found some ignored annotations in the case studies too!

Comment thread tools/src/extract.ts
const read = parseAnnotation(text)
? _readLines.has(line) || _skippedSpans.some(([from, to]) => from <= line && line <= to)
: DIRECTIVES.includes(keyword ?? "") || isOptionDirective(text);
if (!read) errors.push(`${sourceFile.getBaseName()}:${line}: lsc does not read this annotation: ${text}`);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer the relative file path than just the base

Comment thread tools/src/extract.ts
: DIRECTIVES.includes(keyword ?? "") || isOptionDirective(text);
if (!read) errors.push(`${sourceFile.getBaseName()}:${line}: lsc does not read this annotation: ${text}`);
}
if (errors.length > 0) throw new Error(errors.join("\n"));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now that we've updated the case studies, should we make this a warning? or else there should be a flag to make it a warning. i am concerned that some code bases might have further conventions for //@

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Malformed or misplaced //@ annotations are ignored without a word, so a false contract passes

2 participants