Skip to content

Commit fcd03c5

Browse files
lemmyclaude
andcommitted
Run SANY before invoking TLAPS proof commands
TLAPS accepts "raw TLA" (rTLA) that SANY rejects: formulas that are not insensitive to stuttering, permitting unrestrictive assertions about behaviors that should be unassertable in TLA+. Without a SANY check, a user can obtain a passing TLAPS result for a module that is not valid TLA+. Parse the saved module with SANY first (reusing tlaplus.parse) and abort the proof check, surfacing diagnostics, when SANY reports errors. Closes Github issue #526 #526 [Feature][TLAPS] Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Markus Alexander Kuppe <github.com@lemmster.de>
1 parent acc095d commit fcd03c5

2 files changed

Lines changed: 68 additions & 10 deletions

File tree

src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
262262
);
263263
tlapsClient = new TlapsClient(
264264
context,
265+
diagnostic,
265266
details => currentProofStepWebviewViewProvider.showProofStepDetails(details),
266267
configChanged => currentProofStepWebviewViewProvider.considerConfigChanged(configChanged)
267268
);

src/tlaps.ts

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import {
2828
InitResponseCapabilitiesExperimental,
2929
} from './model/paths';
3030
import { moduleSearchPaths, TLAPS } from './paths';
31+
import { parseSpec } from './commands/parseModule';
32+
import { applyDCollection } from './diagnostic';
3133

3234
export 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,69 @@ 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+
// SANY itself failed to run (not a parse error). Let the user decide
198+
// whether to proceed without the SANY check; default to aborting.
199+
const continueLabel = 'Run TLAPS anyway';
200+
const choice = await vscode.window.showErrorMessage(
201+
`Error parsing module with SANY: ${err}`,
202+
continueLabel
203+
);
204+
if (choice === continueLabel) {
205+
this.runCheckStep(document, selection);
206+
}
207+
return;
208+
}
209+
applyDCollection(sanyData.dCollection, this.diagnostic);
210+
const hasErrors = sanyData.dCollection.getMessages().some(
211+
(msg) => msg.diagnostic.severity === vscode.DiagnosticSeverity.Error
212+
);
213+
if (hasErrors) {
214+
// SANY's diagnostics already annotate the module (editor markers and
215+
// the Problems view), so don't invoke TLAPS on an invalid module.
216+
return;
217+
}
218+
this.runCheckStep(document, selection);
219+
}
220+
221+
private runCheckStep(document: vscode.TextDocument, selection: Range) {
222+
if (!this.client) {
223+
return;
224+
}
225+
vscode.commands.executeCommand('tlaplus.tlaps.check-step.lsp',
226+
{
227+
uri: document.uri.toString(),
228+
version: document.version
229+
} as VersionedTextDocumentIdentifier,
230+
selection,
231+
);
232+
}
233+
177234
private readConfig(): boolean {
178235
const config = vscode.workspace.getConfiguration();
179236
const configEnabled = config.get<boolean>('tlaplus.tlaps.enabled');

0 commit comments

Comments
 (0)