xqvm 0.2.1

X-Quadratic Virtual Machine — bytecode interpreter for the XQuad Toolchain
Documentation
// Copyright (C) 2026 Postquant Labs Incorporated
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: AGPL-3.0-or-later

//! Build-time parity: reads `conformance/opcodes.yaml` and emits a const
//! opcode table that the crate compares against the `opcodes!` x-macro in
//! `src/bytecode/types/parity.rs`. Any mismatch becomes a compile error.
//!
//! Build scripts run at compile time with no caller to propagate errors
//! to, so panicking on failure is the correct behaviour. The workspace's
//! `clippy::panic` and `clippy::expect_used` lints are allowed here for
//! that reason.

#![expect(
    clippy::expect_used,
    clippy::panic,
    reason = "build scripts halt the compilation on failure; panics are the idiomatic exit path"
)]

use std::env;
use std::fmt::Write as _;
use std::fs;
use std::path::{Path, PathBuf};

#[derive(serde::Deserialize)]
struct Root {
    opcodes: Vec<Op>,
}

#[derive(serde::Deserialize)]
struct Op {
    code: u8,
    mnemonic: String,
}

fn main() {
    let manifest_dir = PathBuf::from(
        env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set by cargo"),
    );

    // `xqvm/opcodes.yaml` is a symlink to `../conformance/opcodes.yaml`
    // in the workspace (keeping the conformance copy authoritative),
    // but cargo follows the symlink when packaging so the published
    // tarball ships a real file at `xqvm/opcodes.yaml`. Read the
    // symlinked path so both the workspace build and the packaged
    // build resolve identically.
    let yaml_path = manifest_dir.join("opcodes.yaml");

    println!("cargo:rerun-if-changed={}", yaml_path.display());

    let raw = fs::read_to_string(&yaml_path)
        .unwrap_or_else(|e| panic!("failed to read {}: {e}", yaml_path.display()));
    let parsed: Root = serde_yaml_ng::from_str(&raw)
        .unwrap_or_else(|e| panic!("failed to parse {}: {e}", yaml_path.display()));

    let out_dir = env::var_os("OUT_DIR").expect("OUT_DIR must be set by cargo");
    let dest = Path::new(&out_dir).join("opcodes_yaml.rs");

    let mut code = String::new();
    code.push_str("// Auto-generated by build.rs from conformance/opcodes.yaml.\n");
    code.push_str(
        "// Do not edit; regenerate by touching opcodes.yaml and re-running `cargo build`.\n\n",
    );
    code.push_str("const YAML_OPCODES: &[(u8, &str)] = &[\n");
    for op in &parsed.opcodes {
        writeln!(code, "    ({:#04X}, \"{}\"),", op.code, op.mnemonic)
            .expect("writing to a String cannot fail");
    }
    code.push_str("];\n");

    fs::write(&dest, code).unwrap_or_else(|e| panic!("failed to write {}: {e}", dest.display()));
}