Skip to content

Commit 7a0c2fd

Browse files
authored
Model checking: deterministic config resolution + embedded .tla config picking
1 parent c89b9a8 commit 7a0c2fd

File tree

11 files changed

+461
-204
lines changed

11 files changed

+461
-204
lines changed

src/commands/checkModel.ts

Lines changed: 27 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { ChildProcess } from 'child_process';
2-
import { copyFile } from 'fs';
32
import * as path from 'path';
43
import * as vscode from 'vscode';
5-
import { Utils } from 'vscode-uri';
6-
import { LANG_TLAPLUS, LANG_TLAPLUS_CFG, exists, listFiles, replaceExtension } from '../common';
4+
import { LANG_TLAPLUS, LANG_TLAPLUS_CFG } from '../common';
75
import { applyDCollection } from '../diagnostic';
86
import { ModelCheckResult, ModelCheckResultSource, SpecFiles } from '../model/check';
97
import { ToolOutputChannel } from '../outputChannels';
@@ -15,6 +13,7 @@ import {
1513
} from '../panels/checkResultView';
1614
import { TlcModelCheckerStdoutParser } from '../parsers/tlc';
1715
import { runTlc, stopProcess } from '../tla2tools';
16+
import { ModelResolveMode, resolveModelForUri } from './modelResolver';
1817
import { TlcCoverageDecorationProvider } from '../tlcCoverage';
1918

2019
export const CMD_CHECK_MODEL_RUN = 'tlaplus.model.check.run';
@@ -27,8 +26,6 @@ export const CTX_TLC_RUNNING = 'tlaplus.tlc.isRunning';
2726
export const CTX_TLC_CAN_RUN_AGAIN = 'tlaplus.tlc.canRunAgain';
2827

2928
const CFG_CREATE_OUT_FILES = 'tlaplus.tlc.modelChecker.createOutFiles';
30-
const TEMPLATE_CFG_PATH = path.resolve(__dirname, '../tools/template.cfg');
31-
3229
let checkProcess: ChildProcess | undefined;
3330
let lastCheckFiles: SpecFiles | undefined;
3431
let coverageProvider: TlcCoverageDecorationProvider | undefined;
@@ -89,26 +86,14 @@ export async function checkModelCustom(
8986
return;
9087
}
9188
const doc = editor.document;
92-
if (doc.languageId !== LANG_TLAPLUS) {
93-
vscode.window.showWarningMessage('File in the active editor is not a .tla, it cannot be checked as a model');
89+
if (doc.languageId !== LANG_TLAPLUS && doc.languageId !== LANG_TLAPLUS_CFG) {
90+
vscode.window.showWarningMessage('File in the active editor is not a .tla or .cfg, it cannot be checked as a model');
9491
return;
9592
}
96-
// Accept .tla files here because TLC configs and TLA+ modules can share the same file:
97-
// https://github.com/tlaplus/vscode-tlaplus/issues/220
98-
const configFiles = await listFiles(path.dirname(doc.uri.fsPath),
99-
(fName) => fName.endsWith('.cfg') || fName.endsWith('.tla'));
100-
configFiles.sort();
101-
const cfgFileName = await vscode.window.showQuickPick(
102-
configFiles,
103-
{ canPickMany: false, placeHolder: 'Select a model config file', matchOnDetail: true }
104-
);
105-
if (!cfgFileName || cfgFileName.length === 0) {
93+
const specFiles = await getSpecFiles(doc.uri, true, true, 'customPick');
94+
if (!specFiles) {
10695
return;
10796
}
108-
const specFiles = new SpecFiles(
109-
doc.uri.fsPath,
110-
path.join(path.dirname(doc.uri.fsPath), cfgFileName)
111-
);
11297
await doCheckModel(specFiles, true, extContext, diagnostic, true);
11398
}
11499

@@ -186,7 +171,7 @@ export async function doCheckModel(
186171
): Promise<ModelCheckResult | undefined> {
187172
try {
188173
const procInfo = await runTlc(
189-
specFiles.tlaFilePath, path.basename(specFiles.cfgFilePath), showOptionsPrompt, extraOpts);
174+
specFiles.tlaFilePath, specFiles.cfgFilePath, showOptionsPrompt, extraOpts);
190175
if (procInfo === undefined) {
191176
// Command cancelled by user, make sure UI state is reset
192177
vscode.commands.executeCommand('setContext', CTX_TLC_CAN_RUN_AGAIN, !!lastCheckFiles);
@@ -203,7 +188,7 @@ export async function doCheckModel(
203188
updateStatusBarItem(false, lastCheckFiles);
204189
});
205190
if (showCheckResultView) {
206-
attachFileSaver(specFiles.tlaFilePath, checkProcess);
191+
attachFileSaver(specFiles, checkProcess);
207192
revealEmptyCheckResultView(extContext);
208193
}
209194
const resultHolder = new CheckResultHolder();
@@ -242,116 +227,47 @@ export async function doCheckModel(
242227
return undefined;
243228
}
244229

245-
function attachFileSaver(tlaFilePath: string, proc: ChildProcess) {
230+
function attachFileSaver(specFiles: SpecFiles, proc: ChildProcess) {
246231
const createOutFiles = vscode.workspace.getConfiguration().get<boolean>(CFG_CREATE_OUT_FILES);
247232
if (typeof(createOutFiles) === 'undefined' || createOutFiles) {
248-
const outFilePath = replaceExtension(tlaFilePath, 'out');
233+
const outFilePath = path.join(specFiles.outputDir, `${specFiles.modelName}.out`);
249234
saveStreamToFile(proc.stdout, outFilePath);
250235
}
251236
}
252237

253238
/**
254239
* Finds all files that needed to run model check.
255240
*/
256-
export async function getSpecFiles(fileUri: vscode.Uri, warn = true, prefix = 'MC'): Promise<SpecFiles | undefined> {
257-
let specFiles;
258-
259-
// a) Check the given input if it exists.
260-
specFiles = await checkSpecFiles(fileUri, false);
261-
if (specFiles) {
262-
return specFiles;
263-
}
264-
// b) Check alternatives:
265-
// Unless the given filePath already starts with 'MC', prepend MC to the name
266-
// and check if it exists. If yes, it becomes the spec file. If not, fall back
267-
// to the original file. The assumptions is that a user usually has Spec.tla
268-
// open in the editor and doesn't want to switch to MC.tla before model-checking.
269-
// TODO: Ideally, we wouldn't just check filenames here but check the parse result
270-
// TODO: if the module in MCSpec.tla actually extends the module in Spec.
271-
const b = Utils.basename(fileUri);
272-
if (!b.startsWith(prefix) && !b.endsWith('.cfg')) {
273-
const str = fileUri.toString();
274-
const n = str.substr(0, str.lastIndexOf(b)) + prefix + b;
275-
const filePath = vscode.Uri.parse(n).fsPath;
276-
specFiles = new SpecFiles(filePath, replaceExtension(filePath, 'cfg'));
277-
// Here, we make sure that the .cfg *and* the .tla exist.
278-
let canRun = true;
279-
canRun = await checkModelExists(specFiles.cfgFilePath, warn);
280-
canRun = canRun && await checkModuleExists(specFiles.tlaFilePath, warn);
281-
if (canRun) {
282-
return specFiles;
283-
}
284-
}
285-
// c) Deliberately trigger the warning dialog by checking the given input again
286-
// knowing that it doesn't exist.
287-
return await checkSpecFiles(fileUri, warn);
288-
}
289-
290-
async function checkSpecFiles(fileUri: vscode.Uri, warn = true): Promise<SpecFiles | undefined> {
291-
const filePath = fileUri.fsPath;
292-
let specFiles;
293-
let canRun = true;
294-
if (filePath.endsWith('.cfg')) {
295-
specFiles = new SpecFiles(replaceExtension(filePath, 'tla'), filePath);
296-
canRun = await checkModuleExists(specFiles.tlaFilePath, warn);
297-
} else if (filePath.endsWith('.tla')) {
298-
specFiles = new SpecFiles(filePath, replaceExtension(filePath, 'cfg'));
299-
canRun = await checkModelExists(specFiles.cfgFilePath, warn);
300-
}
301-
return canRun ? specFiles : undefined;
302-
}
303-
304-
async function checkModuleExists(modulePath: string, warn = true): Promise<boolean> {
305-
const moduleExists = await exists(modulePath);
306-
if (!moduleExists && warn) {
307-
const moduleFile = path.basename(modulePath);
308-
vscode.window.showWarningMessage(`Corresponding TLA+ module file ${moduleFile} doesn't exist.`);
309-
}
310-
return moduleExists;
311-
}
312-
313-
async function checkModelExists(cfgPath: string, warn = true): Promise<boolean> {
314-
const cfgExists = await exists(cfgPath);
315-
if (!cfgExists && warn) {
316-
showConfigAbsenceWarning(cfgPath);
241+
export async function getSpecFiles(
242+
fileUri: vscode.Uri,
243+
warn = true,
244+
interactive = true,
245+
mode: ModelResolveMode = 'adjacent'
246+
): Promise<SpecFiles | undefined> {
247+
const resolved = await resolveModelForUri(fileUri, warn, interactive, mode);
248+
if (!resolved) {
249+
return undefined;
317250
}
318-
return cfgExists;
251+
return new SpecFiles(
252+
resolved.tlaPath,
253+
resolved.cfgPath,
254+
resolved.modelName,
255+
resolved.outputDir
256+
);
319257
}
320258

321259
function updateStatusBarItem(active: boolean, specFiles: SpecFiles | undefined) {
322260
statusBarItem.text = 'TLC' + (active ?
323-
(specFiles === undefined ? '' : ' (' + specFiles.tlaFileName + '/' + specFiles.cfgFileName + ')')
261+
(specFiles === undefined ? '' : ` (Model: ${specFiles.modelName})`)
324262
+ ' $(gear~spin)' : '');
325263
statusBarItem.tooltip = 'TLA+ model checking' + (active ?
326-
(specFiles === undefined ? '' : ' of ' + specFiles.tlaFileName + '/' + specFiles.cfgFileName)
264+
(specFiles === undefined ? '' : ` of model ${specFiles.modelName}`)
327265
+ ' is running' : ' result');
328266
statusBarItem.command = CMD_CHECK_MODEL_DISPLAY;
329267
statusBarItem.show();
330268
vscode.commands.executeCommand('setContext', CTX_TLC_RUNNING, active);
331269
}
332270

333-
function showConfigAbsenceWarning(cfgPath: string) {
334-
const fileName = path.basename(cfgPath);
335-
const createOption = 'Create model file';
336-
vscode.window.showWarningMessage(`Model file ${fileName} doesn't exist. Cannot check model.`, createOption)
337-
.then((option) => {
338-
if (option === createOption) {
339-
createModelFile(cfgPath);
340-
}
341-
});
342-
}
343-
344-
async function createModelFile(cfgPath: string) {
345-
copyFile(TEMPLATE_CFG_PATH, cfgPath, (err) => {
346-
if (err) {
347-
console.warn(`Error creating config file: ${err}`);
348-
vscode.window.showWarningMessage(`Cannot create model file: ${err}`);
349-
return;
350-
}
351-
vscode.workspace.openTextDocument(cfgPath)
352-
.then(doc => vscode.window.showTextDocument(doc));
353-
});
354-
}
355271

356272
export function mapTlcOutputLine(line: string): string | undefined {
357273
if (line === '') {

0 commit comments

Comments
 (0)