run-rs 0.6.31

Run a subset of Rust as an interpreted script
//! Generates `PathId` from `src/interpreter/path_names.txt`. One path per line, sorted.
//! An entry is a suffix, so `fs::read_to_string` also matches `std::fs::read_to_string`.

use std::collections::BTreeMap;
use std::path::Path;

pub fn read_paths(path: &Path) -> Vec<String> {
    let text = std::fs::read_to_string(path)
        .unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
    let mut rows: Vec<String> = Vec::new();
    for (index, line) in text.lines().enumerate() {
        let name = line.trim();
        if name.is_empty() {
            continue;
        }
        if let Some(previous) = rows.last()
            && previous.as_str() >= name
        {
            panic!(
                "{}:{}: `{name}` is out of order, the table is sorted and unique",
                path.display(),
                index + 1
            );
        }
        rows.push(name.to_string());
    }
    rows
}

/// `fs::read_to_string` -> `FsReadToString`. A constant spelled like a function gets a `Const` suffix.
pub fn variant_names(rows: &[String]) -> Vec<String> {
    let mut names: Vec<String> = rows.iter().map(|row| camel_path(row)).collect();
    let mut by_name: BTreeMap<String, Vec<usize>> = BTreeMap::new();
    for (index, name) in names.iter().enumerate() {
        by_name.entry(name.clone()).or_default().push(index);
    }
    for (name, indices) in by_name {
        if indices.len() == 1 {
            continue;
        }
        let mut constants = 0;
        for &index in &indices {
            if is_constant(&rows[index]) {
                names[index] = format!("{name}Const");
                constants += 1;
            }
        }
        if constants != indices.len() - 1 {
            let rows: Vec<&str> = indices.iter().map(|&i| rows[i].as_str()).collect();
            panic!("path table entries {rows:?} all name the variant `{name}`");
        }
    }
    names
}

fn is_constant(row: &str) -> bool {
    let last = row.rsplit("::").next().unwrap_or(row);
    !last.chars().any(char::is_lowercase)
}

fn camel_path(row: &str) -> String {
    let mut out = String::with_capacity(row.len());
    for segment in row.trim_start_matches("::").split("::") {
        let all_caps = !segment.chars().any(char::is_lowercase);
        for part in segment.split('_') {
            let mut chars = part.chars();
            let Some(first) = chars.next() else {
                continue;
            };
            out.extend(first.to_uppercase());
            if all_caps {
                out.push_str(&chars.as_str().to_lowercase());
            } else {
                out.push_str(chars.as_str());
            }
        }
    }
    out
}

pub fn generate(rows: &[String]) -> String {
    let names = variant_names(rows);
    let max_segments = rows
        .iter()
        .map(|row| row.split("::").count())
        .max()
        .unwrap_or(1);
    let mut out = String::new();
    out.push_str("// Generated by build.rs from src/interpreter/path_names.txt. Do not edit.\n\n");
    out.push_str("#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]\n");
    out.push_str("pub enum PathId {\n");
    for name in &names {
        out.push_str(&format!("    {name},\n"));
    }
    out.push_str("    /// A path the table does not list, a user item or a method reference.\n");
    out.push_str("    Other,\n}\n\n");

    out.push_str("const PATHS: &[&str] = &[\n");
    for row in rows {
        out.push_str(&format!("    {row:?},\n"));
    }
    out.push_str("];\n\n");
    out.push_str("const PATH_IDS: &[PathId] = &[\n");
    for name in &names {
        out.push_str(&format!("    PathId::{name},\n"));
    }
    out.push_str("];\n\n");
    out.push_str(&format!("const MAX_SEGMENTS: usize = {max_segments};\n\n"));

    out.push_str("impl PathId {\n");
    out.push_str("    /// The longest table entry that is a suffix of the path, or `Other`.\n");
    out.push_str("    pub fn resolve(segs: &[String]) -> PathId {\n");
    out.push_str("        let mut key = String::new();\n");
    out.push_str("        for count in (1..=segs.len().min(MAX_SEGMENTS)).rev() {\n");
    out.push_str("            key.clear();\n");
    out.push_str(
        "            for (index, seg) in segs[segs.len() - count..].iter().enumerate() {\n",
    );
    out.push_str("                if index > 0 {\n                    key.push_str(\"::\");\n                }\n");
    out.push_str("                key.push_str(seg);\n            }\n");
    out.push_str("            if let Ok(index) = PATHS.binary_search(&key.as_str()) {\n");
    out.push_str("                return PATH_IDS[index];\n            }\n        }\n");
    out.push_str("        PathId::Other\n    }\n\n");

    out.push_str("    /// The table spelling, `fs::read_to_string`, empty for `Other`.\n");
    out.push_str("    pub fn path(self) -> &'static str {\n");
    out.push_str("        PATHS.get(self as usize).copied().unwrap_or(\"\")\n    }\n\n");

    out.push_str("    /// The last segment, `read_to_string`.\n");
    out.push_str("    pub fn name(self) -> &'static str {\n");
    out.push_str("        let path = self.path();\n");
    out.push_str("        path.rsplit_once(\"::\").map_or(path, |(_, name)| name)\n    }\n\n");

    out.push_str("    /// The segment before the last, `fs`, empty for a bare name.\n");
    out.push_str("    pub fn namespace(self) -> &'static str {\n");
    out.push_str("        let path = self.path();\n");
    out.push_str("        let head = path.rsplit_once(\"::\").map_or(\"\", |(head, _)| head);\n");
    out.push_str(
        "        head.rsplit_once(\"::\").map_or(head, |(_, namespace)| namespace)\n    }\n}\n\n",
    );

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