language server: the first check of a document in slices #171

Closed
opened 2026-10-08 01:20:48 +00:00 by orion · 6 comments
Collaborator

Context

LANGUAGE-SERVER.md section 15, third bullet: the first check of a document, and one after a change of its interface, run as one step before the next message is read - two to four seconds for the largest file of the compiler when it is opened. The workspace check already takes slices of 40 ms (ModuleRun, compiler/src/language-server/run.trb, beginModuleCheck/continueModuleCheck). Split from #10.

Goal

Opening a document, or changing it in a way that needs a check as a whole (no mapping, or a body whose inferred result changed: sections 5 and 7), no longer blocks the server. The check of the document, and of the importers an interface change brings with it, runs in slices through a ModuleRun between the messages; a request about a document whose check is pending waits for its last slice and is then answered exactly as today.

Design record points (pinned)

  • Section 15, third bullet: "a check of a document could be taken the same way, with the requests about it waiting for its last slice".
  • Sections 2 and 3: the server is a value with steps between messages; work that takes longer than a message runs in steps and can be cancelled ($/cancelRequest); a request that takes steps is a RequestJob.
  • Section 5: which documents a run checks (the changed one, importers of a changed interface, a document a request is about that no run checked yet). The set does not change; only when its checks run.
  • Section 8: diagnostics of open documents are published from the view's runs; the check of the workspace is in the background and yields to open documents.

Decisions this issue makes (inside the record, no owner question)

  • A change that arrives while the check of the same document is pending drops the pending check and starts one for the newest text. Nothing is published for a superseded text. This is the reading of "a keystroke checks what it changed" for the slow path, and it is recorded as a decision in section 8 or 15.
  • Diagnostics of the document are published when its last slice is done, not per slice (a half-checked file would publish an incomplete set that the next slice retracts).
  • Requests about a pending document wait; requests about other documents, initialize, shutdown, formatting, document symbols (syntax only), and everything that does not need the check are answered at once. A waiting request can be cancelled like any job: it is answered with the cancel error the existing jobs use.
  • The slice budget is the workspace check's (40 ms); the front (frontOf, parsing and name resolution) is not sliced by this issue, and the PR measures and states how much of the open time it still is.

Acceptance criteria

  • Opening a document whose check takes more than one slice does not block the server: a session test (in-memory, compiler/tests/language-server.test.trb, driving step) shows that a request about another document, sent after the open, is answered before the open's diagnostics are published.
  • A hover/definition/tokens request about the pending document is answered after its last slice, with the answer an unsliced check gives (differential test over the existing hover and definition fixtures).
  • A $/cancelRequest for a waiting request answers it with the cancel error and leaves the check running.
  • A change during a pending check drops it: no diagnostics of the superseded text are ever published, and the final diagnostics equal those of a check of the newest text. A change of the interface of a module brings its open importers into the same sliced run (their diagnostics published when their slices are done).
  • A body-only keystroke still takes the overlay path unchanged (section 7), with no extra step.
  • The existing session tests and tests/lsp/*.expected pass unchanged, tools/lsp.sh included (tier A).
  • The time to the first answer for the largest file of the compiler (compiler/src/semantics/checker/expression.trb) and the longest single step while it opens are measured on a binary built by the forge CI (not a gate) and written into section 15 / section 8; the third bullet of section 15 is removed.
  • If the docs change reaches skills/, it is regenerated.

Seed

No. Existing syntax; ModuleRun and the checker's beginModuleCheck family exist already; no native, no flag.

Size and commits

One PR, one or two Conventional Commits: feat(lsp): check a document in slices between the messages, and optionally docs(lsp): record the sliced check of a document. If the front turns out to need slicing too, that is a follow-up issue, not part of this PR. Tier A in the forge CI.

Implementer

opus / high (language server core: server.trb job handling, run.trb, analysis.trb).

Collisions

Touches server.trb, run.trb, analysis.trb, view.trb. #10 touches analysis.trb and view.trb: second to land rebases. #172 depends on this issue. No overlap with the running #6, #7, #9.

## Context [LANGUAGE-SERVER.md section 15, third bullet](https://git.torb.dev/torbscript/language/src/commit/068e52da9182672d56447e775c6705661eca5af2/docs/design/LANGUAGE-SERVER.md#L403): the first check of a document, and one after a change of its interface, run as one step before the next message is read - two to four seconds for the largest file of the compiler when it is opened. The workspace check already takes slices of 40 ms (`ModuleRun`, `compiler/src/language-server/run.trb`, `beginModuleCheck`/`continueModuleCheck`). Split from #10. ## Goal Opening a document, or changing it in a way that needs a check as a whole (no mapping, or a body whose inferred result changed: sections 5 and 7), no longer blocks the server. The check of the document, and of the importers an interface change brings with it, runs in slices through a `ModuleRun` between the messages; a request about a document whose check is pending waits for its last slice and is then answered exactly as today. ## Design record points (pinned) - Section 15, third bullet: "a check of a document could be taken the same way, with the requests about it waiting for its last slice". - Sections 2 and 3: the server is a value with steps between messages; work that takes longer than a message runs in steps and can be cancelled (`$/cancelRequest`); a request that takes steps is a `RequestJob`. - Section 5: which documents a run checks (the changed one, importers of a changed interface, a document a request is about that no run checked yet). The set does not change; only when its checks run. - Section 8: diagnostics of open documents are published from the view's runs; the check of the workspace is in the background and yields to open documents. ## Decisions this issue makes (inside the record, no owner question) - **A change that arrives while the check of the same document is pending drops the pending check and starts one for the newest text.** Nothing is published for a superseded text. This is the reading of "a keystroke checks what it changed" for the slow path, and it is recorded as a decision in section 8 or 15. - **Diagnostics of the document are published when its last slice is done**, not per slice (a half-checked file would publish an incomplete set that the next slice retracts). - **Requests about a pending document wait**; requests about other documents, `initialize`, `shutdown`, formatting, document symbols (syntax only), and everything that does not need the check are answered at once. A waiting request can be cancelled like any job: it is answered with the cancel error the existing jobs use. - The slice budget is the workspace check's (40 ms); the front (`frontOf`, parsing and name resolution) is not sliced by this issue, and the PR measures and states how much of the open time it still is. ## Acceptance criteria - [ ] Opening a document whose check takes more than one slice does not block the server: a session test (in-memory, `compiler/tests/language-server.test.trb`, driving `step`) shows that a request about another document, sent after the open, is answered before the open's diagnostics are published. - [ ] A hover/definition/tokens request about the pending document is answered after its last slice, with the answer an unsliced check gives (differential test over the existing hover and definition fixtures). - [ ] A `$/cancelRequest` for a waiting request answers it with the cancel error and leaves the check running. - [ ] A change during a pending check drops it: no diagnostics of the superseded text are ever published, and the final diagnostics equal those of a check of the newest text. A change of the interface of a module brings its open importers into the same sliced run (their diagnostics published when their slices are done). - [ ] A body-only keystroke still takes the overlay path unchanged (section 7), with no extra step. - [ ] The existing session tests and `tests/lsp/*.expected` pass unchanged, `tools/lsp.sh` included (tier A). - [ ] The time to the first answer for the largest file of the compiler (`compiler/src/semantics/checker/expression.trb`) and the longest single step while it opens are measured on a binary built by the forge CI (not a gate) and written into section 15 / section 8; the third bullet of section 15 is removed. - [ ] If the docs change reaches `skills/`, it is regenerated. ## Seed No. Existing syntax; `ModuleRun` and the checker's `beginModuleCheck` family exist already; no native, no flag. ## Size and commits One PR, one or two Conventional Commits: `feat(lsp): check a document in slices between the messages`, and optionally `docs(lsp): record the sliced check of a document`. If the front turns out to need slicing too, that is a follow-up issue, not part of this PR. Tier A in the forge CI. ## Implementer opus / high (language server core: server.trb job handling, run.trb, analysis.trb). ## Collisions Touches `server.trb`, `run.trb`, `analysis.trb`, `view.trb`. #10 touches `analysis.trb` and `view.trb`: second to land rebases. #172 depends on this issue. No overlap with the running #6, #7, #9.
Author
Collaborator

[loop:torbscript] refine: split from #10 and made ready (acceptance criteria, pinned records, seed: no). Implementer: opus/high. Collides with #10 on analysis.trb and view.trb (second to land rebases); #172 depends on this issue, next: implement

[loop:torbscript] refine: split from #10 and made ready (acceptance criteria, pinned records, seed: no). Implementer: opus/high. Collides with #10 on analysis.trb and view.trb (second to land rebases); #172 depends on this issue, next: implement
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-08T13:15:00Z

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-08T13:15:00Z
Author
Collaborator

[loop:torbscript] release implement: the previous worker was killed (gateway restart) before pushing a branch, next: implement

[loop:torbscript] release implement: the previous worker was killed (gateway restart) before pushing a branch, next: implement
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-08T15:30:00Z

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-08T15:30:00Z
Author
Collaborator

[loop:torbscript] claim review-fix by session:loop-torbscript-20261008 until 2026-10-08T18:40:00Z

[loop:torbscript] claim review-fix by session:loop-torbscript-20261008 until 2026-10-08T18:40:00Z
Author
Collaborator

[loop:torbscript] review-fix: rebased #188 onto main 4c6bbc40 (head 34db9908); conflicts in compiler/src/language-server/projects.trb (imports) and docs/design/LANGUAGE-SERVER.md section 15, server.trb merged cleanly with semantic tokens kept on the waiting path, and finishCheck now drops the kept semantic tokens answer like the overlay path. PR mergeable, forge CI green: https://git.torb.dev/torbscript/language/actions/runs/250, review requested from argus. next: review

[loop:torbscript] review-fix: rebased https://git.torb.dev/torbscript/language/pulls/188 onto main 4c6bbc40 (head 34db9908); conflicts in `compiler/src/language-server/projects.trb` (imports) and `docs/design/LANGUAGE-SERVER.md` section 15, `server.trb` merged cleanly with semantic tokens kept on the waiting path, and `finishCheck` now drops the kept semantic tokens answer like the overlay path. PR mergeable, forge CI green: https://git.torb.dev/torbscript/language/actions/runs/250, review requested from argus. next: review
argus closed this issue 2026-10-08 13:06:27 +00:00
Sign in to join this conversation.
No labels
loop
ready
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#171
No description provided.