use std::io::{self, BufRead, Write as _};
use std::path::Path;
use miette::{Context, IntoDiagnostic, Result};
use panproto_core::gat::{self, Term};
use super::helpers::load_json;
fn parse_source(source: &str) -> Result<panproto_expr::Expr> {
let tokens =
panproto_expr_parser::tokenize(source).map_err(|e| miette::miette!("lex error: {e}"))?;
panproto_expr_parser::parse(&tokens).map_err(|errs| {
let msgs: Vec<String> = errs.iter().map(ToString::to_string).collect();
miette::miette!("parse error(s):\n{}", msgs.join("\n"))
})
}
pub fn cmd_expr_parse(source: &str, verbose: bool) -> Result<()> {
if verbose {
eprintln!("Parsing: {source}");
}
let expr = parse_source(source)?;
println!("{expr:#?}");
Ok(())
}
pub fn cmd_expr_eval_source(source: &str, verbose: bool) -> Result<()> {
if verbose {
eprintln!("Parsing: {source}");
}
let expr = parse_source(source)?;
if verbose {
eprintln!("AST: {expr:?}");
}
let config = panproto_expr::EvalConfig::default();
let env = panproto_expr::Env::new();
let result = panproto_expr::eval(&expr, &env, &config)
.map_err(|e| miette::miette!("eval error: {e}"))?;
let json = serde_json::to_string_pretty(&result)
.into_diagnostic()
.wrap_err("failed to serialize result")?;
println!("{json}");
Ok(())
}
pub fn cmd_expr_fmt(source: &str, verbose: bool) -> Result<()> {
if verbose {
eprintln!("Parsing: {source}");
}
let expr = parse_source(source)?;
let formatted = panproto_expr_parser::pretty_print(&expr);
println!("{formatted}");
Ok(())
}
pub fn cmd_expr_check_source(source: &str, verbose: bool) -> Result<()> {
if verbose {
eprintln!("Checking: {source}");
}
let tokens = match panproto_expr_parser::tokenize(source) {
Ok(t) => t,
Err(e) => {
println!("lex error: {e}");
miette::bail!("expression contains syntax errors");
}
};
match panproto_expr_parser::parse(&tokens) {
Ok(_) => {
println!("OK");
Ok(())
}
Err(errs) => {
for e in &errs {
println!("parse error: {e}");
}
miette::bail!("expression contains {} parse error(s)", errs.len());
}
}
}
pub fn cmd_expr_gat_eval(file: &Path, env_file: Option<&Path>, verbose: bool) -> Result<()> {
let term: Term = load_json(file)?;
let env: Vec<(String, gat::ModelValue)> = if let Some(env_path) = env_file {
load_json(env_path)?
} else {
Vec::new()
};
if verbose {
eprintln!("Evaluating expression: {term:?}");
if !env.is_empty() {
eprintln!("Environment: {} bindings", env.len());
}
}
let result = eval_term(&term, &env)?;
let json = serde_json::to_string_pretty(&result)
.into_diagnostic()
.wrap_err("failed to serialize result")?;
println!("{json}");
Ok(())
}
pub fn cmd_expr_gat_check(file: &Path, verbose: bool) -> Result<()> {
#[derive(serde::Deserialize)]
struct CheckInput {
term: Term,
theory: gat::Theory,
#[serde(default)]
context: Vec<(String, String)>,
}
let input: CheckInput = load_json(file)?;
if verbose {
eprintln!("Type-checking expression: {:?}", input.term);
eprintln!("Theory sorts: {}", input.theory.sorts.len());
eprintln!("Theory operations: {}", input.theory.ops.len());
}
let mut ctx = rustc_hash::FxHashMap::default();
for (var_name, sort_name) in &input.context {
ctx.insert(
std::sync::Arc::from(var_name.as_str()),
gat::SortExpr::Name(std::sync::Arc::from(sort_name.as_str())),
);
}
match gat::typecheck_term(&input.term, &ctx, &input.theory) {
Ok(sort) => {
println!("Well-formed. Output sort: {sort}");
Ok(())
}
Err(e) => {
println!("Type error: {e}");
miette::bail!("expression type-check failed: {e}");
}
}
}
pub fn cmd_expr_repl() -> Result<()> {
println!("panproto expression REPL");
println!("Enter JSON-encoded GAT terms. Type :q to exit.\n");
let stdin = io::stdin();
let mut env: Vec<(String, gat::ModelValue)> = Vec::new();
loop {
print!("expr> ");
io::stdout().flush().into_diagnostic()?;
let mut line = String::new();
let bytes_read = stdin.lock().read_line(&mut line).into_diagnostic()?;
if bytes_read == 0 {
println!();
break;
}
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed == ":q" || trimmed == ":quit" {
break;
}
if let Some(rest) = trimmed.strip_prefix(":let ") {
if let Some((name, value_str)) = rest.split_once('=') {
let name = name.trim().to_string();
let value_str = value_str.trim();
match serde_json::from_str::<gat::ModelValue>(value_str) {
Ok(value) => {
env.retain(|(k, _)| k != &name);
println!(" {name} = {value:?}");
env.push((name, value));
}
Err(e) => {
println!(" parse error: {e}");
}
}
continue;
}
println!(" usage: :let <name> = <json-value>");
continue;
}
if trimmed == ":env" {
if env.is_empty() {
println!(" (empty)");
} else {
for (k, v) in &env {
println!(" {k} = {v:?}");
}
}
continue;
}
match serde_json::from_str::<Term>(trimmed) {
Ok(term) => match eval_term(&term, &env) {
Ok(result) => {
let json = serde_json::to_string_pretty(&result)
.unwrap_or_else(|_| format!("{result:?}"));
println!(" {json}");
}
Err(e) => {
println!(" error: {e}");
}
},
Err(e) => {
println!(" parse error: {e}");
}
}
}
Ok(())
}
fn eval_term(term: &Term, env: &[(String, gat::ModelValue)]) -> Result<gat::ModelValue> {
match term {
Term::Var(name) => env
.iter()
.find(|(k, _)| k.as_str() == name.as_ref())
.map(|(_, v)| v.clone())
.ok_or_else(|| miette::miette!("unbound variable: {name}")),
Term::App { op, args } => {
let mut evaluated = Vec::with_capacity(args.len());
for arg in args {
evaluated.push(eval_term(arg, env)?);
}
if args.is_empty() {
Ok(gat::ModelValue::Str(op.to_string()))
} else {
Ok(gat::ModelValue::Map({
let mut map = rustc_hash::FxHashMap::default();
map.insert("op".to_string(), gat::ModelValue::Str(op.to_string()));
map.insert("args".to_string(), gat::ModelValue::List(evaluated));
map
}))
}
}
Term::Case { .. } => Err(miette::miette!(
"case terms are not yet supported in CLI expression evaluation"
)),
Term::Hole { .. } => Err(miette::miette!(
"typed holes cannot be evaluated; they only carry type information"
)),
Term::Let { name, bound, body } => {
let v = eval_term(bound, env)?;
let mut extended = env.to_vec();
extended.push((name.to_string(), v));
eval_term(body, &extended)
}
}
}