@informalsystems/quint
Version:
Core tool for the Quint specification language
340 lines • 14.6 kB
JavaScript
;
/* ----------------------------------------------------------------------------------
* Copyright 2025 Informal Systems
* Licensed under the Apache License, Version 2.0.
* See LICENSE in the project root for license information.
* --------------------------------------------------------------------------------- */
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.QuintRustWrapper = void 0;
const verbosity_1 = require("./verbosity");
const json_bigint_1 = __importDefault(require("json-bigint"));
const jsonHelper_1 = require("./jsonHelper");
const itf_1 = require("./itf");
const cli_progress_1 = require("cli-progress");
const path_1 = __importDefault(require("path"));
const os_1 = __importDefault(require("os"));
const chalk_1 = __importDefault(require("chalk"));
const readline_1 = __importDefault(require("readline"));
const child_process_1 = require("child_process");
const config_1 = require("./config");
const QUINT_EVALUATOR_VERSION = 'v0.4.0';
class QuintRustWrapper {
/**
* Constructor for QuintRustWrapper.
* @param {number} verbosityLevel - The level of verbosity for logging.
*/
constructor(verbosityLevel) {
this.verbosityLevel = verbosityLevel;
}
/**
* Simulate the parsed Quint model using the Rust evaluator
*
* @param {ParsedQuint} parsed - The parsed Quint model.
* @param {string} source - The source code of the Quint model.
* @param {QuintEx[]} witnesses - The witnesses for the simulation.
* @param {number} nruns - The number of runs for the simulation.
* @param {number} nsteps - The number of steps per run.
* @param {number} ntraces - The number of traces to store.
* @param {number} nthreads - The number of threads to use.
* @param {bigint} [seed] - Optional seed for reproducibility.
* @param {TraceHook} onTrace - A callback function to be called with trace information for each simulation run.
*
* @returns {Outcome} The outcome of the simulation.
* @throws Will throw an error if the Rust evaluator fails to launch or returns an error.
*/
async simulate(parsed, source, witnesses, nruns, nsteps, ntraces, nthreads, seed, onTrace) {
const exe = await getRustEvaluatorPath();
const args = ['simulate-from-stdin'];
const input = json_bigint_1.default.stringify({
parsed: parsed,
source: source,
witnesses: witnesses,
nruns: nruns,
nsteps: nsteps,
ntraces: ntraces,
nthreads: nthreads,
seed: seed,
}, jsonHelper_1.replacer);
(0, verbosity_1.debugLog)(this.verbosityLevel, 'Starting Rust evaluator synchronously');
// Create progress bar
const progressBar = new cli_progress_1.SingleBar({
clearOnComplete: true,
forceRedraw: true,
format: 'Running... [{bar}] {percentage}% | ETA: {eta}s | {value}/{total} samples | {speed} samples/s',
}, cli_progress_1.Presets.rect);
progressBar.start(nruns, 0, { speed: '0' });
const startTime = Date.now();
// Spawn the Rust evaluator in subprocess
const process = (0, child_process_1.spawn)(exe, args, {
shell: false,
stdio: ['pipe', 'pipe', 'pipe'], // stdin, stdout, stderr
});
// Write the input to stdin
process.stdin.write(input);
process.stdin.end();
// Handle error on launch
process.on('error', (err) => {
throw new Error(`Failed to launch Rust evaluator: ${err.message}`);
});
// Collect output from stdout
const stdout = readline_1.default.createInterface({
input: process.stdout,
terminal: false,
});
let output = '';
stdout.on('line', (line) => {
if (line.trimStart()[0] !== '{') {
console.log(line);
}
else {
output = line;
}
});
// Convert stderr to a readable stream that emits its output line by line
const stderr = readline_1.default.createInterface({
input: process.stderr,
terminal: false,
});
// Handle progress updates from stderr
stderr.on('line', (line) => {
try {
const progress = JSON.parse(line);
if (progress.type === 'progress') {
const elapsedSeconds = (Date.now() - startTime) / 1000;
const speed = Math.round(progress.current / elapsedSeconds);
progressBar.update(progress.current, { speed });
}
}
catch (_) {
// Ignore non-JSON lines
}
});
// Wait for process completion
const exitCode = await new Promise(resolve => {
process.on('close', resolve);
});
progressBar.stop();
if (exitCode !== 0) {
throw new Error(`Rust evaluator exited with code ${exitCode}`);
}
try {
const parsed = json_bigint_1.default.parse(output);
if (parsed.error) {
throw new Error(parsed.error);
}
// Convert traces to ITF
parsed.bestTraces = parsed.bestTraces.map((trace) => ({ ...trace, states: (0, itf_1.ofItf)(trace.states) }));
// Convert errors
parsed.errors = parsed.errors.map((err) => ({ ...err, reference: BigInt(err.reference) }));
// Call onTrace callback for each trace
if (onTrace && parsed.bestTraces.length > 0) {
const firstState = parsed.bestTraces[0].states[0];
const vars = [];
for (let i = 0; i < firstState.args.length; i += 2) {
vars.push(firstState.args[i].value);
}
parsed.bestTraces.forEach((trace, index) => {
const status = trace.result ? 'ok' : 'violation';
onTrace(index, status, vars, trace.states);
});
}
return parsed;
}
catch (error) {
throw new Error(`Failed to parse data from Rust evaluator: ${json_bigint_1.default.stringify(error)}`);
}
}
}
exports.QuintRustWrapper = QuintRustWrapper;
/**
* Get the path to the Rust evaluator executable.
* @param {string} version - The version of the evaluator.
* @returns {string} The path to the Quint evaluator executable.
* @throws Will throw an error if the evaluator is not found or cannot be downloaded.
*/
async function getRustEvaluatorPath(version = QUINT_EVALUATOR_VERSION) {
const path = require('path');
// Determine platform and architecture
const platform = os_1.default.platform();
const arch = os_1.default.arch();
// Map platform and architecture to asset name
let { assetName, executable } = inferAssetAndExecutableNames(platform, arch);
// Check if the evaluator is already downloaded
const evaluatorDir = (0, config_1.rustEvaluatorDir)(version);
const executablePath = path.join(evaluatorDir, executable);
if (await exists(executablePath)) {
return executablePath;
}
// Otherwise, fetch it from GitHub releases
return await fetchEvaluator(version, assetName, executable);
}
/**
* Fetch the latest version of the Quint evaluator from GitHub releases.
* @param {string} version - The version of the evaluator to fetch.
* @param {string} assetName - The name of the asset to download.
* @param {string} executable - The name of the executable file.
* @return {Promise<string>} - The path to the downloaded evaluator executable.
* @throws Will throw an error if the download fails or the asset format is unsupported.
*/
async function fetchEvaluator(version, assetName, executable) {
const path = require('path');
const { unlink, mkdir } = require('fs/promises');
console.log(chalk_1.default.gray(`Fetching Rust evaluator ${version}...`));
// Create a GitHub client
const client = new GitHubClient();
// Fetch the release from GitHub
const release = await client.fetchRelease(version);
const evaluatorDir = (0, config_1.rustEvaluatorDir)(version);
const executablePath = path.join(evaluatorDir, executable);
// Create the evaluator directory if it doesn't exist
await mkdir(evaluatorDir, { recursive: true });
// Download the asset from GitHub
const assetPath = await downloadGitHubAsset(client, release, assetName, evaluatorDir);
// Extract the asset
await extractAsset(executable, assetName, assetPath, evaluatorDir);
// Clean up the downloaded archive
await unlink(assetPath);
console.log(chalk_1.default.green(` [ok] `) + `Rust evaluator installed at: ${executablePath}\n`);
return executablePath;
}
/**
* Extract the downloaded asset to the evaluator directory.
* @param {string} executable - The name of the executable file.
* @param {string} assetName - The name of the asset to extract.
* @param {string} assetPath - The path to the downloaded asset.
* @param {string} evaluatorDir - The path to the evaluator directory.
* @throws Will throw an error if the asset format is unsupported.
*/
async function extractAsset(executable, assetName, assetPath, evaluatorDir) {
const util = require('util');
const exec = util.promisify(require('child_process').exec);
console.log(chalk_1.default.gray(` Extracting ${assetPath}...`));
const executablePath = path_1.default.join(evaluatorDir, executable);
if (assetName.endsWith('.tar.gz')) {
await exec(`tar -xzf ${assetPath} -C ${evaluatorDir} `);
await exec(`chmod +x ${executablePath}`);
}
else if (assetName.endsWith('.zip')) {
// For Windows, use a simple unzip command (requires unzip to be installed)
// You might want to use a JavaScript unzip library for better compatibility
const AdmZip = require('adm-zip');
const zip = new AdmZip(assetPath);
zip.extractAllTo(evaluatorDir, true);
}
else {
throw new Error(`Unsupported asset format: ${assetName}`);
}
}
/**
* Download a GitHub asset from a release.
* @param {GitHubClient} client - The GitHub client to use for downloading.
* @param {GitHubRelease} release - The GitHub release object.
* @param {string} assetName - The name of the asset to download.
* @param {string} evaluatorDir - The directory to save the downloaded asset.
* @returns {Promise<string>} - The path to the downloaded asset.
* @throws Will throw an error if the download fails or the asset is not found.
*/
async function downloadGitHubAsset(client, release, assetName, evaluatorDir) {
const path = require('path');
const version = release.tag_name;
const asset = release.assets.find(asset => asset.name === assetName);
if (!asset) {
throw new Error(`Asset ${assetName} not found in release ${version}`);
}
const assetPath = path.join(evaluatorDir, assetName);
if (await exists(assetPath)) {
console.log(chalk_1.default.gray(`File ${assetPath} already exists. Skipping download.`));
return assetPath;
}
// Download the asset
return await client.downloadAsset(asset, assetPath);
}
function inferAssetAndExecutableNames(platform, arch) {
let assetName = '';
let executable = 'quint_evaluator';
if (platform === 'darwin') {
// macOS
if (arch === 'arm64') {
assetName = 'quint_evaluator-aarch64-apple-darwin.tar.gz';
}
else if (arch === 'x64') {
assetName = 'quint_evaluator-x86_64-apple-darwin.tar.gz';
}
}
else if (platform === 'linux') {
if (arch === 'arm64') {
assetName = 'quint_evaluator-aarch64-unknown-linux-gnu.tar.gz';
}
else if (arch === 'x64') {
assetName = 'quint_evaluator-x86_64-unknown-linux-gnu.tar.gz';
}
}
else if (platform === 'win32') {
if (arch === 'x64') {
assetName = 'quint_evaluator-x86_64-pc-windows-msvc.zip';
executable = 'quint-evaluator.exe';
}
}
if (!assetName) {
throw new Error(`Unsupported platform or architecture: ${platform} ${arch} `);
}
return { assetName, executable };
}
async function exists(filePath) {
const { stat } = require('fs/promises');
return stat(filePath)
.then(() => true)
.catch(() => false);
}
class GitHubClient {
async fetch(url, accept) {
const options = {
redirect: 'follow',
follow: 10,
headers: {
'User-Agent': 'quint-evaluator-fetch',
Accept: accept,
},
};
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`Failed to fetch from GitHub: ${response.statusText}`);
}
return response;
}
async fetchRelease(version) {
const url = `https://api.github.com/repos/informalsystems/quint/releases`;
const response = await this.fetch(url, 'application/vnd.github.v3+json');
const releases = (await response.json());
const release = releases.find(release => release.tag_name === `evaluator/${version}`);
if (!release) {
throw new Error(`Release ${version} not found`);
}
return release;
}
async downloadAsset(asset, path) {
const fs = require('fs');
const { unlink } = require('fs/promises');
const { Readable } = require('stream');
const { finished } = require('stream/promises');
const fileStream = fs.createWriteStream(path, { mode: 0o755, flags: 'wx' });
// Download the asset
console.log(chalk_1.default.gray(` Downloading Rust evaluator from ${asset.url}...`));
try {
const response = await this.fetch(asset.url, 'application/octet-stream');
if (!response.ok) {
throw new Error(`Failed to download Rust evaluator: ${response.statusText}`);
}
await finished(Readable.fromWeb(response.body).pipe(fileStream));
}
catch (err) {
await unlink(path).catch(() => { });
throw err;
}
return path;
}
}
//# sourceMappingURL=quintRustWrapper.js.map