UNPKG

@informalsystems/quint

Version:

Core tool for the Quint specification language

340 lines 14.6 kB
"use strict"; /* ---------------------------------------------------------------------------------- * 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