Skip to content

Commit 868bed4

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 868bed4

3 files changed

Lines changed: 84 additions & 10 deletions

File tree

src/commands/parseModule.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ export async function transpilePlusCal(fileUri: vscode.Uri, token?: vscode.Cance
9494
*/
9595
export async function parseSpec(fileUri: vscode.Uri, token?: vscode.CancellationToken): Promise<SanyData> {
9696
throwIfCancelled(token);
97+
// SANY runs on a filesystem path, so only file:// URIs are supported.
98+
// Reject untitled or virtual (e.g. jarfile://) URIs with a clear error
99+
// instead of letting the underlying process fail obscurely on fsPath.
100+
if (fileUri.scheme !== 'file') {
101+
throw new Error(`Cannot parse '${fileUri.toString()}': only files saved on disk are supported.`);
102+
}
97103
const procInfo = await runSany(fileUri.fsPath);
98104
sanyOutChannel.bindTo(procInfo);
99105
const cancellationDisposable = registerCancellation(procInfo, token);

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: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ import {
2828
InitResponseCapabilitiesExperimental,
2929
} from './model/paths';
3030
import { moduleSearchPaths, TLAPS } from './paths';
31+
import { parseSpec } from './commands/parseModule';
32+
import { SanyData } from './parsers/sany';
33+
import { applyDCollection } from './diagnostic';
3134

3235
export 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

Comments
 (0)