2023-10-26 20:00:06 +00:00
|
|
|
//! Command line interface for the dust programming language.
|
2024-01-17 17:48:51 +00:00
|
|
|
|
2024-01-06 10:00:36 +00:00
|
|
|
use clap::{Parser, Subcommand};
|
2024-01-25 06:28:22 +00:00
|
|
|
use crossterm::event::{KeyCode, KeyModifiers};
|
|
|
|
use nu_ansi_term::Style;
|
|
|
|
use reedline::{
|
2024-01-25 07:17:45 +00:00
|
|
|
default_emacs_keybindings, DefaultPrompt, EditCommand, Emacs, Highlighter, Reedline,
|
|
|
|
ReedlineEvent, Signal, SqliteBackedHistory, StyledText,
|
2023-08-22 15:40:50 +00:00
|
|
|
};
|
|
|
|
|
2024-01-25 06:28:22 +00:00
|
|
|
use std::{fs::read_to_string, path::PathBuf};
|
2023-08-22 15:40:50 +00:00
|
|
|
|
2024-01-25 06:28:22 +00:00
|
|
|
use dust_lang::{built_in_values, Interpreter, Map, Result, Value};
|
2023-08-22 15:40:50 +00:00
|
|
|
|
|
|
|
/// Command-line arguments to be parsed.
|
|
|
|
#[derive(Parser, Debug)]
|
|
|
|
#[command(author, version, about, long_about = None)]
|
|
|
|
struct Args {
|
2023-10-26 20:00:06 +00:00
|
|
|
/// Dust source code to evaluate.
|
2023-08-22 15:40:50 +00:00
|
|
|
#[arg(short, long)]
|
|
|
|
command: Option<String>,
|
|
|
|
|
2023-10-26 20:00:06 +00:00
|
|
|
/// Data to assign to the "input" variable.
|
|
|
|
#[arg(short, long)]
|
|
|
|
input: Option<String>,
|
|
|
|
|
2024-01-17 17:48:51 +00:00
|
|
|
/// File whose contents will be assigned to the "input" variable.
|
2023-10-26 20:00:06 +00:00
|
|
|
#[arg(short = 'p', long)]
|
|
|
|
input_path: Option<String>,
|
|
|
|
|
2024-01-17 17:48:51 +00:00
|
|
|
/// Command for alternate functionality besides running the source.
|
2024-01-06 10:00:36 +00:00
|
|
|
#[command(subcommand)]
|
|
|
|
cli_command: Option<CliCommand>,
|
|
|
|
|
2023-08-23 04:39:46 +00:00
|
|
|
/// Location of the file to run.
|
|
|
|
path: Option<String>,
|
2023-08-22 15:40:50 +00:00
|
|
|
}
|
|
|
|
|
2024-01-06 10:00:36 +00:00
|
|
|
#[derive(Subcommand, Debug)]
|
|
|
|
pub enum CliCommand {
|
2024-01-13 18:39:30 +00:00
|
|
|
/// Output a formatted version of the input.
|
2024-01-06 10:00:36 +00:00
|
|
|
Format,
|
2024-01-13 18:39:30 +00:00
|
|
|
|
|
|
|
/// Output a concrete syntax tree of the input.
|
|
|
|
Syntax,
|
2024-01-06 10:00:36 +00:00
|
|
|
}
|
|
|
|
|
2023-11-18 01:10:07 +00:00
|
|
|
fn main() {
|
2024-01-13 18:30:50 +00:00
|
|
|
env_logger::init();
|
|
|
|
|
2023-08-22 15:40:50 +00:00
|
|
|
let args = Args::parse();
|
2023-12-29 19:52:51 +00:00
|
|
|
let context = Map::new();
|
2023-10-26 20:00:06 +00:00
|
|
|
|
2023-10-28 14:28:43 +00:00
|
|
|
if let Some(input) = args.input {
|
|
|
|
context
|
2024-01-10 01:38:40 +00:00
|
|
|
.set("input".to_string(), Value::string(input))
|
2023-12-13 20:47:41 +00:00
|
|
|
.unwrap();
|
2023-10-28 14:28:43 +00:00
|
|
|
}
|
|
|
|
|
2023-10-26 20:00:06 +00:00
|
|
|
if let Some(path) = args.input_path {
|
2023-11-18 01:10:07 +00:00
|
|
|
let file_contents = read_to_string(path).unwrap();
|
2023-10-26 20:00:06 +00:00
|
|
|
|
2023-12-13 20:47:41 +00:00
|
|
|
context
|
2024-01-10 01:38:40 +00:00
|
|
|
.set("input".to_string(), Value::string(file_contents))
|
2023-12-13 20:47:41 +00:00
|
|
|
.unwrap();
|
2023-10-26 20:00:06 +00:00
|
|
|
}
|
|
|
|
|
2024-01-24 23:57:36 +00:00
|
|
|
if args.path.is_none() && args.command.is_none() {
|
2024-01-25 06:28:22 +00:00
|
|
|
let run_shell_result = run_shell(context);
|
|
|
|
|
|
|
|
match run_shell_result {
|
|
|
|
Ok(_) => {}
|
|
|
|
Err(error) => eprintln!("{error}"),
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
2024-01-24 23:57:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let source = if let Some(path) = &args.path {
|
|
|
|
read_to_string(path).unwrap()
|
|
|
|
} else if let Some(command) = args.command {
|
|
|
|
command
|
|
|
|
} else {
|
|
|
|
String::with_capacity(0)
|
|
|
|
};
|
|
|
|
|
2023-12-30 17:02:58 +00:00
|
|
|
let mut interpreter = Interpreter::new(context);
|
2023-11-15 00:37:19 +00:00
|
|
|
|
2024-01-13 18:39:30 +00:00
|
|
|
if let Some(CliCommand::Syntax) = args.cli_command {
|
2023-12-31 16:46:56 +00:00
|
|
|
interpreter.parse(&source).unwrap();
|
2023-12-29 19:35:52 +00:00
|
|
|
|
|
|
|
println!("{}", interpreter.syntax_tree().unwrap());
|
2024-01-13 18:39:30 +00:00
|
|
|
|
|
|
|
return;
|
2023-11-15 00:37:19 +00:00
|
|
|
}
|
|
|
|
|
2024-01-13 18:39:30 +00:00
|
|
|
if let Some(CliCommand::Format) = args.cli_command {
|
|
|
|
println!("{}", interpreter.format());
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-01-24 23:57:36 +00:00
|
|
|
let eval_result = interpreter.run(&source);
|
|
|
|
|
2023-10-09 19:54:47 +00:00
|
|
|
match eval_result {
|
2023-10-24 00:45:47 +00:00
|
|
|
Ok(value) => {
|
2023-12-22 20:02:22 +00:00
|
|
|
if !value.is_none() {
|
2023-10-24 00:45:47 +00:00
|
|
|
println!("{value}")
|
|
|
|
}
|
|
|
|
}
|
2023-10-09 19:54:47 +00:00
|
|
|
Err(error) => eprintln!("{error}"),
|
2023-08-22 15:40:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-25 06:28:22 +00:00
|
|
|
struct DustHighlighter {
|
|
|
|
context: Map,
|
|
|
|
}
|
2023-08-22 15:40:50 +00:00
|
|
|
|
2024-01-25 06:28:22 +00:00
|
|
|
impl DustHighlighter {
|
|
|
|
fn new(context: Map) -> Self {
|
|
|
|
Self { context }
|
|
|
|
}
|
2023-08-28 20:14:55 +00:00
|
|
|
}
|
|
|
|
|
2024-01-25 06:28:22 +00:00
|
|
|
impl Highlighter for DustHighlighter {
|
|
|
|
fn highlight(&self, line: &str, _cursor: usize) -> reedline::StyledText {
|
|
|
|
fn highlight_identifier(styled: &mut StyledText, word: &str, map: &Map) -> bool {
|
|
|
|
for (key, (value, _type)) in map.variables().unwrap().iter() {
|
2024-01-25 07:17:45 +00:00
|
|
|
if key == &word {
|
2024-01-25 06:28:22 +00:00
|
|
|
styled.push((Style::new().bold(), word.to_string()));
|
2024-01-25 00:41:47 +00:00
|
|
|
|
2024-01-25 06:28:22 +00:00
|
|
|
return true;
|
|
|
|
}
|
2024-01-25 00:41:47 +00:00
|
|
|
|
2024-01-25 06:28:22 +00:00
|
|
|
if let Value::Map(nested_map) = value {
|
|
|
|
return highlight_identifier(styled, key, nested_map);
|
|
|
|
}
|
2024-01-25 00:41:47 +00:00
|
|
|
}
|
|
|
|
|
2024-01-25 06:28:22 +00:00
|
|
|
for built_in_value in built_in_values() {
|
2024-01-25 07:17:45 +00:00
|
|
|
if built_in_value.name() == word {
|
2024-01-25 06:28:22 +00:00
|
|
|
styled.push((Style::new().bold(), word.to_string()));
|
2024-01-25 07:17:45 +00:00
|
|
|
|
|
|
|
return true;
|
2024-01-25 00:41:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-25 06:28:22 +00:00
|
|
|
false
|
2023-08-22 15:40:50 +00:00
|
|
|
}
|
|
|
|
|
2024-01-25 06:28:22 +00:00
|
|
|
let mut styled = StyledText::new();
|
2024-01-25 07:17:45 +00:00
|
|
|
let terminators = [' ', ':', '(', ')', '{', '}', '[', ']'];
|
|
|
|
|
|
|
|
for word in line.split_inclusive(&terminators) {
|
|
|
|
let word_is_highlighted =
|
|
|
|
highlight_identifier(&mut styled, &word[0..word.len() - 1], &self.context);
|
2023-08-22 15:40:50 +00:00
|
|
|
|
2024-01-25 07:17:45 +00:00
|
|
|
if word_is_highlighted {
|
|
|
|
let final_char = word.chars().last().unwrap();
|
2023-08-22 15:40:50 +00:00
|
|
|
|
2024-01-25 07:17:45 +00:00
|
|
|
if terminators.contains(&final_char) {
|
|
|
|
styled.push((Style::new(), final_char.to_string()));
|
|
|
|
}
|
|
|
|
} else {
|
2024-01-25 06:28:22 +00:00
|
|
|
styled.push((Style::new(), word.to_string()));
|
|
|
|
}
|
2023-08-28 20:14:55 +00:00
|
|
|
}
|
2023-08-22 15:40:50 +00:00
|
|
|
|
2024-01-25 06:28:22 +00:00
|
|
|
styled
|
2023-08-22 15:40:50 +00:00
|
|
|
}
|
2023-08-28 20:14:55 +00:00
|
|
|
}
|
2023-08-22 15:40:50 +00:00
|
|
|
|
2024-01-25 06:28:22 +00:00
|
|
|
fn run_shell(context: Map) -> Result<()> {
|
|
|
|
let mut interpreter = Interpreter::new(context.clone());
|
|
|
|
let prompt = DefaultPrompt::default();
|
|
|
|
let mut keybindings = default_emacs_keybindings();
|
|
|
|
|
|
|
|
keybindings.add_binding(
|
2024-01-25 07:17:45 +00:00
|
|
|
KeyModifiers::NONE,
|
|
|
|
KeyCode::Enter,
|
|
|
|
ReedlineEvent::Edit(vec![EditCommand::InsertNewline]),
|
|
|
|
);
|
|
|
|
keybindings.add_binding(
|
|
|
|
KeyModifiers::CONTROL,
|
|
|
|
KeyCode::Char(' '),
|
|
|
|
ReedlineEvent::Submit,
|
2024-01-25 06:28:22 +00:00
|
|
|
);
|
|
|
|
keybindings.add_binding(
|
|
|
|
KeyModifiers::NONE,
|
|
|
|
KeyCode::Tab,
|
|
|
|
ReedlineEvent::UntilFound(vec![
|
|
|
|
ReedlineEvent::Menu("completion_menu".to_string()),
|
|
|
|
ReedlineEvent::MenuNext,
|
|
|
|
]),
|
|
|
|
);
|
|
|
|
|
|
|
|
let edit_mode = Box::new(Emacs::new(keybindings));
|
|
|
|
let history = Box::new(
|
|
|
|
SqliteBackedHistory::with_file(PathBuf::from("target/history"), None, None)
|
|
|
|
.expect("Error loading history."),
|
|
|
|
);
|
|
|
|
let mut line_editor = Reedline::create()
|
|
|
|
.with_edit_mode(edit_mode)
|
|
|
|
.with_history(history)
|
2024-01-25 07:17:45 +00:00
|
|
|
.with_highlighter(Box::new(DustHighlighter::new(context)));
|
2023-08-28 20:14:55 +00:00
|
|
|
|
|
|
|
loop {
|
2024-01-25 06:28:22 +00:00
|
|
|
let sig = line_editor.read_line(&prompt);
|
|
|
|
match sig {
|
|
|
|
Ok(Signal::Success(buffer)) => {
|
2024-01-25 07:17:45 +00:00
|
|
|
if buffer.trim().is_empty() {
|
2024-01-25 06:28:22 +00:00
|
|
|
continue;
|
|
|
|
}
|
2023-08-28 20:14:55 +00:00
|
|
|
|
2024-01-25 06:28:22 +00:00
|
|
|
let run_result = interpreter.run(&buffer);
|
2023-08-28 20:14:55 +00:00
|
|
|
|
2024-01-25 06:28:22 +00:00
|
|
|
match run_result {
|
|
|
|
Ok(value) => {
|
|
|
|
if !value.is_none() {
|
|
|
|
println!("{value}")
|
|
|
|
}
|
2024-01-03 20:25:53 +00:00
|
|
|
}
|
2024-01-25 06:28:22 +00:00
|
|
|
Err(error) => println!("Error: {error}"),
|
2023-08-28 20:14:55 +00:00
|
|
|
}
|
|
|
|
}
|
2024-01-25 06:28:22 +00:00
|
|
|
Ok(Signal::CtrlD) | Ok(Signal::CtrlC) => {
|
|
|
|
println!("\nAborted!");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
x => {
|
|
|
|
println!("Event: {:?}", x);
|
|
|
|
}
|
2023-08-28 20:14:55 +00:00
|
|
|
}
|
2023-08-22 15:40:50 +00:00
|
|
|
}
|
|
|
|
|
2024-01-25 06:28:22 +00:00
|
|
|
Ok(())
|
2023-08-22 15:40:50 +00:00
|
|
|
}
|