@@ -28,6 +28,8 @@ import {
2828 InitResponseCapabilitiesExperimental ,
2929} from './model/paths' ;
3030import { moduleSearchPaths , TLAPS } from './paths' ;
31+ import { parseSpec } from './commands/parseModule' ;
32+ import { applyDCollection } from './diagnostic' ;
3133
3234export enum proofStateNames {
3335 proved = 'proved' ,
@@ -67,6 +69,7 @@ export class TlapsClient {
6769
6870 constructor (
6971 private context : vscode . ExtensionContext ,
72+ private diagnostic : vscode . DiagnosticCollection ,
7073 private currentProofStepDetailsListener : ( ( details : TlapsProofStepDetails ) => void ) ,
7174 private configChangedListener : ( ( configChanged : TlapsConfigChanged ) => void )
7275 ) {
@@ -118,16 +121,7 @@ export class TlapsClient {
118121 if ( ! this . client ) {
119122 return ;
120123 }
121- vscode . commands . executeCommand ( 'tlaplus.tlaps.check-step.lsp' ,
122- {
123- uri : te . document . uri . toString ( ) ,
124- version : te . document . version
125- } as VersionedTextDocumentIdentifier ,
126- {
127- start : te . selection . start ,
128- end : te . selection . end
129- } as Range ,
130- ) ;
124+ this . checkStep ( te ) ;
131125 }
132126 ) ) ;
133127 context . subscriptions . push ( vscode . workspace . onDidChangeConfiguration ( event => {
@@ -174,6 +168,56 @@ export class TlapsClient {
174168 this . tryStop ( ) ;
175169 }
176170
171+ // Runs SANY on the module before forwarding the proof check to TLAPS.
172+ //
173+ // TLAPS and SANY do not accept exactly the same language: TLAPS accepts some
174+ // specifications that SANY (the standard TLA+ front end) correctly rejects.
175+ // TLAPS accepts "raw TLA" (rTLA), which permits unrestrictive assertions
176+ // about behaviors that should be unassertable in TLA+, i.e. formulas that
177+ // are not insensitive to stuttering. Eliminating rTLA is the primary reason
178+ // for running SANY before TLAPS.
179+ // To preserve the invariant the TLA+ Toolbox used to enforce, we parse the
180+ // module with SANY first (reusing the implementation behind `tlaplus.parse`)
181+ // and only invoke TLAPS if the module is a valid TLA+ module.
182+ private async checkStep ( te : vscode . TextEditor ) {
183+ const document = te . document ;
184+ // Capture the selection before any `await`, as it may change meanwhile.
185+ const selection : Range = {
186+ start : te . selection . start ,
187+ end : te . selection . end ,
188+ } ;
189+ // Persist the buffer so SANY and TLAPS operate on the same content.
190+ if ( document . isDirty && ! await document . save ( ) ) {
191+ return ;
192+ }
193+ let sanyData ;
194+ try {
195+ sanyData = await parseSpec ( document . uri ) ;
196+ } catch ( err ) {
197+ vscode . window . showErrorMessage ( `Error parsing module with SANY: ${ err } ` ) ;
198+ return ;
199+ }
200+ applyDCollection ( sanyData . dCollection , this . diagnostic ) ;
201+ const hasErrors = sanyData . dCollection . getMessages ( ) . some (
202+ ( msg ) => msg . diagnostic . severity === vscode . DiagnosticSeverity . Error
203+ ) ;
204+ if ( hasErrors ) {
205+ // SANY's diagnostics already annotate the module (editor markers and
206+ // the Problems view), so don't invoke TLAPS on an invalid module.
207+ return ;
208+ }
209+ if ( ! this . client ) {
210+ return ;
211+ }
212+ vscode . commands . executeCommand ( 'tlaplus.tlaps.check-step.lsp' ,
213+ {
214+ uri : document . uri . toString ( ) ,
215+ version : document . version
216+ } as VersionedTextDocumentIdentifier ,
217+ selection ,
218+ ) ;
219+ }
220+
177221 private readConfig ( ) : boolean {
178222 const config = vscode . workspace . getConfiguration ( ) ;
179223 const configEnabled = config . get < boolean > ( 'tlaplus.tlaps.enabled' ) ;
0 commit comments