sim-codec-classfile 0.1.0

Bounded, lossless Java Virtual Machine classfile codec for SIM
Documentation
#!/usr/bin/env python3
"""Generate the classfile opcode API and reference from opcode-manifest.tsv."""

from __future__ import annotations

import argparse
import pathlib
import re
import sys

ROOT = pathlib.Path(__file__).resolve().parent
MANIFEST = ROOT / "opcode-manifest.tsv"
RUST_OUTPUT = ROOT / "src" / "opcode_generated.rs"
DOC_OUTPUT = ROOT / "OPCODES.md"
FIELDS = ("value", "mnemonic", "identity", "operands", "width", "control", "constant_pool", "since", "until")


def read_manifest(path: pathlib.Path) -> list[dict[str, str]]:
    lines = [line for line in path.read_text(encoding="utf-8").splitlines() if line and not line.startswith("#")]
    if not lines or tuple(lines[0].split("\t")) != FIELDS:
        raise ValueError(f"{path}: expected tab-separated header {' '.join(FIELDS)}")
    rows: list[dict[str, str]] = []
    for line_number, line in enumerate(lines[1:], 2):
        values = line.split("\t")
        if len(values) != len(FIELDS):
            raise ValueError(f"{path}:{line_number}: expected {len(FIELDS)} fields, found {len(values)}")
        row = dict(zip(FIELDS, values))
        try:
            value = int(row["value"], 16)
        except ValueError as error:
            raise ValueError(f"{path}:{line_number}: invalid opcode value {row['value']!r}") from error
        if not 0 <= value <= 0xFF:
            raise ValueError(f"{path}:{line_number}: opcode value is outside one byte")
        row["numeric_value"] = str(value)
        rows.append(row)
    values = [int(row["numeric_value"]) for row in rows]
    if values != list(range(256)):
        raise ValueError(f"{path}: values must cover 0x00..0xff exactly once in ascending order")
    identities = [row["identity"] for row in rows]
    if len(identities) != len(set(identities)) or any(not re.fullmatch(r"[A-Z][A-Za-z0-9]*", item) for item in identities):
        raise ValueError(f"{path}: identities must be unique Rust UpperCamelCase identifiers")
    return rows


def rust_string(value: str) -> str:
    return '"' + value.replace('\\', '\\\\').replace('"', '\\"') + '"'


def rust_opcode_variant(row: dict[str, str]) -> str:
    documentation = f"JVM opcode `{row['mnemonic']}` (`{row['value']}`)."
    return (
        f"    #[doc = {rust_string(documentation)}] "
        f"{row['identity']} = {row['value']},"
    )


def generate_rust(rows: list[dict[str, str]]) -> str:
    variants = "\n".join(rust_opcode_variant(row) for row in rows)
    metadata = "\n".join(
        "    OpcodeMetadata { "
        f"opcode: Opcode::{row['identity']}, mnemonic: {rust_string(row['mnemonic'])}, "
        f"operands: {rust_string(row['operands'])}, width: {rust_string(row['width'])}, "
        f"control: {rust_string(row['control'])}, constant_pool: {rust_string(row['constant_pool'])}, "
        f"since: {rust_string(row['since'])}, until: {rust_string(row['until'])} "
        "},"
        for row in rows
    )
    return f'''// @generated by generate_opcodes.py from opcode-manifest.tsv; DO NOT EDIT.

/// Stable identity for every byte in the JVM opcode space.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[repr(u8)]
#[rustfmt::skip]
pub enum Opcode {{
{variants}
}}

/// Generated metadata for one JVM opcode byte.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct OpcodeMetadata {{
    /// Stable opcode identity.
    pub opcode: Opcode,
    /// JVM mnemonic, or a stable `reserved_*` name for an unassigned byte.
    pub mnemonic: &'static str,
    /// Declarative operand layout.
    pub operands: &'static str,
    /// Encoded width in bytes, or `variable`/`invalid`.
    pub width: &'static str,
    /// Control-flow category.
    pub control: &'static str,
    /// Admitted constant-pool category, or `none`.
    pub constant_pool: &'static str,
    /// Earliest admitted classfile version.
    pub since: &'static str,
    /// Last admitted classfile version, or `unbounded`.
    pub until: &'static str,
}}

/// Complete byte-indexed opcode metadata table.
#[rustfmt::skip]
pub static OPCODES: [OpcodeMetadata; 256] = [
{metadata}
];

impl Opcode {{
    /// Returns the identity for `byte`, including reserved identities.
    #[must_use]
    pub const fn from_byte(byte: u8) -> Self {{
        OPCODES[byte as usize].opcode
    }}

    /// Returns this opcode's generated metadata.
    #[must_use]
    pub const fn metadata(self) -> &'static OpcodeMetadata {{
        &OPCODES[self as usize]
    }}
}}
'''


def generate_docs(rows: list[dict[str, str]]) -> str:
    body = "\n".join(
        f"| `{row['value']}` | `{row['mnemonic']}` | `{row['operands']}` | {row['width']} | {row['control']} | `{row['constant_pool']}` | {row['since']} | {row['until']} |"
        for row in rows
    )
    return """<!-- @generated by generate_opcodes.py from opcode-manifest.tsv; DO NOT EDIT. -->
# JVM opcode manifest

This reference is generated from the crate's sole opcode inventory.

| Byte | Mnemonic | Operand layout | Width | Control | Constant pool | Since | Until |
|---|---|---|---:|---|---|---|---|
""" + body + "\n"


def find_parallel_inventories(root: pathlib.Path) -> list[str]:
    findings: list[str] = []
    known = {row["mnemonic"] for row in read_manifest(root / "opcode-manifest.tsv") if not row["mnemonic"].startswith("reserved_")}
    source_suffixes = {".rs", ".toml", ".json", ".csv", ".tsv", ".yaml", ".yml"}
    allowed = {root / "opcode-manifest.tsv", root / "src" / "opcode_generated.rs"}
    for path in sorted(item for item in root.rglob("*") if item.suffix in source_suffixes):
        if path in allowed or "target" in path.parts:
            continue
        text = path.read_text(encoding="utf-8")
        byte_facts = set(re.findall(r"(?<![A-Za-z0-9_])0x([0-9a-fA-F]{2})(?![A-Za-z0-9_])", text))
        mnemonic_facts = {name for name in known if re.search(rf'\b{re.escape(name)}\b', text)}
        opcode_named_bytes = any(
            len(set(re.findall(r"(?<![A-Za-z0-9_])0x([0-9a-fA-F]{2})(?![A-Za-z0-9_])", declaration))) >= 8
            for declaration in re.findall(
                r"(?is)(?:const|static)\s+[A-Za-z0-9_]*(?:opcode|instruction)[A-Za-z0-9_]*.*?=.*?;",
                text,
            )
        )
        if len(mnemonic_facts) >= 8 or opcode_named_bytes:
            findings.append(f"{path.relative_to(root)} contains a parallel opcode-like inventory ({len(byte_facts)} bytes, {len(mnemonic_facts)} mnemonics)")
    return findings


def write_or_check(path: pathlib.Path, expected: str, check: bool) -> bool:
    if check:
        return path.exists() and path.read_text(encoding="utf-8") == expected
    path.write_text(expected, encoding="utf-8")
    return True


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--check", action="store_true", help="fail unless generated outputs are current")
    parser.add_argument("--scan", action="store_true", help="also reject parallel Rust opcode inventories")
    args = parser.parse_args()
    try:
        rows = read_manifest(MANIFEST)
        current = write_or_check(RUST_OUTPUT, generate_rust(rows), args.check)
        current &= write_or_check(DOC_OUTPUT, generate_docs(rows), args.check)
        findings = find_parallel_inventories(ROOT) if args.scan else []
    except (OSError, ValueError) as error:
        print(error, file=sys.stderr)
        return 1
    if not current:
        print("generated opcode files are stale; run ./generate_opcodes.py", file=sys.stderr)
    for finding in findings:
        print(finding, file=sys.stderr)
    return 0 if current and not findings else 1


if __name__ == "__main__":
    raise SystemExit(main())