@@ -28,6 +28,9 @@ import {
2828 InitResponseCapabilitiesExperimental ,
2929} from './model/paths' ;
3030import { moduleSearchPaths , TLAPS } from './paths' ;
31+ import { parseSpec } from './commands/parseModule' ;
32+ import { SanyData } from './parsers/sany' ;
33+ import { applyDCollection } from './diagnostic' ;
3134
3235export enum proofStateNames {
3336 proved = 'proved' ,
@@ -67,6 +70,7 @@ export class TlapsClient {
6770
6871 constructor (
6972 private context : vscode . ExtensionContext ,
73+ private diagnostic : vscode . DiagnosticCollection ,
7074 private currentProofStepDetailsListener : ( ( details : TlapsProofStepDetails ) => void ) ,
7175 private configChangedListener : ( ( configChanged : TlapsConfigChanged ) => void )
7276 ) {
@@ -118,16 +122,9 @@ export class TlapsClient {
118122 if ( ! this . client ) {
119123 return ;
120124 }
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- ) ;
125+ this . checkStep ( te ) . catch ( ( err ) => {
126+ vscode . window . showErrorMessage ( `TLAPS proof check failed: ${ err } ` ) ;
127+ } ) ;
131128 }
132129 ) ) ;
133130 context . subscriptions . push ( vscode . workspace . onDidChangeConfiguration ( event => {
@@ -174,6 +171,76 @@ export class TlapsClient {
174171 this . tryStop ( ) ;
175172 }
176173
174+ // Runs SANY on the module before forwarding the proof check to TLAPS.
175+ //
176+ // TLAPS and SANY do not accept exactly the same language: TLAPS accepts some
177+ // specifications that SANY (the standard TLA+ front end) correctly rejects.
178+ // TLAPS accepts "raw TLA" (rTLA), which permits unrestrictive assertions
179+ // about behaviors that should be unassertable in TLA+, i.e. formulas that
180+ // are not insensitive to stuttering. Eliminating rTLA is the primary reason
181+ // for running SANY before TLAPS.
182+ // To preserve the invariant the TLA+ Toolbox used to enforce, we parse the
183+ // module with SANY first (reusing the implementation behind `tlaplus.parse`)
184+ // and only invoke TLAPS if the module is a valid TLA+ module.
185+ private async checkStep ( te : vscode . TextEditor ) {
186+ const document = te . document ;
187+ // SANY and the TLAPS LSP expect file:// documents.
188+ if ( document . uri . scheme !== 'file' ) {
189+ vscode . window . showWarningMessage (
190+ 'TLAPS proof checking is only available for TLA+ files saved on disk.'
191+ ) ;
192+ return ;
193+ }
194+ // Capture the selection before any `await`, as it may change meanwhile.
195+ const selection : Range = {
196+ start : te . selection . start ,
197+ end : te . selection . end ,
198+ } ;
199+ // Persist the buffer so SANY and TLAPS operate on the same content.
200+ if ( document . isDirty && ! await document . save ( ) ) {
201+ return ;
202+ }
203+ let sanyData : SanyData ;
204+ try {
205+ sanyData = await parseSpec ( document . uri ) ;
206+ } catch ( err ) {
207+ // SANY itself failed to run (not a parse error). Let the user decide
208+ // whether to proceed without the SANY check; default to aborting.
209+ const continueLabel = 'Run TLAPS anyway' ;
210+ const choice = await vscode . window . showErrorMessage (
211+ `Error parsing module with SANY: ${ err } ` ,
212+ continueLabel
213+ ) ;
214+ if ( choice === continueLabel ) {
215+ this . runCheckStep ( document , selection ) ;
216+ }
217+ return ;
218+ }
219+ applyDCollection ( sanyData . dCollection , this . diagnostic ) ;
220+ const hasErrors = sanyData . dCollection . getMessages ( ) . some (
221+ ( msg ) => msg . diagnostic . severity === vscode . DiagnosticSeverity . Error
222+ ) ;
223+ if ( hasErrors ) {
224+ // SANY's diagnostics already annotate the module (editor markers and
225+ // the Problems view), so don't invoke TLAPS on an invalid module.
226+ return ;
227+ }
228+ this . runCheckStep ( document , selection ) ;
229+ }
230+
231+ private runCheckStep ( document : vscode . TextDocument , selection : Range ) {
232+ if ( ! this . client ) {
233+ return ;
234+ }
235+ vscode . commands . executeCommand ( 'tlaplus.tlaps.check-step.lsp' ,
236+ {
237+ uri : document . uri . toString ( ) ,
238+ version : document . version
239+ } as VersionedTextDocumentIdentifier ,
240+ selection ,
241+ ) ;
242+ }
243+
177244 private readConfig ( ) : boolean {
178245 const config = vscode . workspace . getConfiguration ( ) ;
179246 const configEnabled = config . get < boolean > ( 'tlaplus.tlaps.enabled' ) ;
0 commit comments