#![deny(clippy::all, clippy::pedantic, warnings)]
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::{throw, Error},
evaluator::evaluate,
format::CodeStr,
parser::parse,
tokenizer::tokenize,
type_checker::type_check,
},
clap::{App, AppSettings, Arg, Shell, SubCommand},
std::{fs::read_to_string, io::stdout, path::Path, process::exit, thread},
};
const VERSION: &str = env!("CARGO_PKG_VERSION");
const BIN_NAME: &str = "gram";
const PATH_OPTION: &str = "path";
const CHECK_SUBCOMMAND: &str = "check";
const CHECK_SUBCOMMAND_PATH_OPTION: &str = "check-path";
const RUN_SUBCOMMAND: &str = "run";
const RUN_SUBCOMMAND_PATH_OPTION: &str = "run-path";
const SHELL_COMPLETION_SUBCOMMAND: &str = "shell-completion";
const SHELL_COMPLETION_SUBCOMMAND_SHELL_OPTION: &str = "shell-completion-shell";
const STACK_SIZE: usize = 16 * 1024 * 1024;
fn cli<'a, 'b>() -> App<'a, 'b> {
App::new("Gram")
.version(VERSION)
.version_short("v")
.author("Stephan Boyer <stephan@stephanboyer.com>")
.about(
" \
Gram is programming language for distributed systems. Visit https://www.gram.org for \
more information. \
"
.trim(),
)
.setting(AppSettings::ArgRequiredElseHelp) .setting(AppSettings::ColoredHelp)
.setting(AppSettings::NextLineHelp)
.setting(AppSettings::UnifiedHelpMessage)
.setting(AppSettings::VersionlessSubcommands)
.arg(
Arg::with_name(PATH_OPTION)
.value_name("PATH")
.help("Sets the path of the program entrypoint"),
)
.subcommand(
SubCommand::with_name(CHECK_SUBCOMMAND)
.about("Checks a program")
.arg(
Arg::with_name(CHECK_SUBCOMMAND_PATH_OPTION)
.value_name("PATH")
.help("Sets the path of the program entrypoint")
.required(true), ),
)
.subcommand(
SubCommand::with_name(RUN_SUBCOMMAND)
.about("Runs a program")
.arg(
Arg::with_name(RUN_SUBCOMMAND_PATH_OPTION)
.value_name("PATH")
.help("Sets the path of the program entrypoint")
.required(true), ),
)
.subcommand(
SubCommand::with_name(SHELL_COMPLETION_SUBCOMMAND)
.about(
" \
Prints a shell completion script. Supports Zsh, Fish, Zsh, PowerShell, and \
Elvish. \
"
.trim(),
)
.arg(
Arg::with_name(SHELL_COMPLETION_SUBCOMMAND_SHELL_OPTION)
.value_name("SHELL")
.help("Bash, Fish, Zsh, PowerShell, or Elvish")
.required(true), ),
)
}
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')
.last()
.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,
});
}
};
cli().gen_completions_to(BIN_NAME, shell_variant, &mut stdout());
Ok(())
}
fn entry() -> Result<(), Error> {
let matches = cli().get_matches();
if let Some(source_path) = matches.value_of(PATH_OPTION) {
run(Path::new(source_path), false)?;
} else {
match matches.subcommand_name() {
Some(subcommand) if subcommand == CHECK_SUBCOMMAND => {
let source_path = Path::new(
matches
.subcommand_matches(CHECK_SUBCOMMAND)
.unwrap() .value_of(CHECK_SUBCOMMAND_PATH_OPTION)
.unwrap(),
);
run(source_path, true)?;
}
Some(subcommand) if subcommand == RUN_SUBCOMMAND => {
let source_path = Path::new(
matches
.subcommand_matches(RUN_SUBCOMMAND)
.unwrap() .value_of(RUN_SUBCOMMAND_PATH_OPTION)
.unwrap(),
);
run(source_path, false)?;
}
Some(subcommand) if subcommand == SHELL_COMPLETION_SUBCOMMAND => {
shell_completion(
matches
.subcommand_matches(SHELL_COMPLETION_SUBCOMMAND)
.unwrap() .value_of(SHELL_COMPLETION_SUBCOMMAND_SHELL_OPTION)
.unwrap(),
)?;
}
Some(_) => panic!("Subcommand not implemented."),
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);
});
}