diff --git a/Cargo.toml b/Cargo.toml index 6aab7d4..052610a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,10 @@ readme = "README.md" name = "daisy" path = "src/main.rs" +[lib] +name = "daisycalc" +path = "src/lib.rs" + [profile.release] opt-level = 3 debug = 0 diff --git a/src/entrypoint/mod.rs b/src/entrypoint/mod.rs deleted file mode 100644 index 54ff10f..0000000 --- a/src/entrypoint/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ - - -// Select main script for target system -cfg_if::cfg_if! { - if #[cfg(target_family = "unix")] { - mod unix; - pub use unix::main as main_e; - } else { - pub fn main_e() -> Result<(), std::io::Error> { - unimplemented!("Not yet implemented."); - } - } -} \ No newline at end of file diff --git a/src/entrypoint/unix/mod.rs b/src/entrypoint/unix/mod.rs deleted file mode 100644 index bca160f..0000000 --- a/src/entrypoint/unix/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod unix; -mod promptbuffer; -pub use self::unix::main; \ No newline at end of file diff --git a/src/entrypoint/unix/unix.rs b/src/entrypoint/unix/unix.rs deleted file mode 100644 index 104a60a..0000000 --- a/src/entrypoint/unix/unix.rs +++ /dev/null @@ -1,115 +0,0 @@ -use std::io::stdout; -use std::io::stdin; -use std::env; - -use termion::{ - event::Key, - input::TermRead, - raw::IntoRawMode, - color::DetectColors -}; - -use super::promptbuffer::PromptBuffer; -use crate::command; -use crate::context::Context; -use crate::FormattedText; - -#[inline(always)] -pub fn main() -> Result<(), std::io::Error> { - let mut stdout = stdout().into_raw_mode().unwrap(); - let mut pb: PromptBuffer = PromptBuffer::new(64); - let mut context = Context::new(); - - // Set color compatibilty - let term_colors = stdout.available_colors().unwrap_or(0); - if term_colors >= 256 { - context.config.term_color_type = 2; - } else if term_colors >= 8 { - context.config.term_color_type = 1; - } else { - context.config.term_color_type = 0; - } - - context.config.check(); - - - - // Handle command-line arguments - let args: Vec = env::args().collect(); - if args.iter().any(|s| s == "--help") { - let t = command::do_command(&mut context, &String::from("help")); - t.write(&context, &mut stdout)?; - return Ok(()); - } else if args.iter().any(|s| s == "--version") { - let t = FormattedText::new(format!( - "Daisy v{}\n", env!("CARGO_PKG_VERSION") - )); - t.write(&context, &mut stdout)?; - return Ok(()); - } else if args.iter().any(|s| s == "--debug") { - let t = FormattedText::new(format!( - concat!( - "Daisy v{}\n", - "Your terminal supports {} colors.\n" - ), - env!("CARGO_PKG_VERSION"), - term_colors - )); - t.write(&context, &mut stdout)?; - return Ok(()); - } - - 'outer: loop { - - pb.write_prompt(&mut context, &mut stdout)?; - - let stdin = stdin(); - for c in stdin.keys() { - if let Key::Char(q) = c.as_ref().unwrap() { - match q { - '\n' => { - // Print again without cursor, in case we pressed enter - // while inside a substitution - pb.write_prompt_nocursor(&mut context, &mut stdout)?; - let in_str = pb.enter(); - FormattedText::newline(&mut stdout)?; - if in_str == "" { break; } - - if in_str.trim() == "quit" { - break 'outer; - } else { - let r = crate::do_string(&mut context, &in_str); - - match r { - Ok(t) | Err(t) => { - t.write(&context, &mut stdout).unwrap(); - } - } - } - - break; - }, - _ => { pb.add_char(*q); } - }; - } else { - match c.unwrap() { - Key::Backspace => { pb.backspace(); }, - Key::Delete => { pb.delete(); }, - Key::Left => { pb.cursor_left(); }, - Key::Right => { pb.cursor_right(); }, - Key::Up => { pb.hist_up(); }, - Key::Down => { pb.hist_down(); }, - - Key::Ctrl('d') | - Key::Ctrl('c') => { break 'outer; }, - _ => {} - }; - }; - - pb.write_prompt(&mut context, &mut stdout)?; - } - } - - FormattedText::newline(&mut stdout)?; - return Ok(()); -} \ No newline at end of file diff --git a/src/formattedtext.rs b/src/formattedtext.rs index dfcfc55..ed7c6ef 100644 --- a/src/formattedtext.rs +++ b/src/formattedtext.rs @@ -102,14 +102,7 @@ fn format_map_full(c: char) -> Option { }) } -pub fn format_map(c: char, context: &Context) -> Option { - match context.config.term_color_type { - 0 => format_map_none(c), - 1 => format_map_ansi(c), - 2 => format_map_full(c), - _ => unreachable!("Invalid term_color_type") - } -} + impl FormattedText { @@ -117,6 +110,15 @@ impl FormattedText { write!(stdout, "\n")?; return Ok(()); } + + pub fn format_map(c: char, context: &Context) -> Option { + match context.config.term_color_type { + 0 => format_map_none(c), + 1 => format_map_ansi(c), + 2 => format_map_full(c), + _ => unreachable!("Invalid term_color_type") + } + } } @@ -161,7 +163,7 @@ impl FormattedText { match (a, b) { (c, ']') => { // Normal text - let q = format_map(c, context); + let q = Self::format_map(c, context); if q.is_some() { s.push_str(&q.unwrap()); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..1e85639 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,283 @@ +pub mod parser; +pub mod command; +pub mod quantity; + +use crate::parser::substitute; +use crate::parser::LineLocation; + + +mod context; +mod formattedtext; +mod errors; +mod evaluate; + +pub use crate::formattedtext::FormattedText; +pub use crate::context::Context; +pub use crate::errors::DaisyError; +pub use crate::evaluate::evaluate; + + + +#[inline(always)] +pub fn do_string( + context: &mut Context, + s: &String +) -> Result { + + let r: (LineLocation, DaisyError); + if command::is_command(s) { + return Ok(command::do_command(context, s)); + } else if s.contains("=") { + let x = do_assignment(context, s); + match x { + Ok(t) => { return Ok(t) }, + Err(t) => { r = t } + }; + } else { + let x = do_expression(context, s); + match x { + Ok((t, e)) => { context.push_hist(e); return Ok(t) }, + Err(t) => { r = t } + }; + } + + let (l, e) = r; + let mut t = FormattedText::new("".to_string()); + if l.zero() { + t.push(&format!( + "\n {}\n\n", + e.text().to_string(), + )); + } else { + t.push(&format!( + concat!( + "{}[e]{}[n]\n", + " {}\n\n" + ), + " ".repeat(l.pos + 4), + "^".repeat(l.len), + e.text().to_string(), + )); + } + + return Err(t); +} + +// Handle a simple evaluation string. +// Returns a FormattedText with output that should be printed. +#[inline(always)] +fn do_expression( + context: &mut Context, + s: &String +) -> Result<(FormattedText, parser::Expression), (LineLocation, DaisyError)> { + + let mut output = FormattedText::new("".to_string()); + + let g = parser::parse(context, &s)?; + let g_evaluated = evaluate::evaluate(context, &g)?; + + // Display parsed string + output.push(&format!( + " [s]=>[n] {}\n\n", + g.display(context) + )); + + // Display result + output.push(&format!( + " [r]=[n] {}\n\n", + g_evaluated.display_outer(context), + )); + + return Ok((output, g_evaluated)); +} + + +// Handle a variable or function definition string. +// Returns a FormattedText with output that should be printed. +#[inline(always)] +fn do_assignment( + context: &mut Context, + s: &String +) -> Result { + + let mut output = FormattedText::new("".to_string()); + + let parts = s.split("=").collect::>(); + if parts.len() != 2 { + return Err(( + LineLocation::new_zero(), + DaisyError::Syntax + )); + } + + // Index of first non-whitespace character in left + // (relative to whole prompt) + let starting_left = parts[0] + .char_indices() + .find(|(_, ch)| !(ch.is_whitespace() && *ch != '\n')) + .map(|(i, _)| i) + .unwrap_or_else(|| parts[0].len()); + + // Index of first non-whitespace character in right + // (relative to whole prompt) + // +1 accounts for equals sign + let starting_right = parts[0].chars().count() + 1 + + parts[1] + .char_indices() + .find(|(_, ch)| !(ch.is_whitespace() && *ch != '\n')) + .map(|(i, _)| i) + .unwrap_or_else(|| parts[0].len()); + + + let left = substitute(context, &parts[0].trim().to_string()); + let right = substitute(context, &parts[1].trim().to_string()); + let is_function = left.contains("("); + + // The order of methods below is a bit odd. + // This is intentional, since we want to check a definition's + // variable name before even attempting to parse its content. + if is_function { + let mut mode = 0; + let mut name = String::new(); + let mut args = String::new(); + for c in left.chars() { + match mode { + + // Mode 0: reading function name + 0 => { + if c == '(' { + mode = 1; continue; + } else { name.push(c); } + }, + + // Mode 1: reading arguments + 1 => { + if c == ')' { + mode = 2; continue; + } else { args.push(c); } + }, + + // Mode 2: we should be done by now. + // That close paren should've been the last character. + 2 => { + return Err(( + LineLocation{ pos: starting_left, len: left.chars().count() }, + DaisyError::Syntax + )); + }, + + _ => unreachable!() + } + } + + + let args = args + .split(",").collect::>() + .iter().map(|x| x.trim().to_string()).collect::>(); + + if name.len() == 0 { + return Err(( + LineLocation{ pos: starting_left, len: left.chars().count() }, + DaisyError::Syntax + )); + }; + + if !context.valid_function(&name) { + return Err(( + LineLocation{ pos: starting_left, len: left.chars().count() }, + DaisyError::BadFunction + )); + }; + + if args.iter().find(|x| &x[..] == "").is_some() { + return Err(( + LineLocation{ pos: starting_left, len: left.chars().count() }, + DaisyError::Syntax + )); + }; + + for a in &args { + if !context.valid_varible(a) { + return Err(( + LineLocation{ pos: starting_left, len: left.chars().count() }, + DaisyError::BadVariable + )); + } + } + + // Parse right hand side + let g = parser::parse(context, &right); + let Ok(g) = g else { + let Err((l, e)) = g else { unreachable!() }; + return Err(( + LineLocation{ pos: l.pos + starting_right, len: l.len}, + e + )); + }; + + // Display parsed string + output.push(&format!( + " [s]=>[n] {left} = {}\n\n", + g.display(context) + )); + + // Evaluate expression with shadow variables + for a in &args { context.add_shadow(a.to_string(), None);} + let g_evaluated = evaluate::evaluate(context, &g); + context.clear_shadow(); + let Ok(_g_evaluated) = g_evaluated else { + let Err((l, e)) = g_evaluated else { unreachable!() }; + return Err(( + LineLocation{ pos: l.pos + starting_right, len: l.len}, + e + )); + }; + + // We could push g_evaluated instead, but an un-evaluated string + // makes the 'vars' command prettier. + // + // We still need to evaluate g above, though, to make sure it works. + context.push_function(name, args, g).unwrap(); + } else { + + if !context.valid_varible(&left) { + return Err(( + LineLocation{ pos: starting_left, len: left.chars().count() }, + DaisyError::BadVariable + )); + } + + // Parse right hand side + let g = parser::parse(context, &right); + let Ok(g) = g else { + let Err((l, e)) = g else { unreachable!() }; + return Err(( + LineLocation{ pos: l.pos + starting_right, len: l.len}, + e + )); + }; + + // Display parsed string + output.push(&format!( + " [t]=>[n] {left} = {}\n\n", + g.display(context) + )); + + // Evaluate expression + let g_evaluated = evaluate::evaluate(context, &g); + let Ok(g_evaluated) = g_evaluated else { + let Err((l, e)) = g_evaluated else { unreachable!() }; + return Err(( + LineLocation{ pos: l.pos + starting_right, len: l.len}, + e + )); + }; + + context.push_variable(left.to_string(), g_evaluated).unwrap(); + } + + return Ok(output); + +} + + diff --git a/src/main.rs b/src/main.rs index 0841f07..ac2c86d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,289 +1,124 @@ -pub mod parser; -pub mod command; -pub mod quantity; -pub mod evaluate; -pub mod context; -pub mod errors; -pub mod formattedtext; +pub mod promptbuffer; -use crate::parser::substitute; -use crate::errors::DaisyError; -use crate::formattedtext::FormattedText; -use crate::context::Context; -use crate::parser::LineLocation; +use std::io::stdout; +use std::io::stdin; +use std::env; +use termion::{ + event::Key, + input::TermRead, + raw::IntoRawMode, + color::DetectColors +}; -// Run main script for target system -mod entrypoint; -use crate::entrypoint::main_e; +use promptbuffer::PromptBuffer; +use daisycalc::command; +use daisycalc::Context; +use daisycalc::FormattedText; + +use daisycalc::do_string; #[cfg(test)] mod tests; -fn main() -> Result<(), std::io::Error> { - return main_e(); -} + #[inline(always)] -pub fn do_string( - context: &mut Context, - s: &String -) -> Result { +pub fn main() -> Result<(), std::io::Error> { + let mut stdout = stdout().into_raw_mode().unwrap(); + let mut pb: PromptBuffer = PromptBuffer::new(64); + let mut context = Context::new(); - let r: (LineLocation, DaisyError); - if command::is_command(s) { - return Ok(command::do_command(context, s)); - } else if s.contains("=") { - let x = do_assignment(context, s); - match x { - Ok(t) => { return Ok(t) }, - Err(t) => { r = t } - }; + // Set color compatibilty + let term_colors = stdout.available_colors().unwrap_or(0); + if term_colors >= 256 { + context.config.term_color_type = 2; + } else if term_colors >= 8 { + context.config.term_color_type = 1; } else { - let x = do_expression(context, s); - match x { - Ok((t, e)) => { context.push_hist(e); return Ok(t) }, - Err(t) => { r = t } - }; + context.config.term_color_type = 0; } - let (l, e) = r; - let mut t = FormattedText::new("".to_string()); - if l.zero() { - t.push(&format!( - "\n {}\n\n", - e.text().to_string(), + context.config.check(); + + + + // Handle command-line arguments + let args: Vec = env::args().collect(); + if args.iter().any(|s| s == "--help") { + let t = command::do_command(&mut context, &String::from("help")); + t.write(&context, &mut stdout)?; + return Ok(()); + } else if args.iter().any(|s| s == "--version") { + let t = FormattedText::new(format!( + "Daisy v{}\n", env!("CARGO_PKG_VERSION") )); - } else { - t.push(&format!( + t.write(&context, &mut stdout)?; + return Ok(()); + } else if args.iter().any(|s| s == "--debug") { + let t = FormattedText::new(format!( concat!( - "{}[e]{}[n]\n", - " {}\n\n" + "Daisy v{}\n", + "Your terminal supports {} colors.\n" ), - " ".repeat(l.pos + 4), - "^".repeat(l.len), - e.text().to_string(), + env!("CARGO_PKG_VERSION"), + term_colors )); + t.write(&context, &mut stdout)?; + return Ok(()); } - return Err(t); -} + 'outer: loop { -// Handle a simple evaluation string. -// Returns a FormattedText with output that should be printed. -#[inline(always)] -fn do_expression( - context: &mut Context, - s: &String -) -> Result<(FormattedText, parser::Expression), (LineLocation, DaisyError)> { + pb.write_prompt(&mut context, &mut stdout)?; - let mut output = FormattedText::new("".to_string()); + let stdin = stdin(); + for c in stdin.keys() { + if let Key::Char(q) = c.as_ref().unwrap() { + match q { + '\n' => { + // Print again without cursor, in case we pressed enter + // while inside a substitution + pb.write_prompt_nocursor(&mut context, &mut stdout)?; + let in_str = pb.enter(); + FormattedText::newline(&mut stdout)?; + if in_str == "" { break; } - let g = parser::parse(context, &s)?; - let g_evaluated = evaluate::evaluate(context, &g)?; + if in_str.trim() == "quit" { + break 'outer; + } else { + let r = crate::do_string(&mut context, &in_str); - // Display parsed string - output.push(&format!( - " [s]=>[n] {}\n\n", - g.display(context) - )); + match r { + Ok(t) | Err(t) => { + t.write(&context, &mut stdout).unwrap(); + } + } + } - // Display result - output.push(&format!( - " [r]=[n] {}\n\n", - g_evaluated.display_outer(context), - )); + break; + }, + _ => { pb.add_char(*q); } + }; + } else { + match c.unwrap() { + Key::Backspace => { pb.backspace(); }, + Key::Delete => { pb.delete(); }, + Key::Left => { pb.cursor_left(); }, + Key::Right => { pb.cursor_right(); }, + Key::Up => { pb.hist_up(); }, + Key::Down => { pb.hist_down(); }, - return Ok((output, g_evaluated)); -} + Key::Ctrl('d') | + Key::Ctrl('c') => { break 'outer; }, + _ => {} + }; + }; - -// Handle a variable or function definition string. -// Returns a FormattedText with output that should be printed. -#[inline(always)] -fn do_assignment( - context: &mut Context, - s: &String -) -> Result { - - let mut output = FormattedText::new("".to_string()); - - let parts = s.split("=").collect::>(); - if parts.len() != 2 { - return Err(( - LineLocation::new_zero(), - DaisyError::Syntax - )); + pb.write_prompt(&mut context, &mut stdout)?; + } } - // Index of first non-whitespace character in left - // (relative to whole prompt) - let starting_left = parts[0] - .char_indices() - .find(|(_, ch)| !(ch.is_whitespace() && *ch != '\n')) - .map(|(i, _)| i) - .unwrap_or_else(|| parts[0].len()); - - // Index of first non-whitespace character in right - // (relative to whole prompt) - // +1 accounts for equals sign - let starting_right = parts[0].chars().count() + 1 + - parts[1] - .char_indices() - .find(|(_, ch)| !(ch.is_whitespace() && *ch != '\n')) - .map(|(i, _)| i) - .unwrap_or_else(|| parts[0].len()); - - - let left = substitute(context, &parts[0].trim().to_string()); - let right = substitute(context, &parts[1].trim().to_string()); - let is_function = left.contains("("); - - // The order of methods below is a bit odd. - // This is intentional, since we want to check a definition's - // variable name before even attempting to parse its content. - if is_function { - let mut mode = 0; - let mut name = String::new(); - let mut args = String::new(); - for c in left.chars() { - match mode { - - // Mode 0: reading function name - 0 => { - if c == '(' { - mode = 1; continue; - } else { name.push(c); } - }, - - // Mode 1: reading arguments - 1 => { - if c == ')' { - mode = 2; continue; - } else { args.push(c); } - }, - - // Mode 2: we should be done by now. - // That close paren should've been the last character. - 2 => { - return Err(( - LineLocation{ pos: starting_left, len: left.chars().count() }, - DaisyError::Syntax - )); - }, - - _ => unreachable!() - } - } - - - let args = args - .split(",").collect::>() - .iter().map(|x| x.trim().to_string()).collect::>(); - - if name.len() == 0 { - return Err(( - LineLocation{ pos: starting_left, len: left.chars().count() }, - DaisyError::Syntax - )); - }; - - if !context.valid_function(&name) { - return Err(( - LineLocation{ pos: starting_left, len: left.chars().count() }, - DaisyError::BadFunction - )); - }; - - if args.iter().find(|x| &x[..] == "").is_some() { - return Err(( - LineLocation{ pos: starting_left, len: left.chars().count() }, - DaisyError::Syntax - )); - }; - - for a in &args { - if !context.valid_varible(a) { - return Err(( - LineLocation{ pos: starting_left, len: left.chars().count() }, - DaisyError::BadVariable - )); - } - } - - // Parse right hand side - let g = parser::parse(context, &right); - let Ok(g) = g else { - let Err((l, e)) = g else { unreachable!() }; - return Err(( - LineLocation{ pos: l.pos + starting_right, len: l.len}, - e - )); - }; - - // Display parsed string - output.push(&format!( - " [s]=>[n] {left} = {}\n\n", - g.display(context) - )); - - // Evaluate expression with shadow variables - for a in &args { context.add_shadow(a.to_string(), None);} - let g_evaluated = evaluate::evaluate(context, &g); - context.clear_shadow(); - let Ok(_g_evaluated) = g_evaluated else { - let Err((l, e)) = g_evaluated else { unreachable!() }; - return Err(( - LineLocation{ pos: l.pos + starting_right, len: l.len}, - e - )); - }; - - // We could push g_evaluated instead, but an un-evaluated string - // makes the 'vars' command prettier. - // - // We still need to evaluate g above, though, to make sure it works. - context.push_function(name, args, g).unwrap(); - } else { - - if !context.valid_varible(&left) { - return Err(( - LineLocation{ pos: starting_left, len: left.chars().count() }, - DaisyError::BadVariable - )); - } - - // Parse right hand side - let g = parser::parse(context, &right); - let Ok(g) = g else { - let Err((l, e)) = g else { unreachable!() }; - return Err(( - LineLocation{ pos: l.pos + starting_right, len: l.len}, - e - )); - }; - - // Display parsed string - output.push(&format!( - " [t]=>[n] {left} = {}\n\n", - g.display(context) - )); - - // Evaluate expression - let g_evaluated = evaluate::evaluate(context, &g); - let Ok(g_evaluated) = g_evaluated else { - let Err((l, e)) = g_evaluated else { unreachable!() }; - return Err(( - LineLocation{ pos: l.pos + starting_right, len: l.len}, - e - )); - }; - - context.push_variable(left.to_string(), g_evaluated).unwrap(); - } - - return Ok(output); - -} - - + FormattedText::newline(&mut stdout)?; + return Ok(()); +} \ No newline at end of file diff --git a/src/entrypoint/unix/promptbuffer.rs b/src/promptbuffer.rs similarity index 95% rename from src/entrypoint/unix/promptbuffer.rs rename to src/promptbuffer.rs index 619f759..0b6bc6b 100644 --- a/src/entrypoint/unix/promptbuffer.rs +++ b/src/promptbuffer.rs @@ -1,9 +1,9 @@ use std::collections::VecDeque; use std::io::Write; use termion::raw::RawTerminal; -use crate::formattedtext; -use crate::parser::substitute_cursor; -use crate::context::Context; +use daisycalc::FormattedText; +use daisycalc::parser::substitute_cursor; +use daisycalc::Context; const PROMPT_STR: &str = "==> "; @@ -55,8 +55,8 @@ impl PromptBuffer { write!( stdout, "\r{}{PROMPT_STR}{}{}", - formattedtext::format_map('p', context).unwrap(), - formattedtext::format_map('n', context).unwrap(), + FormattedText::format_map('p', context).unwrap(), + FormattedText::format_map('n', context).unwrap(), s )?; diff --git a/src/tests.rs b/src/tests.rs index d54f10f..e765a57 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,7 +1,7 @@ // Many of these have been borrowed from insect. -use crate::parser; -use crate::evaluate::evaluate; -use crate::context::Context; +use daisycalc::parser; +use daisycalc::evaluate; +use daisycalc::Context; fn eval_to_str(s: &str) -> Result { let g = match parser::parse_no_context(&String::from(s)) {