use crate::ast::{ArgSep, Expr, VarType};
use crate::console::Console;
use crate::eval::{BuiltinFunction, CallableMetadata, CallableMetadataBuilder};
use crate::exec::{self, BuiltinCommand, Machine};
use async_trait::async_trait;
use std::cell::RefCell;
use std::collections::{BTreeMap, HashMap};
use std::rc::Rc;
const LANG_REFERENCE: &str = r"
Symbols (variable and function references):
name? Boolean (TRUE and FALSE).
name% Integer (32 bits).
name$ String.
name Type determined by value or definition.
Assignments:
varref = expr
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 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: ...: END WHILE
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() -> String {
format!(
"
EndBASIC {}
Copyright 2020 Julio Merino
Project page at <{}>
License Apache Version 2.0 <http://www.apache.org/licenses/LICENSE-2.0>
",
env!("CARGO_PKG_VERSION"),
env!("CARGO_PKG_HOMEPAGE")
)
}
fn compute_callables<'a>(
commands: &'a HashMap<&'static str, Rc<dyn BuiltinCommand>>,
functions: &'a HashMap<&'static str, Rc<dyn BuiltinFunction>>,
) -> HashMap<&'static str, &'a CallableMetadata> {
let mut callables: HashMap<&'static str, &'a CallableMetadata> = HashMap::default();
for (name, command) in commands.iter() {
assert!(!callables.contains_key(name), "Command names are in a map; must be unique");
callables.insert(&name, command.metadata());
}
for (name, function) in functions.iter() {
assert!(!callables.contains_key(name), "Command and function names are not disjoint");
callables.insert(&name, function.metadata());
}
callables
}
fn build_index(
callables: &HashMap<&'static str, &CallableMetadata>,
) -> (BTreeMap<&'static str, BTreeMap<String, &'static str>>, usize) {
let mut index = BTreeMap::default();
let mut max_length = 0;
for metadata in callables.values() {
let name = format!("{}{}", metadata.name(), metadata.return_type().annotation());
if name.len() > max_length {
max_length = name.len();
}
let blurb = metadata.description().next().unwrap();
index.entry(metadata.category()).or_insert_with(BTreeMap::default).insert(name, blurb);
}
(index, max_length)
}
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("Interpreter manipulation")
.with_description(
"Prints interactive help.
Without arguments, shows a summary of all available help topics.
With a single argument, shows detailed information about the given help topic, command, or \
function.",
)
.build(),
console,
})
}
fn summary(&self, callables: &HashMap<&'static str, &CallableMetadata>) -> exec::Result<()> {
let (index, max_length) = build_index(callables);
let mut console = self.console.borrow_mut();
for line in header().lines() {
console.print(line)?;
}
for (category, by_name) in index.iter() {
console.print("")?;
console.print(&format!(" >> {} <<", category))?;
for (name, blurb) in by_name.iter() {
let filler = " ".repeat(max_length - name.len());
console.print(&format!(" {}{} {}", name, filler, blurb))?;
}
}
console.print("")?;
console.print(" Type HELP followed by a command or function name for details.")?;
console.print(" Type HELP LANG for a quick reference guide about the language.")?;
console.print("")?;
Ok(())
}
fn describe_callable(&self, metadata: &CallableMetadata) -> exec::Result<()> {
let mut console = self.console.borrow_mut();
console.print("")?;
if metadata.return_type() == VarType::Void {
if metadata.syntax().is_empty() {
console.print(&format!(" {}", metadata.name()))?
} else {
console.print(&format!(" {} {}", metadata.name(), metadata.syntax()))?
}
} else {
console.print(&format!(
" {}{}({})",
metadata.name(),
metadata.return_type().annotation(),
metadata.syntax(),
))?;
}
for line in metadata.description() {
console.print("")?;
console.print(&format!(" {}", line))?;
}
console.print("")?;
Ok(())
}
fn describe_lang(&self) -> exec::Result<()> {
let mut console = self.console.borrow_mut();
for line in LANG_REFERENCE.lines() {
console.print(line)?;
}
console.print("")?;
Ok(())
}
}
#[async_trait(?Send)]
impl BuiltinCommand for HelpCommand {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
async fn exec(
&self,
args: &[(Option<Expr>, ArgSep)],
machine: &mut Machine,
) -> exec::Result<()> {
let callables = compute_callables(machine.get_commands(), machine.get_functions());
match args {
[] => self.summary(&callables)?,
[(Some(Expr::Symbol(vref)), ArgSep::End)] => {
let name = vref.name().to_ascii_uppercase();
if name == "LANG" {
if vref.ref_type() != VarType::Auto {
return exec::new_usage_error("Incompatible type annotation");
}
self.describe_lang()?;
} else {
match callables.get(name.as_str()) {
Some(metadata) => {
if vref.ref_type() != VarType::Auto
&& vref.ref_type() != metadata.return_type()
{
return exec::new_usage_error("Incompatible type annotation");
}
self.describe_callable(metadata)?;
}
None => {
return exec::new_usage_error(format!(
"Cannot describe unknown command or function {}",
name
))
}
}
}
}
_ => return exec::new_usage_error("HELP takes zero or only one argument"),
}
Ok(())
}
}
#[cfg(test)]
pub(crate) mod testutils {
use super::*;
use crate::ast::Value;
use crate::eval::{self, CallableMetadata, CallableMetadataBuilder};
pub(crate) struct DoNothingCommand {
metadata: CallableMetadata,
}
impl DoNothingCommand {
pub fn new() -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("DO_NOTHING", VarType::Void)
.with_syntax("this [would] <be|the> syntax \"specification\"")
.with_category("Testing")
.with_description(
"This is the blurb.
First paragraph of the extended description.
Second paragraph of the extended description.",
)
.build(),
})
}
}
#[async_trait(?Send)]
impl BuiltinCommand for DoNothingCommand {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
async fn exec(
&self,
_args: &[(Option<Expr>, ArgSep)],
_machine: &mut Machine,
) -> exec::Result<()> {
Ok(())
}
}
pub(crate) struct EmptyFunction {
metadata: CallableMetadata,
}
impl EmptyFunction {
pub(crate) fn new() -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("EMPTY", VarType::Text)
.with_syntax("this [would] <be|the> syntax \"specification\"")
.with_category("Testing")
.with_description(
"This is the blurb.
First paragraph of the extended description.
Second paragraph of the extended description.",
)
.build(),
})
}
}
impl BuiltinFunction for EmptyFunction {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
fn exec(&self, _args: Vec<Value>) -> eval::FunctionResult {
Ok(Value::Text("irrelevant".to_owned()))
}
}
}
#[cfg(test)]
mod tests {
use super::testutils::*;
use super::*;
use crate::console::testutils::*;
use crate::exec::MachineBuilder;
use futures_lite::future::block_on;
use std::cell::RefCell;
fn flatten_captured_out(output: &[CapturedOut]) -> String {
output.iter().fold(String::new(), |result, o| match o {
CapturedOut::Print(text) => result + &text + "\n",
_ => panic!("Unexpected element in output"),
})
}
fn do_error_test(input: &str, expected_err: &str) {
let console = Rc::from(RefCell::from(MockConsoleBuilder::new().build()));
let mut machine = MachineBuilder::default()
.add_command(HelpCommand::new(console.clone()))
.add_command(DoNothingCommand::new())
.add_function(EmptyFunction::new())
.build();
assert_eq!(
expected_err,
format!(
"{}",
block_on(machine.exec(&mut input.as_bytes())).expect_err("Execution did not fail")
)
);
assert!(console.borrow().captured_out().is_empty());
}
#[test]
fn test_help_summarize_callables() {
let console = Rc::from(RefCell::from(MockConsoleBuilder::new().build()));
let mut machine = MachineBuilder::default()
.add_command(HelpCommand::new(console.clone()))
.add_command(DoNothingCommand::new())
.add_function(EmptyFunction::new())
.build();
block_on(machine.exec(&mut b"HELP".as_ref())).unwrap();
let text = flatten_captured_out(console.borrow().captured_out());
assert_eq!(
header()
+ "
>> Interpreter manipulation <<
HELP Prints interactive help.
>> Testing <<
DO_NOTHING This is the blurb.
EMPTY$ This is the blurb.
Type HELP followed by a command or function name for details.
Type HELP LANG for a quick reference guide about the language.
",
text
);
}
#[test]
fn test_help_describe_command() {
let console = Rc::from(RefCell::from(MockConsoleBuilder::new().build()));
let mut machine = MachineBuilder::default()
.add_command(HelpCommand::new(console.clone()))
.add_command(DoNothingCommand::new())
.build();
block_on(machine.exec(&mut b"help Do_Nothing".as_ref())).unwrap();
let text = flatten_captured_out(console.borrow().captured_out());
assert_eq!(
"
DO_NOTHING this [would] <be|the> syntax \"specification\"
This is the blurb.
First paragraph of the extended description.
Second paragraph of the extended description.
",
&text
);
}
fn do_help_describe_function_test(name: &str) {
let console = Rc::from(RefCell::from(MockConsoleBuilder::new().build()));
let mut machine = MachineBuilder::default()
.add_command(HelpCommand::new(console.clone()))
.add_function(EmptyFunction::new())
.build();
block_on(machine.exec(&mut format!("help {}", name).as_bytes())).unwrap();
let text = flatten_captured_out(console.borrow().captured_out());
assert_eq!(
"
EMPTY$(this [would] <be|the> syntax \"specification\")
This is the blurb.
First paragraph of the extended description.
Second paragraph of the extended description.
",
&text
);
}
#[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() {
let console = Rc::from(RefCell::from(MockConsoleBuilder::new().build()));
let mut machine = MachineBuilder::default()
.add_command(HelpCommand::new(console.clone()))
.add_command(DoNothingCommand::new())
.build();
block_on(machine.exec(&mut b"help lang".as_ref())).unwrap();
let text = flatten_captured_out(console.borrow().captured_out());
assert_eq!(String::from(LANG_REFERENCE) + "\n", text);
}
#[test]
fn test_help_errors() {
do_error_test("HELP foo bar", "Unexpected value in expression");
do_error_test("HELP foo, bar", "HELP takes zero or only one argument");
do_error_test("HELP lang%", "Incompatible type annotation");
do_error_test("HELP foo$", "Cannot describe unknown command or function FOO");
do_error_test("HELP foo", "Cannot describe unknown command or function FOO");
do_error_test("HELP do_nothing$", "Incompatible type annotation");
do_error_test("HELP empty?", "Incompatible type annotation");
}
}