Repository: sharkdp/numbat (rust)

Issue:
Add help command
I propose to add a `help` command (similar to insect), that will show examples of 
1. How to define new variables and units
2. How to load unused modules (like `use units::stoney`)
3. basic things like https://github.com/sharkdp/numbat/raw/master/assets/numbat-interactive.png

And it should have a link to the documentation https://numbat.dev/doc/

Pull request description:
add `help` (and `?`) commands to numbat REPL
This resolves #175 .

I implemented an `insect`-like `help` command for the REPL (with `?` as an alias) by writing the package description, running a few examples through the REPL itself and then printing the link to the online documentation. The examples are printed along with the prompt to show the user how it would look and then the result of the examples are printed just like any other user command to show the user how it would look on their computer.

I like this so that the output of the examples are automatically updated along with any output formatting changes in numbat, but this has the downside of potentially polluting the user namespace if any of the examples have `let` or `use` in them. For now, I just avoid using those commands in the examples.

Patch:
diff --git a/README.md b/README.md
index 8ff063d..4f9e87e 100644
--- a/README.md
+++ b/README.md
@@ -167,3 +167,9 @@ you don't have to recompile Numbat to see your changes.
 Alternatively, you can create a symlink from `~/.config/numbat/modules` to the `numbat/modules/`
 folder in the repository (see [this page](https://numbat.dev/doc/cli-customization.html#module-paths)
 for the standard paths on other operating systems).
+
+## Contact us
+
+To contact us, either [open a GitHub issue](https://github.com/sharkdp/numbat/issues/new/choose)
+or [discussion](https://github.com/sharkdp/numbat/discussions), or pop into `#numbat` on
+[Libera.Chat](https://libera.chat) ([link to webchat](https://web.libera.chat/gamja/#numbat)).
diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md
index 896086c..cfba8b7 100644
--- a/book/src/SUMMARY.md
+++ b/book/src/SUMMARY.md
@@ -56,3 +56,7 @@
 # Notes for developers
 
 - [Type system](./type-system.md)
+
+# Support
+
+- [Contact us](./contact-us.md)
diff --git a/book/src/cli-usage.md b/book/src/cli-usage.md
index ff124b2..9db423a 100644
--- a/book/src/cli-usage.md
+++ b/book/src/cli-usage.md
@@ -37,6 +37,7 @@ There is a set of special commands that only work in interactive mode:
 |---------|--------|
 | `list`, `ll`, `ls` | List all constants, units, and dimensions |
 | `clear` | Clear screen |
+| `help`, `?` | View short help text |
 | `quit`, `exit` | Quit the session |
 
 ### Key bindings
diff --git a/book/src/contact-us.md b/book/src/contact-us.md
new file mode 100644
index 0000000..ae4540c
--- /dev/null
+++ b/book/src/contact-us.md
@@ -0,0 +1,6 @@
+# Contact us
+
+To contact us, either [open a GitHub issue](https://github.com/sharkdp/numbat/issues/new/choose)
+or [discussion](https://github.com/sharkdp/numbat/discussions), or pop into
+`#numbat` on [Libera.Chat](https://libera.chat) ([link
+to webchat](https://web.libera.chat/gamja/#numbat)).
diff --git a/book/src/web-usage.md b/book/src/web-usage.md
index ed8fcb1..6363562 100644
--- a/book/src/web-usage.md
+++ b/book/src/web-usage.md
@@ -25,6 +25,7 @@ There is a set of special commands that only work in the web version:
 | Command | Action |
 |---------|--------|
 | `list`, `ll`, `ls` | List all constants, units, and dimensions |
+| `help`, `?` | View short help text |
 | `reset` | Reset state (clear constants, functions, units, …) |
 | `clear` | Clear screen |
 
diff --git a/numbat-cli/Cargo.toml b/numbat-cli/Cargo.toml
index b080ef9..bf5befe 100644
--- a/numbat-cli/Cargo.toml
+++ b/numbat-cli/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "numbat-cli"
-description = " A statically typed programming language for scientific computations with first class support for physical dimensions and units"
+description = "A statically typed programming language for scientific computations with first class support for physical dimensions and units."
 authors = ["David Peter <mail@david-peter.de>"]
 categories = ["command-line-utilities", "science", "mathematics", "compilers"]
 keywords = ["language", "compiler", "physics", "units", "calculation"]
diff --git a/numbat-cli/src/main.rs b/numbat-cli/src/main.rs
index 430c9bf..5a2e28f 100644
--- a/numbat-cli/src/main.rs
+++ b/numbat-cli/src/main.rs
@@ -8,12 +8,13 @@ use highlighter::NumbatHighlighter;
 
 use itertools::Itertools;
 use numbat::diagnostic::ErrorDiagnostic;
+use numbat::help::help_markup;
 use numbat::markup as m;
 use numbat::module_importer::{BuiltinModuleImporter, ChainedImporter, FileSystemImporter};
 use numbat::pretty_print::PrettyPrint;
 use numbat::resolver::CodeSource;
-use numbat::{Context, ExitStatus, InterpreterResult, NumbatError};
-use numbat::{InterpreterSettings, NameResolutionError, Type};
+use numbat::{Context, InterpreterResult, NumbatError};
+use numbat::{InterpreterSettings, NameResolutionError};
 
 use anyhow::{bail, Context as AnyhowContext, Result};
 use clap::{Parser, ValueEnum};
@@ -29,7 +30,13 @@ use std::path::PathBuf;
 use std::sync::{Arc, Mutex};
 use std::{fs, thread};
 
-type ControlFlow = std::ops::ControlFlow<numbat::ExitStatus>;
+#[derive(Debug, PartialEq, Eq)]
+pub enum ExitStatus {
+    Success,
+    Error,
+}
+
+type ControlFlow = std::ops::ControlFlow<ExitStatus>;
 
 const PROMPT: &str = ">>> ";
 
@@ -284,6 +291,14 @@ impl Cli {
                             "quit" | "exit" => {
                                 return Ok(());
                             }
+                            "help" | "?" => {
+                                let help = help_markup();
+                                print!("{}", ansi_format(&help, true));
+                                // currently, the ansi formatter adds indents
+                                // _after_ each newline and so we need to manually
+                                // add an extra blank line to absorb this indent
+                                println!();
+                            }
                             _ => {
                                 let result = self.parse_and_evaluate(
                                     &line,
@@ -371,36 +386,15 @@ impl Cli {
                     println!();
                 }
 
+                let result_markup = interpreter_result.to_markup(statements.last(), &registry);
+                print!("{}", ansi_format(&result_markup, false));
+
                 match interpreter_result {
-                    InterpreterResult::Value(value) => {
-                        let type_ = statements.last().map_or(m::empty(), |s| {
-                            if let numbat::Statement::Expression(e) = s {
-                                let type_ = e.get_type();
-
-                                if type_ == Type::scalar() {
-                                    m::empty()
-                                } else {
-                                    m::dimmed("    [")
-                                        + e.get_type().to_readable_type(&registry)
-                                        + m::dimmed("]")
-                                }
-                            } else {
-                                m::empty()
-                            }
-                        });
-
-                        let q_markup = m::whitespace("    ")
-                            + m::operator("=")
-                            + m::space()
-                            + value.pretty_print()
-                            + type_;
-                        println!("{}", ansi_format(&q_markup, false));
+                    InterpreterResult::Value(_) => {
                         println!();
-
                         ControlFlow::Continue(())
                     }
                     InterpreterResult::Continue => ControlFlow::Continue(()),
-                    InterpreterResult::Exit(exit_status) => ControlFlow::Break(exit_status),
                 }
             }
             Err(NumbatError::ResolverError(e)) => {
diff --git a/numbat-wasm/src/lib.rs b/numbat-wasm/src/lib.rs
index 6b5fd16..b5638c0 100644
--- a/numbat-wasm/src/lib.rs
+++ b/numbat-wasm/src/lib.rs
@@ -6,13 +6,14 @@ use numbat::module_importer::BuiltinModuleImporter;
 use std::sync::{Arc, Mutex};
 use wasm_bindgen::prelude::*;
 
-use jquery_terminal_formatter::{jt_format, JqueryTerminalFormatter};
+use jquery_terminal_formatter::JqueryTerminalFormatter;
 
+use numbat::help::help_markup;
 use numbat::markup::Formatter;
 use numbat::pretty_print::PrettyPrint;
 use numbat::resolver::CodeSource;
-use numbat::{markup as m, NameResolutionError, NumbatError, Type};
-use numbat::{Context, InterpreterResult, InterpreterSettings};
+use numbat::{markup as m, NameResolutionError, NumbatError};
+use numbat::{Context, InterpreterSettings};
 
 use crate::jquery_terminal_formatter::JqueryTerminalWriter;
 
@@ -98,42 +99,8 @@ impl Numbat {
                     output.push_str("\n");
                 }
 
-                match result {
-                    InterpreterResult::Value(value) => {
-                        if !to_be_printed.is_empty() {
-                            output.push_str("\n");
-                        }
-
-                        // TODO: the following statement is copied from numbat-cli. Move this to the numbat crate
-                        // to avoid duplication.
-                        let type_ = statements.last().map_or(m::empty(), |s| {
-                            if let numbat::Statement::Expression(e) = s {
-                                let type_ = e.get_type();
-
-                                if type_ == Type::scalar() {
-                                    m::empty()
-                                } else {
-                                    m::dimmed("    [")
-                                        + e.get_type().to_readable_type(&registry)
-                                        + m::dimmed("]")
-                                }
-                            } else {
-                                m::empty()
-                            }
-                        });
-
-                        let markup = m::whitespace("    ")
-                            + m::operator("=")
-                            + m::space()
-                            + value.pretty_print()
-                            + type_;
-                        output.push_str(&fmt.format(&markup, true));
-                    }
-                    InterpreterResult::Continue => {}
-                    InterpreterResult::Exit(_) => {
-                        output.push_str(&jt_format(Some("error"), "Error!".into()))
-                    }
-                }
+                let result_markup = result.to_markup(statements.last(), &registry);
+                output.push_str(&fmt.format(&result_markup, true));
 
                 InterpreterOutput {
                     output,
@@ -156,6 +123,12 @@ impl Numbat {
         fmt.format(&markup, false).into()
     }
 
+    pub fn help(&self) -> JsValue {
+        let markup = help_markup();
+        let fmt = JqueryTerminalFormatter {};
+        fmt.format(&markup, true).into()
+    }
+
     pub fn get_completions_for(&self, input: &str) -> Vec<JsValue> {
         self.ctx
             .get_completions_for(input)
diff --git a/numbat-wasm/www/index.js b/numbat-wasm/www/index.js
index a887b62..93929cc 100644
--- a/numbat-wasm/www/index.js
+++ b/numbat-wasm/www/index.js
@@ -53,6 +53,8 @@ function interpret(input) {
     this.clear();
   } else if (input_trimmed == "list" || input_trimmed == "ll" || input_trimmed == "ls") {
     output = numbat.print_environment();
+  } else if (input_trimmed == "help" || input_trimmed == "?") {
+    output = numbat.help();
   } else {
     var result = numbat.interpret(input);
     output = result.output;
diff --git a/numbat/Cargo.toml b/numbat/Cargo.toml
index 010fc4e..5838895 100644
--- a/numbat/Cargo.toml
+++ b/numbat/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "numbat"
-description = " A statically typed programming language for scientific computations with first class support for physical dimensions and units"
+description = "A statically typed programming language for scientific computations with first class support for physical dimensions and units."
 authors = ["David Peter <mail@david-peter.de>"]
 categories = ["science", "mathematics", "compilers"]
 keywords = ["language", "compiler", "physics", "units", "calculation"]
diff --git a/numbat/examples/inspect.rs b/numbat/examples/inspect.rs
index 150975d..9f72e89 100644
--- a/numbat/examples/inspect.rs
+++ b/numbat/examples/inspect.rs
@@ -5,8 +5,7 @@ fn main() {
     let mut importer = FileSystemImporter::default();
     importer.add_path("numbat/modules");
     let mut ctx = Context::new(importer);
-    let result = ctx.interpret("use all", CodeSource::Internal).unwrap();
-    assert!(result.1.is_success());
+    let _result = ctx.interpret("use all", CodeSource::Internal).unwrap();
 
     println!("| Dimension | Unit name | Identifier(s) |");
     println!("| --- | --- | --- |");
diff --git a/numbat/examples/unit_graph.rs b/numbat/examples/unit_graph.rs
index ff89e04..1c6e707 100644
--- a/numbat/examples/unit_graph.rs
+++ b/numbat/examples/unit_graph.rs
@@ -13,8 +13,7 @@ fn main() {
     let mut importer = FileSystemImporter::default();
     importer.add_path("numbat/modules");
     let mut ctx = Context::new(importer);
-    let result = ctx.interpret("use all", CodeSource::Internal).unwrap();
-    assert!(result.1.is_success());
+    let _ = ctx.interpret("use all", CodeSource::Internal).unwrap();
 
     println!("digraph G {{");
     println!("  layout=fdp;");
diff --git a/numbat/src/help.rs b/numbat/src/help.rs
new file mode 100644
index 0000000..385ead6
--- /dev/null
+++ b/numbat/src/help.rs
@@ -0,0 +1,64 @@
+/// Print a help, linking the documentation, and live-running some examples
+/// in an isolated context.
+use crate::markup as m;
+use crate::module_importer::BuiltinModuleImporter;
+use crate::resolver::CodeSource;
+use crate::Context;
+use crate::InterpreterSettings;
+
+use std::sync::{Arc, Mutex};
+
+fn evaluate_example(context: &mut Context, input: &str) -> m::Markup {
+    let statement_output: Arc<Mutex<Vec<m::Markup>>> = Arc::new(Mutex::new(vec![]));
+    let statement_output_c = statement_output.clone();
+    let mut settings = InterpreterSettings {
+        print_fn: Box::new(move |s: &m::Markup| {
+            statement_output_c.lock().unwrap().push(s.clone());
+        }),
+    };
+
+    let (statements, interpreter_result) = context
+        .interpret_with_settings(&mut settings, input, CodeSource::Internal)
+        .expect("No error in 'help' examples");
+
+    let markup =
+        statement_output
+            .lock()
+            .unwrap()
+            .iter()
+            .fold(m::empty(), |accumulated_mk, single_line| {
+                accumulated_mk + m::nl() + m::whitespace("  ") + single_line.clone() + m::nl()
+            })
+            + interpreter_result.to_markup(statements.last(), context.dimension_registry());
+
+    markup
+}
+
+pub fn help_markup() -> m::Markup {
+    let mut output = m::nl()
+        + m::text("Numbat is a statically typed programming language for scientific computations")
+        + m::nl()
+        + m::text("with first class support for physical dimensions and units. Please refer to")
+        + m::nl()
+        + m::text("the full documentation online at ")
+        + m::string("https://numbat.dev/doc/")
+        + m::text(" or try one of these ")
+        + m::nl()
+        + m::text("examples:")
+        + m::nl()
+        + m::nl();
+
+    let examples = [
+        "8 km / (1 h + 25 min)",
+        "atan2(30 cm, 1 m) -> deg",
+        "let ω = 2 π c / 660 nm",
+        r#"print("Energy of red photons: {ℏ ω -> eV}")"#,
+    ];
+    let mut example_context = Context::new(BuiltinModuleImporter::default());
+    let _use_prelude_output = evaluate_example(&mut example_context, "use prelude");
+    for example in examples.iter() {
+        output += m::text(">>> ") + m::text(example) + m::nl();
+        output += evaluate_example(&mut example_context, example) + m::nl();
+    }
+    output
+}
diff --git a/numbat/src/interpreter.rs b/numbat/src/interpreter.rs
index 4ce9f63..9180512 100644
--- a/numbat/src/interpreter.rs
+++ b/numbat/src/interpreter.rs
@@ -1,10 +1,14 @@
 use crate::{
+    dimension::DimensionRegistry,
     markup::Markup,
+    pretty_print::PrettyPrint,
     quantity::{Quantity, QuantityError},
-    typed_ast::Statement,
+    typed_ast::{Statement, Type},
     unit_registry::{UnitRegistry, UnitRegistryError},
 };
 
+use crate::markup as m;
+
 use thiserror::Error;
 
 pub use crate::value::Value;
@@ -35,26 +39,45 @@ pub enum RuntimeError {
     UserError(String),
 }
 
-#[derive(Debug, PartialEq, Eq)]
-pub enum ExitStatus {
-    Success,
-    Error,
-}
-
 #[derive(Debug, PartialEq, Eq)]
 #[must_use]
 pub enum InterpreterResult {
     Value(Value),
     Continue,
-    Exit(ExitStatus),
 }
 
 impl InterpreterResult {
-    pub fn is_success(&self) -> bool {
+    pub fn to_markup(
+        &self,
+        evaluated_statement: Option<&Statement>,
+        registry: &DimensionRegistry,
+    ) -> Markup {
         match self {
-            Self::Value(_) => true,
-            Self::Continue => true,
-            Self::Exit(_) => false,
+            Self::Value(value) => {
+                let type_markup = if let Some(s) = evaluated_statement {
+                    if let Statement::Expression(e) = s {
+                        let type_ = e.get_type();
+                        if type_ == Type::scalar() {
+                            m::empty()
+                        } else {
+                            m::dimmed("    [")
+                                + e.get_type().to_readable_type(&registry)
+                                + m::dimmed("]")
+                        }
+                    } else {
+                        m::empty()
+                    }
+                } else {
+                    m::empty()
+                };
+                m::whitespace("    ")
+                    + m::operator("=")
+                    + m::space()
+                    + value.pretty_print()
+                    + type_markup
+                    + m::nl()
+            }
+            Self::Continue => m::empty(),
         }
     }
 }
diff --git a/numbat/src/lib.rs b/numbat/src/lib.rs
index e857565..7c37212 100644
--- a/numbat/src/lib.rs
+++ b/numbat/src/lib.rs
@@ -7,6 +7,7 @@ pub mod diagnostic;
 mod dimension;
 mod ffi;
 mod gamma;
+pub mod help;
 mod interpreter;
 pub mod keywords;
 pub mod markup;
@@ -51,7 +52,6 @@ use thiserror::Error;
 use typechecker::{TypeCheckError, TypeChecker};
 
 pub use diagnostic::Diagnostic;
-pub use interpreter::ExitStatus;
 pub use interpreter::InterpreterResult;
 pub use interpreter::InterpreterSettings;
 pub use interpreter::RuntimeError;
@@ -425,12 +425,17 @@ impl Context {
                             }
                         }
 
-                        self.interpret_with_settings(
+                        let _ = self.interpret_with_settings(
                             &mut no_print_settings,
                             "use units::currencies",
                             CodeSource::Internal,
-                        )
-                        .ok();
+                        )?;
+
+                        // Make sure we do not run into an infinite loop in case loading that
+                        // module did not bring in the required currency unit identifier. This
+                        // can happen if the list of currency identifiers is not in sync with
+                        // what the module actually defines.
+                        self.load_currency_module_on_demand = false;
 
                         // Now we try to evaluate the user expression again:
                         return self.interpret_with_settings(settings, code, code_source);


Test patch:
diff --git a/numbat-cli/tests/integration.rs b/numbat-cli/tests/integration.rs
index 4465714..8979cb9 100644
--- a/numbat-cli/tests/integration.rs
+++ b/numbat-cli/tests/integration.rs
@@ -92,3 +92,14 @@ fn without_prelude() {
         .success()
         .stdout(predicates::str::contains("5.2"));
 }
+
+#[test]
+fn help_text() {
+    numbat()
+        .write_stdin("help")
+        .assert()
+        .success()
+        .stdout(predicates::str::contains(
+            "Energy of red photons: 1.87855 eV",
+        ));
+}
diff --git a/numbat/tests/common.rs b/numbat/tests/common.rs
index 727c633..5cebbcc 100644
--- a/numbat/tests/common.rs
+++ b/numbat/tests/common.rs
@@ -20,11 +20,9 @@ pub fn get_test_context() -> Context {
     static CONTEXT: Lazy<Context> = Lazy::new(|| {
         let mut context = get_test_context_without_prelude();
 
-        assert!(context
+        let _ = context
             .interpret("use prelude", CodeSource::Internal)
-            .expect("Error while running prelude")
-            .1
-            .is_success());
+            .expect("Error while running prelude");
         context
     });