#!/usr/bin/env node // Every OpenQASM program in the repository's conformance material and // examples, parsed by this grammar: none may hold an error or a missing node. // // node script/check-programs.mjs [--out DIR] [--list] // // The programs are every tracked .qasm file under wai/ (the conformance // artifacts, the workbench's examples, the editor corpus), the Studio's worked // examples, the sample programs the language service offers, every // here-document holding a program in the quantum command line's and service's // READMEs and smoke scripts, and every ```qasm block in wai/'s Markdown. With // --out, each is also written to DIR, numbered, for other checks to read. import { execFileSync, spawnSync } from 'node:child_process'; import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; const here = dirname(fileURLToPath(import.meta.url)); const grammar = resolve(here, '..'); const repo = resolve(grammar, '../../..'); const tracked = execFileSync('git', ['ls-files', 'wai'], { cwd: repo, encoding: 'utf8' }).split('\n').filter(Boolean); const read = (f) => readFileSync(join(repo, f), 'utf8'); const programs = []; const add = (name, text, broken = false) => programs.push({ name, text, broken }); // The intent set repairs some programs from a syntax error (its expected repairs // name a code from Q001 to Q005, the lexer's and the parser's): each must hold // that error here too, as the broken program below does. const intents = 'wai/quantum-conformance/intents'; const syntax = new Set(); if (tracked.includes(`${intents}/manifest.json`)) { for (const e of JSON.parse(read(`${intents}/manifest.json`)).cases ?? []) { if ((e.expect?.repairs ?? []).some((c) => /^Q00[1-5]$/.test(c))) syntax.add(`${intents}/${e.program}`); } } // The documentation runner's broken.qasm is broken on purpose too (a missing // `;`): the command line's README repairs it. syntax.add('wai/tools/quantum-docs/fixtures/broken.qasm'); for (const f of tracked.filter((f) => f.endsWith('.qasm'))) add(f, read(f), syntax.has(f)); const { EXAMPLES } = await import(pathToFileURL(join(repo, 'wai/quantum-studio/src/examples.js')).href); for (const e of EXAMPLES) add(`wai/quantum-studio/src/examples.js: ${e.id}`, e.job.qasm); // The language service's samples, as Rust string literals in its source. const service = 'wai/wai-quantum/src/engine/editor/mod.rs'; const samples = read(service).split('pub const SAMPLES')[1].split('\n];')[0]; for (const m of samples.matchAll(/\(\s*"([a-z]+)",\s*"[^"]*",\s*"((?:[^"\\]|\\.)*)",?\s*\)/g)) { add(`${service}: ${m[1]}`, JSON.parse(`"${m[2]}"`)); } const heredoc = /<<-?'?([A-Z_]+)'?\n([\s\S]*?)\n\1\n/g; for (const f of tracked.filter((f) => /^wai\/wai-quantum(-cli|-service)\/[^/]*\.(md|sh)$/.test(f))) { for (const m of read(f).matchAll(heredoc)) if (m[2].trimStart().startsWith('OPENQASM')) add(`${f}: here-document`, m[2]); } const fence = /```(?:qasm|openqasm)\n([\s\S]*?)```/g; for (const f of tracked.filter((f) => f.endsWith('.md'))) { for (const m of read(f).matchAll(fence)) add(`${f}: qasm block`, m[1]); } const dir = process.argv.includes('--out') ? resolve(process.argv[process.argv.indexOf('--out') + 1]) : mkdtempSync(join(tmpdir(), 'openqasm-programs-')); mkdirSync(dir, { recursive: true }); // A program with a missing `;` must fail, or a clean result would say nothing. programs.push({ name: 'a broken program (must fail)', text: 'OPENQASM 3.0;\nqubit q\nh q;\n', broken: true }); const scratch = mkdtempSync(join(tmpdir(), 'openqasm-broken-')); const files = programs.map((p, i) => { const f = join(p.broken ? scratch : dir, `${String(i).padStart(3, '0')}.qasm`); writeFileSync(f, p.text); return f; }); // One run for them all, so the parser is built once. const r = spawnSync('npx', ['--no-install', 'tree-sitter', 'parse', '--quiet', '--json-summary', ...files], { cwd: grammar, encoding: 'utf8', maxBuffer: 64 << 20 }); const out = r.stdout || ''; const at = out.search(/^\{/m); if (at < 0) { console.error(`the parser gave no summary (status ${r.status})\n${r.stderr}`); process.exit(1); } const summaries = JSON.parse(out.slice(at)).parse_summaries; if (summaries.length !== files.length) { console.error(`${files.length} programs, ${summaries.length} parsed`); process.exit(1); } let failed = 0; for (const s of summaries) { const i = files.indexOf(s.file); if (programs[i]?.broken) { if (s.successful) { console.error(`${programs[i].name} parsed without an error, but it is broken on purpose: the check cannot see errors`); process.exit(1); } continue; } if (s.successful) continue; failed += 1; const why = out.slice(0, at).split('\n').filter((l) => l.startsWith(s.file)).join('\n'); console.error(`FAILED ${i >= 0 ? programs[i].name : s.file}: ${why}`); } if (process.argv.includes('--list')) for (const p of programs) console.log(p.name); const real = programs.filter((p) => !p.broken); const kinds = new Set(real.map((p) => p.name.split(':')[0])); console.log(`${real.length - failed} of ${real.length} programs parse without an error (from ${kinds.size} files); ${programs.length - real.length} broken on purpose, each refused`); if (real.length < 20 || !real.some((p) => p.name.startsWith(service))) { console.error('fewer programs than the repository holds: the collection is broken'); process.exit(1); } process.exit(failed ? 1 : 0);