#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
use ksl::{
BUILTIN_FUNCTIONS,
Environment,
MODULE_NAME_ENV,
MODULE_PATH_ENV,
eval::eval,
expand_tilde,
init_environment,
token::{TokenType, source_to_token},
value::Value,
};
#[derive(clap::Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[arg(short, long, conflicts_with = "file_path")]
code: Option<String>,
#[arg(conflicts_with = "code")]
file_path: Option<String>,
#[arg(last = true)]
args: Vec<String>,
}
fn main() {
let cli = <Cli as clap::Parser>::parse();
let env_args = (!cli.args.is_empty()).then_some(Value::List(
cli.args
.into_iter()
.map(|arg| Value::String(std::sync::Arc::from(arg)))
.collect::<std::sync::Arc<[Value]>>(),
));
if let Err(e) = if let Some(code) = cli.code {
run_ksl(&code, &code, init_environment(env_args))
} else if let Some(path) = cli.file_path {
read_ksl_file(&path).and_then(|source| run_ksl(&source, &path, init_environment(env_args)))
} else {
repl(env_args);
return;
} {
eprintln!("Error[ksl::main]: {e}");
std::process::exit(1);
}
}
fn run_ksl(source: &str, label: &str, env: Environment) -> Result<(), String> {
eval(source, env)
.map(|val| {
if !matches!(val, Value::Unit) {
println!("{val}")
}
})
.map_err(|e| format!("Failed to evaluate `{label}` with:\n\t{e}"))
}
fn read_ksl_file(path: &str) -> Result<String, String> {
expand_tilde(std::sync::Arc::from(path))
.map_err(|e| e.to_string())
.and_then(|real_path| {
std::fs::read_to_string(&real_path).map_err(|e| format!("Unable to read file `{}`: {e}", real_path.display()))
})
}
#[derive(Default)]
struct KSLValidator;
impl reedline::Validator for KSLValidator {
fn validate(&self, line: &str) -> reedline::ValidationResult {
fn is_input_complete(buffer: &str) -> bool {
let mut chars = buffer.chars().peekable();
let (mut brace_bal, mut bracket_bal, mut comment_depth) = (0i32, 0i32, 0usize);
let mut in_string = false;
let mut has_content = false;
while let Some(ch) = chars.next() {
if !ch.is_whitespace() {
has_content = true;
}
match ch {
'(' if comment_depth > 0 && chars.next_if_eq(&'*').is_some() => comment_depth += 1,
'*' if comment_depth > 0 && chars.next_if_eq(&')').is_some() => comment_depth -= 1,
_ if comment_depth > 0 => continue,
'"' => in_string = !in_string,
_ if in_string => continue,
'(' if chars.next_if_eq(&'*').is_some() => comment_depth = 1,
'{' => brace_bal += 1,
'}' => brace_bal -= 1,
'[' => bracket_bal += 1,
']' => bracket_bal -= 1,
_ => (),
}
}
has_content && comment_depth == 0 && !in_string && brace_bal <= 0 && bracket_bal <= 0
}
if is_input_complete(line) {
reedline::ValidationResult::Complete
} else {
reedline::ValidationResult::Incomplete
}
}
}
struct KSLHighlighter {
bif: nu_ansi_term::Style,
symbol: nu_ansi_term::Style,
atom: nu_ansi_term::Style,
constant: nu_ansi_term::Style,
string: nu_ansi_term::Style,
chr: nu_ansi_term::Style,
number: nu_ansi_term::Style,
normal: nu_ansi_term::Style,
}
impl Default for KSLHighlighter {
fn default() -> Self {
KSLHighlighter {
bif: nu_ansi_term::Color::Default.bold().italic(),
symbol: nu_ansi_term::Color::Yellow.normal(),
atom: nu_ansi_term::Color::Red.normal(),
constant: nu_ansi_term::Color::LightRed.bold().italic(),
string: nu_ansi_term::Color::LightGreen.normal(),
chr: nu_ansi_term::Color::Purple.normal().italic(),
number: nu_ansi_term::Color::Cyan.bold(),
normal: nu_ansi_term::Color::DarkGray.normal(),
}
}
}
fn convert_pos_to_chr_index(full_text: &str, line_num: usize, col_num: usize, line_starts: &[usize]) -> Option<usize> {
if line_num == 0 || col_num == 0 {
return None;
}
let line_start_byte = *line_starts.get(line_num - 1)?;
let line_content = full_text.get(line_start_byte..)?.lines().next()?;
match line_content.char_indices().nth(col_num - 1) {
Some((col_byte_offset, _)) => Some(line_start_byte + col_byte_offset),
None => Some(line_start_byte + line_content.len()),
}
}
impl reedline::Highlighter for KSLHighlighter {
fn highlight(&self, line: &str, _cursor: usize) -> reedline::StyledText {
let mut st = reedline::StyledText::new();
if line.is_empty() {
return st;
}
let line_starts: Vec<usize> = std::iter::once(0)
.chain(line.match_indices('\n').map(|(i, _)| i + 1))
.collect();
match source_to_token(line) {
Ok(toks) => {
let mut last_pos = 0;
for tok in toks {
let (start_line, start_col) = tok.location.0;
let (end_line, end_col) = tok.location.1;
let start_byte = match convert_pos_to_chr_index(line, start_line, start_col, &line_starts) {
Some(b) => b,
None => continue,
};
let end_char_byte = match convert_pos_to_chr_index(line, end_line, end_col, &line_starts) {
Some(b) => b,
None => continue,
};
let end_byte = end_char_byte
+ line
.get(end_char_byte..)
.and_then(|s| s.chars().next())
.map_or(0, |c| c.len_utf8());
if last_pos < start_byte {
st.push((self.normal, String::from(&line[last_pos..start_byte])));
}
let style = match &tok.value {
TokenType::Symbol(sym) => {
if BUILTIN_FUNCTIONS.contains(&sym.iter().collect::<String>().as_str()) {
self.bif
} else {
self.symbol
}
}
TokenType::Atom(atm) => {
if ["t", "f", "ok", "err"].contains(&atm.iter().collect::<String>().as_str()) {
self.constant
} else {
self.atom
}
}
TokenType::String(_) => self.string,
TokenType::Char(_) => self.chr,
TokenType::Number(_) => self.number,
_ => self.normal,
};
st.push((style, String::from(&line[start_byte..end_byte])));
last_pos = end_byte;
}
if last_pos < line.len() {
st.push((self.normal, String::from(&line[last_pos..])));
}
}
Err(_) => {
st.push((self.normal, line.to_string()));
}
}
st
}
}
struct KSLCompleter {
pub words: Vec<String>,
}
impl reedline::Completer for KSLCompleter {
fn complete(&mut self, line: &str, pos: usize) -> Vec<reedline::Suggestion> {
let word_start = line[..pos]
.char_indices()
.rev()
.find_map(|(idx, ch)| (ch.is_whitespace() || "[]{},;".contains(ch)).then_some(idx + ch.len_utf8()))
.unwrap_or(0);
let word_to_complete = &line[word_start..pos];
if word_to_complete.is_empty() {
return Vec::new();
}
nucleo_matcher::pattern::Atom::new(
word_to_complete,
nucleo_matcher::pattern::CaseMatching::Smart,
nucleo_matcher::pattern::Normalization::Smart,
nucleo_matcher::pattern::AtomKind::Fuzzy,
true,
)
.match_list(self.words.iter(), &mut {
let mut config = nucleo_matcher::Config::DEFAULT;
config.prefer_prefix = true;
nucleo_matcher::Matcher::new(config)
})
.into_iter()
.map(|(word, _)| reedline::Suggestion {
value: word.clone(),
span: reedline::Span::new(word_start, pos),
append_whitespace: false,
..reedline::Suggestion::default()
})
.collect::<Vec<reedline::Suggestion>>()
}
}
fn collect_visible_keys(env: &Environment) -> Vec<String> {
let mut keys = std::collections::HashSet::new();
let mut current = Some(env.clone());
while let Some(scope_arc) = current {
for key in scope_arc.store.upgradable_read().keys() {
keys.insert(key.to_string());
}
current = scope_arc.parent.clone();
}
let mut result: Vec<String> = keys.into_iter().collect();
result.sort();
result
}
pub fn repl(env_args: Option<Value>) {
let repl_env = init_environment(env_args);
let mut repl_count: usize = 1;
let mut editor = reedline::Reedline::create()
.with_menu(reedline::ReedlineMenu::EngineCompleter(Box::new(
reedline::MenuBuilder::with_name(reedline::ColumnarMenu::default(), "BIFs")
.with_columns(4)
.with_column_width(None)
.with_column_padding(2),
)))
.with_edit_mode(Box::new(reedline::Emacs::new({
let mut kbds = reedline::default_emacs_keybindings();
kbds.add_binding(
reedline::KeyModifiers::NONE,
reedline::KeyCode::Tab,
reedline::ReedlineEvent::UntilFound(vec![
reedline::ReedlineEvent::Menu(String::from("BIFs")),
reedline::ReedlineEvent::MenuNext,
]),
);
kbds.add_binding(
reedline::KeyModifiers::ALT,
reedline::KeyCode::Enter,
reedline::ReedlineEvent::Edit(vec![reedline::EditCommand::InsertNewline]),
);
kbds
})))
.with_validator(Box::new(KSLValidator))
.with_highlighter(Box::new(KSLHighlighter::default()));
println!("KSL REPL");
println!("Use `.help` to view help information.");
println!("Use `.exit` or Ctrl-D to exit.");
println!();
loop {
let prompt = reedline::DefaultPrompt::new(
reedline::DefaultPromptSegment::Basic(format!("I[{}]", repl_count)),
reedline::DefaultPromptSegment::Empty,
);
let words = collect_visible_keys(&repl_env);
editor = editor.with_completer(Box::new(KSLCompleter { words }));
let signal = editor.read_line(&prompt);
match signal {
Ok(reedline::Signal::Success(buffer)) => {
let input = buffer.trim();
if input.is_empty() {
continue;
}
match try_handle_user_command(input, repl_env.clone()) {
None => break,
Some(false) => continue,
Some(true) => match eval(input, repl_env.clone()) {
Ok(result) => {
let out_prompt = nu_ansi_term::Style::new()
.bold()
.fg(nu_ansi_term::Color::DarkGray)
.paint(format!("O[{}]= ", repl_count));
println!("{}{}", out_prompt, result);
let _ = repl_env.store.write().remove(MODULE_NAME_ENV.as_ref());
repl_count += 1;
}
Err(e) => eprintln!(
"{}",
nu_ansi_term::Style::new()
.fg(nu_ansi_term::Color::Red)
.paint(e.as_ref())
),
},
}
}
Ok(reedline::Signal::CtrlC) => {
continue;
}
Ok(_) => {
break;
}
Err(err) => {
eprintln!(
concat!("Error[ksl::repl]: ", "Error reading line with `{}`."),
err
);
}
}
}
}
fn try_handle_user_command(input: &str, env: Environment) -> Option<bool> {
fn print_wrapped(items: impl Iterator<Item = String>) {
let max_len = 72;
let mut current_line = String::new();
for item in items {
if current_line.is_empty() {
current_line.push_str(&item);
} else if current_line.len() + 1 + item.len() > max_len {
println!("{}", current_line);
current_line.clear();
current_line.push_str(&item);
} else {
current_line.push(' ');
current_line.push_str(&item);
}
}
if !current_line.is_empty() {
println!("{}", current_line);
}
}
match input {
".exit" => None,
".help" => {
print_help_information();
Some(false)
}
".env" => {
println!();
println!(
"{}",
nu_ansi_term::Style::new().bold().paint("[ Built-ins ]")
);
let builtin_iter = BUILTIN_FUNCTIONS.iter().map(|name| format!("[{}]", name));
print_wrapped(builtin_iter);
let mut user_vars = Vec::new();
for (k, v) in env.store.read().iter() {
if k.as_ref() == MODULE_PATH_ENV.as_ref()
|| k.as_ref() == MODULE_NAME_ENV.as_ref()
|| BUILTIN_FUNCTIONS.contains(&k.as_ref())
{
continue;
}
user_vars.push(format!("<{}, {}>", k, v));
}
if !user_vars.is_empty() {
println!();
println!(
"{}",
nu_ansi_term::Style::new().bold().paint("[ Variables ]")
);
print_wrapped(user_vars.into_iter());
}
println!();
Some(false)
}
_ => Some(true),
}
}
fn print_help_information() {
println!();
println!(".help: Display this information.");
println!(".env: Output all bindings in the current environment.");
println!(".exit: Exit the REPL.");
println!();
}