use crate::console::{refill_and_print, Console};
use crate::exec::CATEGORY;
use async_trait::async_trait;
use endbasic_core::ast::{ArgSep, Expr, VarType};
use endbasic_core::exec::Machine;
use endbasic_core::syms::{
CallError, CallableMetadata, CallableMetadataBuilder, Command, CommandResult, Symbols,
};
use radix_trie::{Trie, TrieCommon};
use std::cell::RefCell;
use std::collections::{BTreeMap, HashMap};
use std::io;
use std::rc::Rc;
const LANG_REFERENCE: &str = r"
Symbols (variable, array and function references):
name? Boolean (TRUE and FALSE).
name# Floating point (double).
name% Integer (32 bits).
name$ String.
name Type determined by value or definition.
Assignments and declarations:
varref[(dim1[, ..., dimN])] = expr
DIM varname[(dim1[, ..., dimN])] [AS BOOLEAN|DOUBLE|INTEGER|STRING]
Expressions:
a + b a - b a * b a / b a MOD b -a
a AND b NOT a a OR b a XOR b
a = b a <> b a < b a <= b a > b a >= b
(a) varref
arrayref(s1[, ..., sN]) funcref(a1[, ..., aN])
Flow control:
IF expr THEN: ...: ELSE IF expr THEN: ...: ELSE: ...: END IF
FOR varref = expr TO expr [STEP int]: ...: NEXT
WHILE expr: ...: WEND
Misc:
st1: st2 Separates statements (same as a newline).
REM text Comment until end of line.
' text Comment until end of line.
, Long separator for arguments to builtin call.
; Short separator for arguments to builtin call.
";
fn header() -> Vec<String> {
vec![
"".to_owned(),
format!(" EndBASIC {}", env!("CARGO_PKG_VERSION")),
" Copyright 2020-2022 Julio Merino".to_owned(),
"".to_owned(),
format!(" Project page at <{}>", env!("CARGO_PKG_HOMEPAGE")),
" License Apache Version 2.0 <http://www.apache.org/licenses/LICENSE-2.0>".to_owned(),
]
}
trait Topic {
fn name(&self) -> &str;
fn title(&self) -> &str;
fn show_in_summary(&self) -> bool;
fn describe(&self, _console: &mut dyn Console) -> io::Result<()>;
}
struct CallableTopic {
name: String,
metadata: CallableMetadata,
}
impl Topic for CallableTopic {
fn name(&self) -> &str {
&self.name
}
fn title(&self) -> &str {
self.metadata.description().next().unwrap()
}
fn show_in_summary(&self) -> bool {
false
}
fn describe(&self, console: &mut dyn Console) -> io::Result<()> {
console.print("")?;
if self.metadata.return_type() == VarType::Void {
if self.metadata.syntax().is_empty() {
refill_and_print(console, self.metadata.name(), " ")?
} else {
refill_and_print(
console,
&format!("{} {}", self.metadata.name(), self.metadata.syntax()),
" ",
)?
}
} else {
refill_and_print(
console,
&format!(
"{}{}({})",
self.metadata.name(),
self.metadata.return_type().annotation(),
self.metadata.syntax(),
),
" ",
)?;
}
for paragraph in self.metadata.description() {
console.print("")?;
refill_and_print(console, paragraph, " ")?;
}
console.print("")?;
Ok(())
}
}
struct CategoryTopic {
name: &'static str,
metadatas: Vec<CallableMetadata>,
}
impl Topic for CategoryTopic {
fn name(&self) -> &str {
self.name
}
fn title(&self) -> &str {
self.name
}
fn show_in_summary(&self) -> bool {
true
}
fn describe(&self, console: &mut dyn Console) -> io::Result<()> {
let description = self.metadatas.get(0).expect("Must have at least one symbol").category();
let mut index = BTreeMap::default();
let mut max_length = 0;
for metadata in &self.metadatas {
debug_assert_eq!(
description,
metadata.category(),
"All commands registered in this category must be equivalent"
);
let name = format!("{}{}", metadata.name(), metadata.return_type().annotation());
if name.len() > max_length {
max_length = name.len();
}
let blurb = metadata.description().next().unwrap();
let previous = index.insert(name, blurb);
assert!(previous.is_none(), "Names should have been unique");
}
console.print("")?;
for line in description.lines() {
refill_and_print(console, line, " ")?;
console.print("")?;
}
for (name, blurb) in index.iter() {
let filler = " ".repeat(max_length - name.len());
console.print(&format!(" >> {}{} {}", name, filler, blurb))?;
}
console.print("")?;
refill_and_print(
console,
" Type HELP followed by the name of a symbol for details.",
" ",
)?;
console.print("")?;
Ok(())
}
}
struct LanguageTopic {}
impl Topic for LanguageTopic {
fn name(&self) -> &str {
"Language reference"
}
fn title(&self) -> &str {
"Language reference"
}
fn show_in_summary(&self) -> bool {
true
}
fn describe(&self, console: &mut dyn Console) -> io::Result<()> {
for line in LANG_REFERENCE.lines() {
console.print(line)?;
}
console.print("")?;
Ok(())
}
}
struct Topics(Trie<String, Box<dyn Topic>>);
impl Topics {
fn new(symbols: &Symbols) -> Self {
fn insert(topics: &mut Trie<String, Box<dyn Topic>>, topic: Box<dyn Topic>) {
let key = topic.name().to_ascii_uppercase();
topics.insert(key, topic);
}
let mut topics = Trie::default();
insert(&mut topics, Box::from(LanguageTopic {}));
let mut categories = HashMap::new();
for (name, symbol) in symbols.as_hashmap().iter() {
if let Some(metadata) = symbol.metadata() {
let category_title = metadata.category().lines().next().unwrap();
categories
.entry(category_title)
.or_insert_with(Vec::default)
.push(metadata.clone());
insert(
&mut topics,
Box::from(CallableTopic {
name: format!("{}{}", name, metadata.return_type().annotation()),
metadata: metadata.clone(),
}),
);
}
}
for (name, metadatas) in categories.into_iter() {
insert(&mut topics, Box::from(CategoryTopic { name, metadatas }));
}
Self(topics)
}
fn find(&self, name: &str) -> Result<&dyn Topic, CallError> {
let key = name.to_ascii_uppercase();
if let Some(topic) = self.0.get(&key) {
return Ok(topic.as_ref());
}
match self.0.get_raw_descendant(&key) {
Some(subtrie) => {
let children: Vec<(&String, &Box<dyn Topic>)> = subtrie.iter().collect();
match children[..] {
[(_name, topic)] => Ok(topic.as_ref()),
_ => {
let completions: Vec<String> =
children.iter().map(|(name, _topic)| (*name).to_owned()).collect();
Err(CallError::ArgumentError(format!(
"Ambiguous help topic {}; candidates are: {}",
name,
completions.join(", ")
)))
}
}
}
None => Err(CallError::ArgumentError(format!("Unknown help topic {}", name))),
}
}
fn values(&self) -> radix_trie::iter::Values<String, Box<dyn Topic>> {
self.0.values()
}
}
pub struct HelpCommand {
metadata: CallableMetadata,
console: Rc<RefCell<dyn Console>>,
}
impl HelpCommand {
pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("HELP", VarType::Void)
.with_syntax("[topic]")
.with_category(CATEGORY)
.with_description(
"Prints interactive help.
Without arguments, shows a summary of all available top-level help topics.
With a single argument, which may be a bare name or a string, shows detailed information about the \
given help topic, command, or function. Topic names with spaces in them must be double-quoted.
Topic names are case-insensitive and can be specified as prefixes, in which case the topic whose \
name starts with the prefix will be shown. For example, the following invocations are all \
equivalent: HELP CON, HELP console, HELP \"Console manipulation\".",
)
.build(),
console,
})
}
fn summary(&self, topics: &Topics) -> io::Result<()> {
let mut console = self.console.borrow_mut();
for line in header() {
refill_and_print(&mut *console, &line, " ")?;
}
console.print("")?;
refill_and_print(&mut *console, "Top-level help topics:", " ")?;
console.print("")?;
for topic in topics.values() {
if topic.show_in_summary() {
console.print(&format!(" >> {}", topic.title()))?;
}
}
console.print("")?;
refill_and_print(
&mut *console,
"Type HELP followed by the name of a topic for details.",
" ",
)?;
refill_and_print(
&mut *console,
"Type HELP HELP for details on how to specify topic names.",
" ",
)?;
console.print("")?;
Ok(())
}
}
#[async_trait(?Send)]
impl Command for HelpCommand {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
async fn exec(&self, args: &[(Option<Expr>, ArgSep)], machine: &mut Machine) -> CommandResult {
let topics = Topics::new(machine.get_symbols());
match args {
[] => {
self.summary(&topics)?;
}
[(Some(Expr::Symbol(vref)), ArgSep::End)] => {
let topic = topics.find(&format!("{}", vref))?;
let mut console = self.console.borrow_mut();
topic.describe(&mut *console)?;
}
[(Some(Expr::Text(name)), ArgSep::End)] => {
let topic = topics.find(name)?;
let mut console = self.console.borrow_mut();
topic.describe(&mut *console)?;
}
_ => {
return Err(CallError::ArgumentError(
"HELP takes zero or only one argument".to_owned(),
))
}
}
Ok(())
}
}
pub fn add_all(machine: &mut Machine, console: Rc<RefCell<dyn Console>>) {
machine.add_command(HelpCommand::new(console));
}
#[cfg(test)]
pub(crate) mod testutils {
use super::*;
use endbasic_core::ast::Value;
use endbasic_core::syms::{
CallableMetadata, CallableMetadataBuilder, Function, FunctionResult,
};
pub(crate) struct DoNothingCommand {
metadata: CallableMetadata,
}
impl DoNothingCommand {
pub(crate) fn new() -> Rc<Self> {
DoNothingCommand::new_with_name("DO_NOTHING")
}
pub fn new_with_name(name: &'static str) -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new(name, VarType::Void)
.with_syntax("this [would] <be|the> syntax \"specification\"")
.with_category(
"Testing
This is a sample category for testing.",
)
.with_description(
"This is the blurb.
First paragraph of the extended description.
Second paragraph of the extended description.",
)
.build(),
})
}
}
#[async_trait(?Send)]
impl Command for DoNothingCommand {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
async fn exec(
&self,
_args: &[(Option<Expr>, ArgSep)],
_machine: &mut Machine,
) -> CommandResult {
Ok(())
}
}
pub(crate) struct EmptyFunction {
metadata: CallableMetadata,
}
impl EmptyFunction {
pub(crate) fn new() -> Rc<Self> {
EmptyFunction::new_with_name("EMPTY")
}
pub(crate) fn new_with_name(name: &'static str) -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new(name, VarType::Text)
.with_syntax("this [would] <be|the> syntax \"specification\"")
.with_category(
"Testing
This is a sample category for testing.",
)
.with_description(
"This is the blurb.
First paragraph of the extended description.
Second paragraph of the extended description.",
)
.build(),
})
}
}
#[async_trait(?Send)]
impl Function for EmptyFunction {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
async fn exec(&self, _args: &[Expr], _symbols: &mut Symbols) -> FunctionResult {
Ok(Value::Text("irrelevant".to_owned()))
}
}
}
#[cfg(test)]
mod tests {
use super::testutils::*;
use super::*;
use crate::testutils::*;
fn tester() -> Tester {
let tester = Tester::empty();
let console = tester.get_console();
tester.add_command(HelpCommand::new(console))
}
#[test]
fn test_help_summarize_symbols() {
tester()
.add_command(DoNothingCommand::new())
.add_function(EmptyFunction::new())
.run("HELP")
.expect_prints(header())
.expect_prints([
"",
" Top-level help topics:",
"",
" >> Interpreter",
" >> Language reference",
" >> Testing",
"",
" Type HELP followed by the name of a topic for details.",
" Type HELP HELP for details on how to specify topic names.",
"",
])
.check();
}
#[test]
fn test_help_describe_callables_topic() {
tester()
.add_command(DoNothingCommand::new())
.add_function(EmptyFunction::new())
.run("help testing")
.expect_prints([
"",
" Testing",
"",
" This is a sample category for testing.",
"",
" >> DO_NOTHING This is the blurb.",
" >> EMPTY$ This is the blurb.",
"",
" Type HELP followed by the name of a symbol for details.",
"",
])
.check();
}
#[test]
fn test_help_describe_command() {
tester()
.add_command(DoNothingCommand::new())
.run("help Do_Nothing")
.expect_prints([
"",
" DO_NOTHING this [would] <be|the> syntax \"specification\"",
"",
" This is the blurb.",
"",
" First paragraph of the extended description.",
"",
" Second paragraph of the extended description.",
"",
])
.check();
}
fn do_help_describe_function_test(name: &str) {
tester()
.add_function(EmptyFunction::new())
.run(format!("help {}", name))
.expect_prints([
"",
" EMPTY$(this [would] <be|the> syntax \"specification\")",
"",
" This is the blurb.",
"",
" First paragraph of the extended description.",
"",
" Second paragraph of the extended description.",
"",
])
.check();
}
#[test]
fn test_help_describe_function_without_annotation() {
do_help_describe_function_test("Empty")
}
#[test]
fn test_help_describe_function_with_annotation() {
do_help_describe_function_test("EMPTY$")
}
#[test]
fn test_help_lang() {
for cmd in &["help lang", "help language", r#"help "Language Reference""#] {
tester()
.run(*cmd)
.expect_prints(LANG_REFERENCE.lines().collect::<Vec<&str>>())
.expect_prints([""])
.check();
}
}
#[test]
fn test_help_prefix_search() {
fn exp_output(name: &str, is_function: bool) -> Vec<String> {
let spec = if is_function {
format!(" {}(this [would] <be|the> syntax \"specification\")", name)
} else {
format!(" {} this [would] <be|the> syntax \"specification\"", name)
};
vec![
"".to_owned(),
spec,
"".to_owned(),
" This is the blurb.".to_owned(),
"".to_owned(),
" First paragraph of the extended description.".to_owned(),
"".to_owned(),
" Second paragraph of the extended description.".to_owned(),
"".to_owned(),
]
}
for cmd in &["help aa", "help aab", "help aabc"] {
tester()
.add_function(EmptyFunction::new_with_name("AABC"))
.add_function(EmptyFunction::new_with_name("ABC"))
.add_function(EmptyFunction::new_with_name("BC"))
.run(*cmd)
.expect_prints(exp_output("AABC$", true))
.check();
}
for cmd in &["help b", "help bc"] {
tester()
.add_function(EmptyFunction::new_with_name("AABC"))
.add_function(EmptyFunction::new_with_name("ABC"))
.add_function(EmptyFunction::new_with_name("BC"))
.run(*cmd)
.expect_prints(exp_output("BC$", true))
.check();
}
tester()
.add_command(DoNothingCommand::new_with_name("AAAB"))
.add_command(DoNothingCommand::new_with_name("AAAA"))
.add_command(DoNothingCommand::new_with_name("AAAAA"))
.run("help aaaa")
.expect_prints(exp_output("AAAA", false))
.check();
tester()
.add_command(DoNothingCommand::new_with_name("AB"))
.add_function(EmptyFunction::new_with_name("ABC"))
.add_function(EmptyFunction::new_with_name("AABC"))
.run("help a")
.expect_err("Ambiguous help topic a; candidates are: AABC$, AB, ABC$")
.check();
}
#[test]
fn test_help_errors() {
let mut t =
tester().add_command(DoNothingCommand::new()).add_function(EmptyFunction::new());
t.run("HELP foo bar").expect_err("Unexpected value in expression").check();
t.run("HELP foo, bar").expect_err("HELP takes zero or only one argument").check();
t.run("HELP lang%").expect_err("Unknown help topic lang%").check();
t.run("HELP foo$").expect_err("Unknown help topic foo$").check();
t.run("HELP foo").expect_err("Unknown help topic foo").check();
t.run("HELP do_nothing$").expect_err("Unknown help topic do_nothing$").check();
t.run("HELP empty?").expect_err("Unknown help topic empty?").check();
let mut t = tester();
t.run("HELP undoc").expect_err("Unknown help topic undoc").check();
t.run("undoc = 3: HELP undoc")
.expect_err("Unknown help topic undoc")
.expect_var("undoc", 3)
.check();
let mut t = tester();
t.run("HELP undoc").expect_err("Unknown help topic undoc").check();
t.run("DIM undoc(3): HELP undoc")
.expect_err("Unknown help topic undoc")
.expect_array("undoc", VarType::Integer, &[3], vec![])
.check();
}
}