mod assertions;
mod de_bruijn;
mod equality;
mod error;
mod evaluator;
mod format;
mod normalizer;
mod parser;
mod term;
mod token;
mod tokenizer;
mod type_checker;
mod unifier;
use {
crate::{
error::{Error, throw},
evaluator::evaluate,
format::CodeStr,
parser::parse,
tokenizer::tokenize,
type_checker::type_check,
},
clap::{ArgAction, Args, CommandFactory, Parser, Subcommand as ClapSubcommand},
clap_complete::{Shell, generate},
std::{fs::read_to_string, io::stdout, path::Path, process::exit, thread},
};
const BIN_NAME: &str = "gram";
const STACK_SIZE: usize = 16 * 1024 * 1024;
const ABOUT: &str = concat!(
env!("CARGO_PKG_DESCRIPTION"),
"\n\n",
"More information can be found at: ",
env!("CARGO_PKG_HOMEPAGE"),
);
#[derive(Parser)]
#[command(
about = ABOUT,
version,
arg_required_else_help = true, // [tag:arg_required_else_help]
disable_version_flag = true
)]
struct Cli {
#[arg(short, long, help = "Print version", action = ArgAction::Version)]
_version: Option<bool>,
#[arg(help = "Set the path to the program entrypoint")]
path: Option<String>,
#[command(subcommand)]
command: Option<GramCommand>,
}
#[derive(Args)]
struct ProgramPathArg {
#[arg(help = "Set the path to the program entrypoint")]
path: String,
}
#[derive(Args)]
struct ShellCompletionArgs {
#[arg(help = "Bash, Fish, Zsh, PowerShell, or Elvish")]
shell: String,
}
#[derive(ClapSubcommand)]
enum GramCommand {
#[command(about = "Check a program")]
Check(ProgramPathArg),
#[command(about = "Run a program")]
Run(ProgramPathArg),
#[command(
name = "shell-completion",
about = "Print a shell completion script. Supports Bash, Fish, Zsh, PowerShell, and Elvish."
)]
ShellCompletion(ShellCompletionArgs),
}
fn run(source_path: &Path, check_only: bool) -> Result<(), Error> {
let collect_errors = |errors: Vec<Error>| Error {
message: errors
.iter()
.fold(String::new(), |acc, error| {
format!(
"{}\n{}{}",
acc,
if acc
.split('\n')
.next_back()
.unwrap()
.chars()
.all(|c| c == ' ' || c == '\u{203e}')
{
""
} else {
"\n"
},
error,
)
})
.trim()
.to_owned(),
reason: None,
};
let source_contents = read_to_string(source_path).map_err(|error| {
throw(
&format!(
"Error when reading file {}.",
source_path.to_string_lossy().code_str(),
),
None,
None,
Some(error),
)
})?;
let tokens = tokenize(Some(source_path), &source_contents).map_err(collect_errors)?;
let term =
parse(Some(source_path), &source_contents, &tokens[..], &[]).map_err(collect_errors)?;
let mut typing_context = vec![];
let mut definitions_context = vec![];
let (elaborated_term, elaborated_type) = type_check(
Some(source_path),
&source_contents,
&term,
&mut typing_context,
&mut definitions_context,
)
.map_err(collect_errors)?;
if check_only {
println!(
"Elaborated term:\n\n{}",
elaborated_term.to_string().code_str(),
);
println!(
"\nElaborated type:\n\n{}",
elaborated_type.to_string().code_str(),
);
} else {
let value = evaluate(&elaborated_term)?;
println!("{}", value.to_string().code_str());
}
Ok(())
}
fn shell_completion(shell: &str) -> Result<(), Error> {
let shell_variant = match shell.trim().to_lowercase().as_ref() {
"bash" => Shell::Bash,
"fish" => Shell::Fish,
"zsh" => Shell::Zsh,
"powershell" => Shell::PowerShell,
"elvish" => Shell::Elvish,
_ => {
return Err(Error {
message: format!(
"Unknown shell {}. Must be one of Bash, Fish, Zsh, PowerShell, or Elvish.",
shell.code_str(),
),
reason: None,
});
}
};
let mut command = Cli::command();
generate(shell_variant, &mut command, BIN_NAME, &mut stdout());
Ok(())
}
fn entry() -> Result<(), Error> {
let cli = Cli::parse();
if let Some(source_path) = cli.path.as_deref() {
run(Path::new(source_path), false)?;
} else {
match cli.command {
Some(GramCommand::Check(args)) => {
run(Path::new(&args.path), true)?;
}
Some(GramCommand::Run(args)) => {
run(Path::new(&args.path), false)?;
}
Some(GramCommand::ShellCompletion(args)) => {
shell_completion(&args.shell)?;
}
None => panic!("The help message should have been printed."),
}
}
Ok(())
}
fn main() {
thread::Builder::new()
.stack_size(STACK_SIZE)
.spawn(|| {
if let Err(e) = entry() {
eprintln!("{e}");
exit(1);
}
})
.unwrap_or_else(|e| {
eprintln!("Error spawning thread: {e:?}");
exit(1);
})
.join()
.unwrap_or_else(|e| {
eprintln!("Error joining thread: {e:?}");
exit(1);
});
}
#[cfg(test)]
mod tests {
use super::Cli;
use clap::CommandFactory;
#[test]
fn verify_cli() {
Cli::command().debug_assert();
}
}