run-rs 0.6.19

Run a subset of Rust as an interpreted script
//! Generates `BuiltinId` from `src/interpreter/method_names.txt`.
//! One name per line, sorted. A trailing `mut` marks a method that mutates its receiver.
//! So a typo in a bridge arm is a compile error.

use std::fmt::Write as _;
use std::path::Path;

pub struct MethodRow {
    pub name: String,
    pub mutates: bool,
}

pub fn read_table(path: &Path) -> Vec<MethodRow> {
    let text = std::fs::read_to_string(path)
        .unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
    let mut rows = Vec::new();
    for (index, line) in text.lines().enumerate() {
        let mut words = line.split_whitespace();
        let Some(name) = words.next() else {
            continue;
        };
        let mutates = match words.next() {
            None => false,
            Some("mut") => true,
            Some(other) => panic!(
                "{}:{}: unknown flag `{other}`, only `mut` is allowed",
                path.display(),
                index + 1
            ),
        };
        if let Some(previous) = rows.last().map(|row: &MethodRow| row.name.as_str())
            && previous >= name
        {
            panic!(
                "{}:{}: `{name}` is out of order, the table is sorted and unique",
                path.display(),
                index + 1
            );
        }
        rows.push(MethodRow {
            name: name.to_string(),
            mutates,
        });
    }
    rows
}

/// `split_first` -> `SplitFirst`
pub fn camel(name: &str) -> String {
    let mut out = String::with_capacity(name.len());
    for part in name.split('_') {
        let mut chars = part.chars();
        if let Some(first) = chars.next() {
            out.extend(first.to_uppercase());
            out.push_str(chars.as_str());
        }
    }
    out
}

pub fn generate(rows: &[MethodRow]) -> String {
    let mut out = String::new();
    out.push_str(
        "// Generated by build.rs from src/interpreter/method_names.txt. Do not edit.\n\n",
    );
    out.push_str("#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]\n");
    out.push_str("pub enum BuiltinId {\n");
    for row in rows {
        let _ = writeln!(out, "    {},", camel(&row.name));
    }
    out.push_str("    /// A name the table does not list, a user method or an error.\n");
    out.push_str("    Other,\n}\n\n");

    // Same order as the variants, so `NAMES[id as usize]` gives the name and a binary search
    // finds an id.
    // `Other` is last and has the empty name.
    out.push_str("const NAMES: &[&str] = &[\n");
    for row in rows {
        let _ = writeln!(out, "    {:?},", row.name);
    }
    out.push_str("    \"\",\n];\n\n");
    out.push_str("const IDS: &[BuiltinId] = &[\n");
    for row in rows {
        let _ = writeln!(out, "    BuiltinId::{},", camel(&row.name));
    }
    out.push_str("];\n\n");

    out.push_str("impl BuiltinId {\n");
    out.push_str("    pub fn resolve(name: &str) -> BuiltinId {\n");
    out.push_str("        match IDS.binary_search_by(|id| NAMES[*id as usize].cmp(name)) {\n");
    out.push_str("            Ok(index) => IDS[index],\n");
    out.push_str("            Err(_) => BuiltinId::Other,\n        }\n    }\n\n");

    out.push_str("    pub fn name(self) -> &'static str {\n");
    out.push_str("        NAMES[self as usize]\n    }\n\n");

    out.push_str("    /// Whether the method mutates its receiver in place.\n");
    out.push_str("    pub fn mutates(self) -> bool {\n        matches!(\n            self,\n");
    let mutating: Vec<String> = rows
        .iter()
        .filter(|row| row.mutates)
        .map(|row| format!("BuiltinId::{}", camel(&row.name)))
        .collect();
    let _ = writeln!(out, "            {}", mutating.join("\n                | "));
    out.push_str("        )\n    }\n}\n\n");

    out.push_str("impl std::fmt::Display for BuiltinId {\n");
    out.push_str("    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n");
    out.push_str("        f.write_str(self.name())\n    }\n}\n");
    out
}