use std::fs;
use std::path::Path;
use std::process;
use harn_parser::operators::{Associativity, Precedence};
const TABLE_BEGIN: &str = "<!-- operator-precedence:begin -->";
const TABLE_END: &str = "<!-- operator-precedence:end -->";
pub(crate) fn run(markdown_paths: &[String], tree_sitter_path: &str, check_only: bool) {
let mut outputs = Vec::with_capacity(markdown_paths.len() + 1);
for markdown in markdown_paths {
let path = Path::new(markdown);
let current = fs::read_to_string(path).unwrap_or_else(|error| {
eprintln!("error: cannot read {}: {error}", path.display());
process::exit(1);
});
let generated = replace_table(¤t).unwrap_or_else(|| {
eprintln!(
"error: {} has no `{TABLE_BEGIN}` ... `{TABLE_END}` region",
path.display()
);
process::exit(1);
});
outputs.push((path, generated));
}
outputs.push((Path::new(tree_sitter_path), tree_sitter_constants()));
for (path, generated) in outputs {
write_or_check(path, &generated, check_only);
}
}
fn write_or_check(path: &Path, generated: &str, check_only: bool) {
let existing = fs::read_to_string(path).unwrap_or_default();
if existing.replace("\r\n", "\n") == generated {
return;
}
if check_only {
eprintln!(
"error: {} is stale relative to harn_parser::operators::Precedence.",
path.display()
);
eprintln!("hint: run `make gen-operator-precedence` to regenerate.");
process::exit(1);
}
if let Err(error) = fs::write(path, generated) {
eprintln!("error: cannot write {}: {error}", path.display());
process::exit(1);
}
println!("wrote {}", path.display());
}
fn replace_table(spec: &str) -> Option<String> {
let start = spec.find(TABLE_BEGIN)? + TABLE_BEGIN.len();
let end = start + spec.get(start..)?.find(TABLE_END)?;
let mut out = String::with_capacity(spec.len());
out.push_str(spec.get(..start)?);
out.push('\n');
out.push_str(&precedence_table());
out.push_str(spec.get(end..)?);
Some(out)
}
fn precedence_table() -> String {
let mut table = String::from(
"| Precedence | Operators | Associativity | Description |\n|---|---|---|---|\n",
);
for group in Precedence::ALL {
let operators = group
.spellings()
.iter()
.map(|spelling| format!("`{}`", spelling.replace('|', "\\|")))
.collect::<Vec<_>>()
.join(" ");
let associativity_label = match group.associativity() {
Associativity::Left => "Left",
Associativity::Right => "Right",
Associativity::None => "None",
};
table.push_str(&format!(
"| {} | {operators} | {associativity_label} | {} |\n",
group.level(),
group.description()
));
}
table
}
fn tree_sitter_constants() -> String {
let mut out = String::from(
"// Generated by `make gen-operator-precedence` from\n\
// crates/harn-parser/src/operators.rs. Do not edit by hand.\n\
//\n\
// Expression precedence groups, loosest first. A larger number binds tighter.\n\
module.exports = Object.freeze({\n",
);
for group in Precedence::ALL {
out.push_str(&format!(" {}: {},\n", group.name(), group.level()));
}
out.push_str("});\n");
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn table_lists_every_group_loosest_first() {
let table = precedence_table();
let rows: Vec<&str> = table.lines().skip(2).collect();
assert_eq!(rows.len(), Precedence::ALL.len());
assert!(rows[0].starts_with("| 1 | `\\|>` | Left | Pipe |"));
assert!(rows[6].contains("`??`"));
assert!(rows[7].contains("`..` `..=` | None | Range formation"));
}
#[test]
fn replace_table_requires_markers() {
assert_eq!(replace_table("no markers"), None);
let spec = format!("intro\n{TABLE_BEGIN}\nstale\n{TABLE_END}\noutro\n");
let replaced = replace_table(&spec).unwrap();
assert!(replaced.contains("| Range formation |"));
assert!(!replaced.contains("stale"));
assert!(replaced.ends_with(&format!("{TABLE_END}\noutro\n")));
}
}