diff --git a/lib/src/metta/runner/mod.rs b/lib/src/metta/runner/mod.rs index c418a2fb7..73f12db54 100644 --- a/lib/src/metta/runner/mod.rs +++ b/lib/src/metta/runner/mod.rs @@ -29,6 +29,15 @@ mod arithmetics; const EXEC_SYMBOL : Atom = sym!("!"); +pub fn atom_is_error(atom: &Atom) -> bool { + match atom { + Atom::Expression(expr) => { + expr.children().len() > 0 && expr.children()[0] == ERROR_SYMBOL + }, + _ => false, + } +} + #[derive(Clone, Debug, PartialEq)] pub struct Metta(Rc); @@ -36,7 +45,7 @@ pub struct Metta(Rc); pub struct MettaContents { space: DynSpace, tokenizer: Shared, - settings: Shared>, + settings: Shared>, modules: Shared>, search_paths: Vec, } @@ -141,19 +150,22 @@ impl Metta { &self.0.modules } - pub(crate) fn settings(&self) -> &Shared> { + pub fn settings(&self) -> &Shared> { &self.0.settings } - #[cfg(test)] - fn set_setting(&self, key: String, value: String) { + pub fn set_setting(&self, key: String, value: Atom) { self.0.settings.borrow_mut().insert(key, value); } - fn get_setting(&self, key: &str) -> Option { + pub fn get_setting(&self, key: &str) -> Option { self.0.settings.borrow().get(key.into()).cloned() } + pub fn get_setting_string(&self, key: &str) -> Option { + self.0.settings.borrow().get(key.into()).map(|a| a.to_string()) + } + pub fn run(&self, parser: &mut SExprParser) -> Result>, String> { let mut state = self.start_run(); @@ -182,16 +194,7 @@ impl Metta { match interpreter_state.into_result() { Err(msg) => return Err(msg), Ok(result) => { - fn is_error(atom: &Atom) -> bool { - match atom { - Atom::Expression(expr) => { - expr.children().len() > 0 && expr.children()[0] == ERROR_SYMBOL - }, - _ => false, - } - } - - let error = result.iter().any(|atom| is_error(atom)); + let error = result.iter().any(|atom| atom_is_error(atom)); state.results.push(result); if error { state.mode = MettaRunnerMode::TERMINATE; @@ -261,7 +264,7 @@ impl Metta { } fn type_check(&self, atom: Atom) -> Result { - let is_type_check_enabled = self.get_setting("type-check").map_or(false, |val| val == "auto"); + let is_type_check_enabled = self.get_setting_string("type-check").map_or(false, |val| val == "auto"); if is_type_check_enabled && !validate_atom(self.0.space.borrow().as_space(), &atom) { Err(Atom::expr([ERROR_SYMBOL, atom, BAD_TYPE_SYMBOL])) } else { @@ -337,7 +340,7 @@ mod tests { "; let metta = Metta::new(DynSpace::new(GroundingSpace::new()), Shared::new(Tokenizer::new())); - metta.set_setting("type-check".into(), "auto".into()); + metta.set_setting("type-check".into(), sym!("auto")); let result = metta.run(&mut SExprParser::new(program)); assert_eq!(result, Ok(vec![vec![expr!("Error" ("foo" "b") "BadType")]])); } @@ -351,7 +354,7 @@ mod tests { "; let metta = Metta::new(DynSpace::new(GroundingSpace::new()), Shared::new(Tokenizer::new())); - metta.set_setting("type-check".into(), "auto".into()); + metta.set_setting("type-check".into(), sym!("auto")); let result = metta.run(&mut SExprParser::new(program)); assert_eq!(result, Ok(vec![vec![expr!("Error" ("foo" "b") "BadType")]])); } @@ -406,7 +409,7 @@ mod tests { "; let metta = Metta::new(DynSpace::new(GroundingSpace::new()), Shared::new(Tokenizer::new())); - metta.set_setting("type-check".into(), "auto".into()); + metta.set_setting("type-check".into(), sym!("auto")); let result = metta.run(&mut SExprParser::new(program)); assert_eq!(result, Ok(vec![vec![expr!("Error" ("foo" "b") "BadType")]])); } diff --git a/lib/src/metta/runner/stdlib.rs b/lib/src/metta/runner/stdlib.rs index 94ec8b32b..46744b516 100644 --- a/lib/src/metta/runner/stdlib.rs +++ b/lib/src/metta/runner/stdlib.rs @@ -10,6 +10,7 @@ use crate::common::shared::Shared; use crate::common::assert::vec_eq_no_order; use crate::common::ReplacingMapper; +use std::convert::TryFrom; use std::rc::Rc; use std::cell::RefCell; use std::fmt::Display; @@ -170,14 +171,6 @@ impl Display for BindOp { } } -// TODO: move it into hyperon::atom module? -fn atom_as_sym(atom: &Atom) -> Option<&SymbolAtom> { - match atom { - Atom::Symbol(sym) => Some(sym), - _ => None, - } -} - impl Grounded for BindOp { fn type_(&self) -> Atom { Atom::expr([ARROW_SYMBOL, ATOM_TYPE_SYMBOL, ATOM_TYPE_UNDEFINED, ATOM_TYPE_UNDEFINED]) @@ -185,7 +178,7 @@ impl Grounded for BindOp { fn execute(&self, args: &[Atom]) -> Result, ExecError> { let arg_error = || ExecError::from("bind! expects two arguments: token and atom"); - let token = atom_as_sym(args.get(0).ok_or_else(arg_error)?).ok_or("bind! expects symbol atom as a token")?.name(); + let token = <&SymbolAtom>::try_from(args.get(0).ok_or_else(arg_error)?).map_err(|_| "bind! expects symbol atom as a token")?.name(); let atom = args.get(1).ok_or_else(arg_error)?.clone(); let token_regex = Regex::new(token).map_err(|err| format!("Could convert token {} into regex: {}", token, err))?; @@ -678,11 +671,11 @@ impl Grounded for SuperposeOp { #[derive(Clone, PartialEq, Debug)] pub struct PragmaOp { - settings: Shared>, + settings: Shared>, } impl PragmaOp { - pub fn new(settings: Shared>) -> Self { + pub fn new(settings: Shared>) -> Self { Self{ settings } } } @@ -700,12 +693,9 @@ impl Grounded for PragmaOp { fn execute(&self, args: &[Atom]) -> Result, ExecError> { let arg_error = || ExecError::from("pragma! expects key and value as arguments"); - let key = atom_as_sym(args.get(0).ok_or_else(arg_error)?).ok_or("pragma! expects symbol atom as a key")?.name(); - let value = atom_as_sym(args.get(1).ok_or_else(arg_error)?).ok_or("pragma! expects symbol atom as a value")?.name(); - - // TODO: add support for Grounded values when needed - self.settings.borrow_mut().insert(key.into(), value.into()); - + let key = <&SymbolAtom>::try_from(args.get(0).ok_or_else(arg_error)?).map_err(|_| "pragma! expects symbol atom as a key")?.name(); + let value = args.get(1).ok_or_else(arg_error)?; + self.settings.borrow_mut().insert(key.into(), value.clone()); Ok(vec![]) } diff --git a/lib/src/metta/runner/stdlib2.rs b/lib/src/metta/runner/stdlib2.rs index ce7f2d69b..2a0659047 100644 --- a/lib/src/metta/runner/stdlib2.rs +++ b/lib/src/metta/runner/stdlib2.rs @@ -293,11 +293,11 @@ impl Grounded for CollapseOp { #[derive(Clone, PartialEq, Debug)] pub struct PragmaOp { - settings: Shared>, + settings: Shared>, } impl PragmaOp { - pub fn new(settings: Shared>) -> Self { + pub fn new(settings: Shared>) -> Self { Self{ settings } } } @@ -317,12 +317,8 @@ impl Grounded for PragmaOp { let arg_error = || ExecError::from("pragma! expects key and value as arguments"); let key = TryInto::<&SymbolAtom>::try_into(args.get(0).ok_or_else(arg_error)?) .map_err(|_| "pragma! expects symbol atom as a key")?.name(); - let value = TryInto::<&SymbolAtom>::try_into(args.get(1).ok_or_else(arg_error)?) - .map_err(|_| "pragma! expects symbol atom as a value")?.name(); - - // TODO: add support for Grounded values when needed - self.settings.borrow_mut().insert(key.into(), value.into()); - + let value = args.get(1).ok_or_else(arg_error)?; + self.settings.borrow_mut().insert(key.into(), value.clone()); Ok(vec![]) } diff --git a/repl/Cargo.toml b/repl/Cargo.toml index b787a6811..157439a6c 100644 --- a/repl/Cargo.toml +++ b/repl/Cargo.toml @@ -15,7 +15,7 @@ clap = { version = "4.4.0", features = ["derive"] } directories = "5.0.1" signal-hook = "0.3.17" pyo3 = { version = "0.19.2", features = ["auto-initialize"], optional = true } -semver = { version = "1.0.18", optional = true } +pep440_rs = { version = "0.3.11", optional = true } [[bin]] name = "metta" @@ -23,5 +23,5 @@ path = "src/main.rs" [features] # default = ["python", "minimal"] -python = ["pyo3", "semver"] +python = ["pyo3", "pep440_rs"] minimal = ["hyperon/minimal"] diff --git a/repl/src/config.default.metta b/repl/src/config.default.metta deleted file mode 100644 index 73247a498..000000000 --- a/repl/src/config.default.metta +++ /dev/null @@ -1,16 +0,0 @@ - -; TODO: Let the "includePaths" be modifiable, but I want better string manipulation atoms - -(= ReplDefaultPrompt "> ") -; (= ReplStyledPrompt "\x1b[1;32m> \x1b[0m") ; TODO: currently the MeTTa string parser doesn't resolve escape chars, although perhaps it should - -; TODO: somebody with better design sense should tweak these, and also provide dark-mode setings -; ANSI escape codes to configure the syntax highlighter -(= ReplBracketStyles ("94" "93" "95" "96")) -(= ReplCommentStyle "32") -(= ReplVariableStyle "33") -(= ReplSymbolStyle "34") -(= ReplStringStyle "31") -(= ReplErrorStyle "91") -(= ReplBracketMatchStyle "1;7") -; (= ReplBracketMatchEnabled True) ;TODO: enable this when I have a reliable value interchange path built. Another use for https://github.com/trueagi-io/hyperon-experimental/issues/351 diff --git a/repl/src/config_params.rs b/repl/src/config_params.rs index 3b3f29da3..ae4414607 100644 --- a/repl/src/config_params.rs +++ b/repl/src/config_params.rs @@ -3,18 +3,34 @@ use std::path::{Path, PathBuf}; use std::io::Write; use std::fs; -const DEFAULT_CONFIG_METTA: &[u8] = include_bytes!("config.default.metta"); +const DEFAULT_INIT_METTA: &[u8] = include_bytes!("init.default.metta"); +const DEFAULT_REPL_METTA: &[u8] = include_bytes!("repl.default.metta"); + +pub const CFG_PROMPT: &str = "&ReplPrompt"; +pub const CFG_STYLED_PROMPT: &str = "&ReplStyledPrompt"; +pub const CFG_BRACKET_STYLES: &str = "&ReplBracketStyles"; +pub const CFG_COMMENT_STYLE: &str = "&ReplCommentStyle"; +pub const CFG_VARIABLE_STYLE: &str = "&ReplVariableStyle"; +pub const CFG_SYMBOL_STYLE: &str = "&ReplSymbolStyle"; +pub const CFG_STRING_STYLE: &str = "&ReplStringStyle"; +pub const CFG_ERROR_STYLE: &str = "&ReplErrorStyle"; +pub const CFG_BRACKET_MATCH_STYLE: &str = "&ReplBracketMatchStyle"; +pub const CFG_BRACKET_MATCH_ENABLED: &str = "&ReplBracketMatchEnabled"; +pub const CFG_HISTORY_MAX_LEN: &str = "&ReplHistoryMaxLen"; #[derive(Default, Debug)] pub struct ReplParams { /// Path to the config dir for the whole repl, in an OS-specific location - config_dir: PathBuf, + pub config_dir: PathBuf, - /// Path to the config.metta file, used to configure the repl - config_metta_path: PathBuf, + /// A path to the init.metta file that's run to customize the MeTTa environment + pub init_metta_path: PathBuf, + + /// A path to the repl.metta file that's run to configure the repl environment + pub repl_config_metta_path: PathBuf, /// Path to the dir containing the script being run, or the cwd the repl was invoked from in interactive mode - metta_working_dir: PathBuf, + pub metta_working_dir: PathBuf, /// Other include paths, specified either through command-line args or config settings include_paths: Vec, @@ -44,15 +60,24 @@ impl ReplParams { let modules_dir = config_dir.join("modules"); std::fs::create_dir_all(&modules_dir).unwrap(); - //Create the default config.metta file, if none exists - let config_metta_path = config_dir.join("config.metta"); - if !config_metta_path.exists() { + //Create the default init.metta file and repl.meta file, if they don't already exist + let init_metta_path = config_dir.join("init.metta"); + if !init_metta_path.exists() { + let mut file = fs::OpenOptions::new() + .create(true) + .write(true) + .open(&init_metta_path) + .expect(&format!("Error creating default init file at {init_metta_path:?}")); + file.write_all(&DEFAULT_INIT_METTA).unwrap(); + } + let repl_config_metta_path = config_dir.join("repl.metta"); + if !repl_config_metta_path.exists() { let mut file = fs::OpenOptions::new() .create(true) .write(true) - .open(&config_metta_path) - .expect(&format!("Error creating default config file at {config_metta_path:?}")); - file.write_all(&DEFAULT_CONFIG_METTA).unwrap(); + .open(&repl_config_metta_path) + .expect(&format!("Error creating default repl config file at {repl_config_metta_path:?}")); + file.write_all(&DEFAULT_REPL_METTA).unwrap(); } //Push the "modules" dir, as the last place to search after the paths specified on the cmd line @@ -63,7 +88,8 @@ impl ReplParams { Self { config_dir: config_dir.into(), - config_metta_path, + init_metta_path, + repl_config_metta_path, metta_working_dir, include_paths, history_file: Some(config_dir.join("history.txt")), @@ -77,14 +103,21 @@ impl ReplParams { [self.metta_working_dir.clone()].into_iter().chain( self.include_paths.iter().cloned()) } +} - /// A path to the config.metta file that's run to configure the repl environment - pub fn config_metta_path(&self) -> &PathBuf { - - //TODO: Temporary access to avoid warning. Delete soon - let _ = self.config_dir; - - &self.config_metta_path - } - +/// Returns the MeTTa code to init the Repl's MeTTa params and set them to default values +pub fn builtin_init_metta_code() -> String { + format!(r#" + ; !(bind! {CFG_HISTORY_MAX_LEN} (new-state 500)) ; TODO, enable this when value-bridging is implemented + !(bind! {CFG_PROMPT} (new-state "> ")) + ; !(bind! {CFG_STYLED_PROMPT} (new-state (concat "\x1b[1;32m" (get-state {CFG_PROMPT}) "\x1b[0m"))) ;TODO, two things before this works. Escape parsing in string literals, and string stdlib type with concat operation + !(bind! {CFG_BRACKET_STYLES} (new-state ("94" "93" "95" "96"))) + !(bind! {CFG_COMMENT_STYLE} (new-state "32")) + !(bind! {CFG_VARIABLE_STYLE} (new-state "33")) + !(bind! {CFG_SYMBOL_STYLE} (new-state "34")) + !(bind! {CFG_STRING_STYLE} (new-state "31")) + !(bind! {CFG_ERROR_STYLE} (new-state "91")) + !(bind! {CFG_BRACKET_MATCH_STYLE} (new-state "1;7")) + ; !(bind! {CFG_BRACKET_MATCH_ENABLED} (new-state True)) ; TODO, enable this when value-bridging is implemented + "#) } diff --git a/repl/src/init.default.metta b/repl/src/init.default.metta new file mode 100644 index 000000000..04e8c3752 --- /dev/null +++ b/repl/src/init.default.metta @@ -0,0 +1,2 @@ + +; TODO: Let the "importPaths" be modifiable, but I want better string manipulation atoms diff --git a/repl/src/interactive_helper.rs b/repl/src/interactive_helper.rs index 60e7f8fc9..af784ecff 100644 --- a/repl/src/interactive_helper.rs +++ b/repl/src/interactive_helper.rs @@ -1,5 +1,6 @@ use std::borrow::Cow::{self, Borrowed, Owned}; +use std::sync::{Arc, Mutex}; use std::cell::RefCell; use rustyline::completion::FilenameCompleter; @@ -11,6 +12,7 @@ use rustyline::{Completer, Helper, Hinter}; use hyperon::metta::text::{SExprParser, SyntaxNodeType}; +use crate::config_params::*; use crate::metta_shim::MettaShim; #[derive(Helper, Completer, Hinter)] @@ -22,7 +24,8 @@ pub struct ReplHelper { hinter: HistoryHinter, pub colored_prompt: String, cursor_bracket: std::cell::Cell>, // If the cursor is over or near a bracket to match - checked_line: std::cell::RefCell, + pub force_submit: Arc>, // We use this to communicate between the key event handler and the Validator + checked_line: RefCell, style: StyleSettings, } @@ -35,7 +38,7 @@ struct StyleSettings { string_style: String, error_style: String, bracket_match_style: String, - // bracket_match_enabled: bool, //TODO + bracket_match_enabled: bool, } impl Highlighter for ReplHelper { @@ -117,9 +120,11 @@ impl Highlighter for ReplHelper { } //See if we need to render this node with the "bracket blink" - if let Some((_matching_char, blink_idx)) = &blink_char { - if node.src_range.contains(blink_idx) { - style_sequence.push(&self.style.bracket_match_style); + if self.style.bracket_match_enabled { + if let Some((_matching_char, blink_idx)) = &blink_char { + if node.src_range.contains(blink_idx) { + style_sequence.push(&self.style.bracket_match_style); + } } } @@ -167,9 +172,13 @@ impl Validator for ReplHelper { fn validate(&self, ctx: &mut ValidationContext) -> Result { //This validator implements the following behavior: - // * if user hits enter and line is valid, it will be submitted. - // * if user hits enter and line is invalid, it will treat it as a newline - // * If user hits enter twice in a row, it will report a syntax error + // * if user hits enter and the line is valid, and the cursor is at the end of the line, it will be submitted. + // * if user hits ctrl-J (force submit) and the line is valid, it will be submitted regardless of cursor position + // * if user hits enter and the line is invalid, a newline will be inserted at the cursor position, unless + // a linefeed has just been added to the end of the line, in which case a syntax error is reported + // * if user hits ctrl-J (force submit) and line is invalid, it will be a syntax error, regardless of cursor position + let force_submit = *self.force_submit.lock().unwrap(); + *self.force_submit.lock().unwrap() = false; let mut validation_result = ValidationResult::Incomplete; self.metta.borrow_mut().inside_env(|metta| { let mut parser = SExprParser::new(ctx.input()); @@ -188,7 +197,8 @@ impl Validator for ReplHelper { if input.len() < 1 { break; } - if *self.checked_line.borrow() != &input[0..input.len()-1] || input.as_bytes()[input.len()-1] != b'\n' { + if !force_submit && + (*self.checked_line.borrow() != &input[0..input.len()-1] || input.as_bytes()[input.len()-1] != b'\n') { *self.checked_line.borrow_mut() = ctx.input().to_string(); } else { validation_result = ValidationResult::Invalid(Some( @@ -216,22 +226,25 @@ impl ReplHelper { hinter: HistoryHinter {}, colored_prompt: "".to_owned(), cursor_bracket: std::cell::Cell::new(None), - checked_line: std::cell::RefCell::new(String::new()), + force_submit: Arc::new(Mutex::new(false)), + checked_line: RefCell::new(String::new()), style, } } } impl StyleSettings { + const ERR_STR: &str = "Fatal Error: Invalid REPL config"; pub fn new(metta_shim: &mut MettaShim) -> Self { Self { - bracket_styles: metta_shim.get_config_expr_vec("ReplBracketStyles").unwrap_or(vec!["94".to_string(), "93".to_string(), "95".to_string(), "96".to_string()]), - comment_style: metta_shim.get_config_string("ReplCommentStyle").unwrap_or("32".to_string()), - variable_style: metta_shim.get_config_string("ReplVariableStyle").unwrap_or("33".to_string()), - symbol_style: metta_shim.get_config_string("ReplSymbolStyle").unwrap_or("34".to_string()), - string_style: metta_shim.get_config_string("ReplStringStyle").unwrap_or("31".to_string()), - error_style: metta_shim.get_config_string("ReplErrorStyle").unwrap_or("91".to_string()), - bracket_match_style: metta_shim.get_config_string("ReplBracketMatchStyle").unwrap_or("1;7".to_string()), + bracket_styles: metta_shim.get_config_expr_vec(CFG_BRACKET_STYLES).expect(Self::ERR_STR), + comment_style: metta_shim.get_config_string(CFG_COMMENT_STYLE).expect(Self::ERR_STR), + variable_style: metta_shim.get_config_string(CFG_VARIABLE_STYLE).expect(Self::ERR_STR), + symbol_style: metta_shim.get_config_string(CFG_SYMBOL_STYLE).expect(Self::ERR_STR), + string_style: metta_shim.get_config_string(CFG_STRING_STYLE).expect(Self::ERR_STR), + error_style: metta_shim.get_config_string(CFG_ERROR_STYLE).expect(Self::ERR_STR), + bracket_match_style: metta_shim.get_config_string(CFG_BRACKET_MATCH_STYLE).expect(Self::ERR_STR), + bracket_match_enabled: metta_shim.get_config_atom(CFG_BRACKET_MATCH_ENABLED).map(|_bool_atom| true).unwrap_or(true), //TODO, make this work when we can bridge value atoms } } } diff --git a/repl/src/main.rs b/repl/src/main.rs index e93620a27..1c52c1faa 100644 --- a/repl/src/main.rs +++ b/repl/src/main.rs @@ -2,10 +2,10 @@ use std::path::PathBuf; use std::thread; use std::process::exit; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; use rustyline::error::ReadlineError; -use rustyline::{Cmd, CompletionType, Config, EditMode, Editor, KeyEvent, KeyCode, Modifiers}; +use rustyline::{Cmd, CompletionType, Config, EditMode, Editor, KeyEvent, KeyCode, Modifiers, EventContext, RepeatCount, EventHandler, ConditionalEventHandler, Event}; use anyhow::Result; use clap::Parser; @@ -109,11 +109,20 @@ fn main() -> Result<()> { // To debug rustyline: // RUST_LOG=rustyline=debug cargo run --example example 2> debug.log -fn start_interactive_mode(repl_params: Shared, metta: MettaShim) -> rustyline::Result<()> { +fn start_interactive_mode(repl_params: Shared, mut metta: MettaShim) -> rustyline::Result<()> { + + //Run the built-in repl-init code + metta.exec(&builtin_init_metta_code()); + + //Run the repl init file + metta.load_metta_module(repl_params.borrow().repl_config_metta_path.clone()); + let max_len = metta.get_config_int(CFG_HISTORY_MAX_LEN).unwrap_or_else(|| 500); //Init RustyLine let config = Config::builder() .history_ignore_space(true) + .max_history_size(max_len as usize).unwrap() + .history_ignore_dups(false).unwrap() .completion_type(CompletionType::List) .edit_mode(EditMode::Emacs) .build(); @@ -125,16 +134,11 @@ fn start_interactive_mode(repl_params: Shared, metta: MettaShim) -> rl.set_helper(Some(helper)); //KEY BEHAVIOR: Enter and ctrl-M will add a newline when the cursor is in the middle of a line, while // ctrl-J will submit the line. - //TODO: Rustyline seems to have a bug where this is only true sometimes. Needs to be debugged. - // Ideally Rustyline could just subsume the whole "accept_in_the_middle" behavior with a design that - // allows the Validator to access the key event, so the Validator could make the decision without - // special logic inside rustyline. + //TODO: Document this behavior in the README.md, and possibly other key bindings also rl.bind_sequence(KeyEvent( KeyCode::Enter, Modifiers::NONE ), Cmd::AcceptOrInsertLine { accept_in_the_middle: false, }); - rl.bind_sequence(KeyEvent::ctrl('j'), Cmd::AcceptOrInsertLine { - accept_in_the_middle: true, - }); + rl.bind_sequence(KeyEvent::ctrl('j'), EventHandler::Conditional(Box::new(EnterKeyHandler::new(rl.helper().unwrap().force_submit.clone())))); rl.bind_sequence(KeyEvent::alt('n'), Cmd::HistorySearchForward); rl.bind_sequence(KeyEvent::alt('p'), Cmd::HistorySearchBackward); if let Some(history_path) = &repl_params.borrow().history_file { @@ -146,12 +150,12 @@ fn start_interactive_mode(repl_params: Shared, metta: MettaShim) -> //The Interpreter Loop loop { - //Set the prompt based on resolving a MeTTa variable + //Set the prompt based on the MeTTa pragma settings let prompt = { let helper = rl.helper_mut().unwrap(); let mut metta = helper.metta.borrow_mut(); - let prompt = metta.get_config_string("ReplDefaultPrompt").unwrap_or("> ".to_string()); - let styled_prompt = metta.get_config_string("ReplStyledPrompt").unwrap_or(format!("\x1b[1;32m{prompt}\x1b[0m")); + let prompt = metta.get_config_string(CFG_PROMPT).expect("Fatal Error: Invalid REPL config"); + let styled_prompt = metta.get_config_string(CFG_STYLED_PROMPT).unwrap_or_else(|| format!("\x1b[1;32m{prompt}\x1b[0m")); //TODO, Let this default be set inside builtin_init_metta_code() when strings can be parsed with escape chars helper.colored_prompt = styled_prompt; prompt }; @@ -186,3 +190,30 @@ fn start_interactive_mode(repl_params: Shared, metta: MettaShim) -> Ok(()) } + +struct EnterKeyHandler { + force_submit: Arc> +} + +impl EnterKeyHandler { + fn new(force_submit: Arc>) -> Self { + Self { + force_submit + } + } +} + +impl ConditionalEventHandler for EnterKeyHandler { + fn handle( + &self, + _evt: &Event, + _n: RepeatCount, + _positive: bool, + _ctx: &EventContext<'_> + ) -> Option { + *self.force_submit.lock().unwrap() = true; + Some(Cmd::AcceptOrInsertLine { + accept_in_the_middle: true, + }) + } +} diff --git a/repl/src/metta_shim.rs b/repl/src/metta_shim.rs index 7efb6ef8b..d14c89c0f 100644 --- a/repl/src/metta_shim.rs +++ b/repl/src/metta_shim.rs @@ -3,11 +3,9 @@ use std::path::PathBuf; use hyperon::ExpressionAtom; use hyperon::Atom; -use hyperon::atom::VariableAtom; use hyperon::space::*; use hyperon::space::grounding::GroundingSpace; -use hyperon::metta::*; -use hyperon::metta::runner::Metta; +use hyperon::metta::runner::{Metta, atom_is_error}; #[cfg(not(feature = "minimal"))] use hyperon::metta::runner::stdlib::register_rust_tokens; #[cfg(feature = "minimal")] @@ -74,12 +72,17 @@ impl MettaShim { //Init HyperonPy if the repl includes Python support #[cfg(feature = "python")] - { + if let Err(err) = || -> Result<(), String> { //Confirm the hyperonpy version is compatible - py_mod_loading::confirm_hyperonpy_version(">=0.1.0, <0.2.0").unwrap(); + py_mod_loading::confirm_hyperonpy_version(">=0.1.0, <0.2.0")?; //Load the hyperonpy Python stdlib - py_mod_loading::load_python_module(&new_shim.metta, "hyperon.stdlib").unwrap(); + py_mod_loading::load_python_module(&new_shim.metta, "hyperon.stdlib")?; + + Ok(()) + }() { + eprintln!("Fatal Error: {err}"); + std::process::exit(-1); } //Load the Rust stdlib @@ -97,10 +100,9 @@ impl MettaShim { #[cfg(not(feature = "python"))] new_shim.metta.tokenizer().borrow_mut().register_token_with_regex_str("extend-py!", move |_| { Atom::gnd(py_mod_err::ImportPyErr) }); - //Run the config.metta file + //Run the init.metta file let repl_params = repl_params.borrow(); - let config_metta_path = repl_params.config_metta_path(); - new_shim.load_metta_module(config_metta_path.clone()); + new_shim.load_metta_module(repl_params.init_metta_path.clone()); new_shim } @@ -146,13 +148,15 @@ impl MettaShim { } pub fn get_config_atom(&mut self, config_name: &str) -> Option { + self.exec(&format!("!(get-state {config_name})")); + + #[allow(unused_assignments)] let mut result = None; metta_shim_env!{{ - let val = VariableAtom::new("val"); - let bindings_set = self.metta.space().query(&Atom::expr(vec![EQUAL_SYMBOL, Atom::sym(config_name.to_string()), Atom::Variable(val.clone())])); - if let Some(bindings) = bindings_set.into_iter().next() { - result = bindings.resolve(&val); - } + result = self.result.get(0) + .and_then(|vec| vec.get(0)) + .and_then(|atom| (!atom_is_error(atom)).then_some(atom)) + .cloned() }} result } @@ -163,6 +167,7 @@ impl MettaShim { #[allow(unused_assignments)] let mut result = None; metta_shim_env!{{ + //TODO: We need to do atom type checking here result = Some(Self::strip_quotes(atom.to_string())); }} result @@ -193,13 +198,19 @@ impl MettaShim { if let Ok(expr) = ExpressionAtom::try_from(atom) { result = Some(expr.into_children() .into_iter() - .map(|atom| Self::strip_quotes(atom.to_string())) + .map(|atom| { + //TODO: We need to do atom type checking here + Self::strip_quotes(atom.to_string()) + }) .collect()) } }} result } + pub fn get_config_int(&mut self, _config_name: &str) -> Option { + None //TODO. Make this work when I have reliable value atom bridging + } } #[cfg(not(feature = "python"))] @@ -237,8 +248,9 @@ mod py_mod_err { #[cfg(feature = "python")] mod py_mod_loading { use std::fmt::Display; + use std::str::FromStr; use std::path::PathBuf; - use semver::{Version, VersionReq}; + use pep440_rs::{parse_version_specifiers, Version}; use pyo3::prelude::*; use pyo3::types::{PyTuple, PyDict}; use hyperon::*; @@ -263,13 +275,16 @@ mod py_mod_loading { pub fn confirm_hyperonpy_version(req_str: &str) -> Result<(), String> { - let req = VersionReq::parse(req_str).unwrap(); + let req = parse_version_specifiers(req_str).unwrap(); let version_string = get_hyperonpy_version()?; - let version = Version::parse(&version_string).map_err(|e| format!("Error parsing HyperonPy version: '{version_string}', {e}"))?; - if req.matches(&version) { + //NOTE: Version parsing errors will be encountered by users building hyperonpy from source with an abnormal configuration + // Therefore references to the "hyperon source directory" are ok. Users who get hyperonpy from PyPi won't hit this issue + let version = Version::from_str(&version_string) + .map_err(|_e| format!("Invalid HyperonPy version found: '{version_string}'.\nPlease update the package by running `python -m pip install -e ./python[dev]` from your hyperon source directory."))?; + if req.iter().all(|specifier| specifier.contains(&version)) { Ok(()) } else { - Err(format!("MeTTa repl requires HyperonPy version matching '{req}'. Found version: '{version}'")) + Err(format!("MeTTa repl requires HyperonPy version matching '{req_str}'. Found version: '{version}'")) } } diff --git a/repl/src/repl.default.metta b/repl/src/repl.default.metta new file mode 100644 index 000000000..7bf6499c8 --- /dev/null +++ b/repl/src/repl.default.metta @@ -0,0 +1,16 @@ + +; !(change-state! &ReplHistoryMaxLen 500) ; TODO: enable this when I have value bridging implemented. + +!(change-state! &ReplPrompt "> ") +; !(change-state! &ReplStyledPrompt "\x1b[1;32m> \x1b[0m") ; TODO: currently the MeTTa string parser doesn't resolve escape chars, although perhaps it should + +; TODO: somebody with better design sense should tweak these, and also provide dark-mode setings +; ANSI escape codes to configure the syntax highlighter +!(change-state! &ReplBracketStyles ("36" "35" "33" "32")) ; 36: cyan, 35: magenta, 33: yellow, 32: green +!(change-state! &ReplCommentStyle "90") ; 90: light gray +!(change-state! &ReplVariableStyle "33") ; 33: yellow +!(change-state! &ReplSymbolStyle "0") ; 0: default text (as per terminal setting) +!(change-state! &ReplStringStyle "31") ; 31: dark red +!(change-state! &ReplErrorStyle "91") ; 91: bright red +!(change-state! &ReplBracketMatchStyle "1;7") ; 1: bold, 7: reverse (swap background and foreground colors) +; !(change-state! &ReplBracketMatchEnabled True) ;TODO: enable this when I have a reliable value interchange path built. Another use for https://github.com/trueagi-io/hyperon-experimental/issues/351