From 4cbfdde4a3ad3d423fb2045ffb6fd36dfe2a9024 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 30 Jan 2024 18:13:30 -0500 Subject: [PATCH] Write docs; Refine library API --- src/abstract_tree/assignment.rs | 1 + src/abstract_tree/assignment_operator.rs | 1 + src/abstract_tree/built_in_value.rs | 43 +++++- src/interpret.rs | 172 +++++++++++++---------- src/lib.rs | 12 +- src/main.rs | 9 +- tests/format.rs | 26 ++-- 7 files changed, 164 insertions(+), 100 deletions(-) diff --git a/src/abstract_tree/assignment.rs b/src/abstract_tree/assignment.rs index c082f58..0ae82ec 100644 --- a/src/abstract_tree/assignment.rs +++ b/src/abstract_tree/assignment.rs @@ -5,6 +5,7 @@ use crate::{ SyntaxNode, SyntaxPosition, Type, TypeSpecification, Value, }; +/// Variable assignment, including add-assign and subtract-assign operations. #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)] pub struct Assignment { identifier: Identifier, diff --git a/src/abstract_tree/assignment_operator.rs b/src/abstract_tree/assignment_operator.rs index 69ef328..4fe09da 100644 --- a/src/abstract_tree/assignment_operator.rs +++ b/src/abstract_tree/assignment_operator.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::{AbstractTree, Error, Format, Map, Result, SyntaxNode, Type, Value}; +/// Operators that be used in an assignment statement. #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)] pub enum AssignmentOperator { Equal, diff --git a/src/abstract_tree/built_in_value.rs b/src/abstract_tree/built_in_value.rs index 9735548..471a75a 100644 --- a/src/abstract_tree/built_in_value.rs +++ b/src/abstract_tree/built_in_value.rs @@ -14,36 +14,57 @@ static JSON: OnceLock = OnceLock::new(); static RANDOM: OnceLock = OnceLock::new(); static STRING: OnceLock = OnceLock::new(); +/// Returns the entire built-in value API. pub fn built_in_values() -> impl Iterator { all() } +/// A variable with a hard-coded key that is globally available. #[derive(Sequence, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum BuiltInValue { + /// The arguments used to launch the current program. Args, + + /// Create an error if two values are not equal. AssertEqual, + + /// File system tools. Fs, + + /// JSON format tools. Json, + + /// Get the length of a collection. Length, + + /// Print a value to stdout. Output, + + /// Random value generators. Random, + + /// String utilities. Str, } impl BuiltInValue { + /// Returns the hard-coded key used to identify the value. pub fn name(&self) -> &'static str { match self { BuiltInValue::Args => "args", BuiltInValue::AssertEqual => "assert_equal", BuiltInValue::Fs => "fs", BuiltInValue::Json => "json", - BuiltInValue::Length => "length", + BuiltInValue::Length => BuiltInFunction::Length.name(), BuiltInValue::Output => "output", BuiltInValue::Random => "random", BuiltInValue::Str => "str", } } + /// Returns a brief description of the value's features. + /// + /// This is used by the shell when suggesting completions. pub fn description(&self) -> &'static str { match self { BuiltInValue::Args => "The command line arguments sent to this program.", @@ -57,6 +78,9 @@ impl BuiltInValue { } } + /// Returns the value's type. + /// + /// This is checked with a unit test to ensure it matches the value. pub fn r#type(&self) -> Type { match self { BuiltInValue::Args => Type::list(Type::String), @@ -70,6 +94,8 @@ impl BuiltInValue { } } + /// Returns the value by creating it or, if it has already been accessed, retrieving it from its + /// [OnceLock][]. pub fn get(&self) -> &Value { match self { BuiltInValue::Args => ARGS.get_or_init(|| { @@ -178,3 +204,18 @@ impl Format for BuiltInValue { output.push_str(&self.get().to_string()); } } + +#[cfg(test)] +mod tests { + use crate::built_in_values; + + #[test] + fn check_built_in_types() { + for built_in_value in built_in_values() { + let expected = built_in_value.r#type(); + let actual = built_in_value.get().r#type(); + + assert_eq!(expected, actual); + } + } +} diff --git a/src/interpret.rs b/src/interpret.rs index 1232772..ddc16b0 100644 --- a/src/interpret.rs +++ b/src/interpret.rs @@ -1,20 +1,48 @@ -//! Tools to run and/or format dust source code. +//! Tools to interpret dust source code. //! -//! You can use this library externally by calling either of the "interpret" -//! functions or by constructing your own Interpreter. -use tree_sitter::{Parser, Tree as TSTree, TreeCursor}; +//! This module has three tools to run Dust code. +//! +//! - [interpret] is the simples way to run Dust code inside of an application or library +//! - [interpret_with_context] allows you to set variables on the execution context +//! - [Interpreter] is an advanced tool that can parse, verify, run and format Dust code +//! +//! # Examples +//! +//! Run some Dust and get the result. +//! +//! ```rust +//! # use dust_lang::*; +//! assert_eq!( +//! interpret("1 + 2 + 3"), +//! Ok(Value::Integer(6)) +//! ); +//! ``` +//! +//! Create a custom context with variables you can use in your code. +//! +//! ```rust +//! # use dust_lang::*; +//! let context = Map::new(); +//! +//! context.set("one".into(), 1.into()); +//! context.set("two".into(), 2.into()); +//! context.set("three".into(), 3.into()); +//! +//! let dust_code = "four = 4; one + two + three + four"; +//! +//! assert_eq!( +//! interpret_with_context(dust_code, context), +//! Ok(Value::Integer(10)) +//! ); +//! ``` +use tree_sitter::{Node as SyntaxNode, Parser, Tree as SyntaxTree, TreeCursor}; -use crate::{language, AbstractTree, Error, Format, Map, Result, Root, SyntaxNode, Value}; +use crate::{language, AbstractTree, Error, Format, Map, Result, Root, Value}; /// Interpret the given source code. Returns the value of last statement or the /// first error encountered. /// -/// # Examples -/// -/// ```rust -/// # use dust_lang::*; -/// assert_eq!(interpret("1 + 2 + 3"), Ok(Value::Integer(6))); -/// ``` +/// See the [module-level docs][self] for more info. pub fn interpret(source: &str) -> Result { interpret_with_context(source, Map::new()) } @@ -26,23 +54,7 @@ pub fn interpret(source: &str) -> Result { /// for the `` type. Any value can be set, including functions and nested /// maps. /// -/// # Examples -/// -/// ```rust -/// # use dust_lang::*; -/// let context = Map::new(); -/// -/// context.set("one".into(), 1.into()); -/// context.set("two".into(), 2.into()); -/// context.set("three".into(), 3.into()); -/// -/// let dust_code = "four = 4 one + two + three + four"; -/// -/// assert_eq!( -/// interpret_with_context(dust_code, context), -/// Ok(Value::Integer(10)) -/// ); -/// ``` +/// See the [module-level docs][self] for more info. pub fn interpret_with_context(source: &str, context: Map) -> Result { let mut interpreter = Interpreter::new(context); let value = interpreter.run(source)?; @@ -51,14 +63,17 @@ pub fn interpret_with_context(source: &str, context: Map) -> Result { } /// A source code interpreter for the Dust language. +/// +/// The interpreter's most important functions are used to parse dust source code, verify it is safe +/// and run it and they are written in a way that forces them to be used safely. Each step in this +/// process contains the prior steps, meaning that the same code is always used to create the syntax /// tree, abstract tree and final evaluation. This avoids a critical logic error. pub struct Interpreter { parser: Parser, context: Map, - syntax_tree: Option, - abstract_tree: Option, } impl Interpreter { + /// Creates a new interpreter with the given variable context. pub fn new(context: Map) -> Self { let mut parser = Parser::new(); @@ -66,15 +81,35 @@ impl Interpreter { .set_language(language()) .expect("Language version is incompatible with tree sitter version."); - Interpreter { - parser, - context, - syntax_tree: None, - abstract_tree: None, + parser.set_logger(Some(Box::new(|log_type, message| { + log::info!("{}", message) + }))); + + Interpreter { parser, context } + } + + /// Generates a syntax tree from the source. Returns an error if the the parser is cancelled for + /// taking too long. The syntax tree may contain error nodes, which represent syntax errors. + /// + /// Tree sitter is designed to be run on every keystroke, so this is generally a lightweight + /// function to call. + pub fn parse(&mut self, source: &str) -> Result { + if let Some(tree) = self.parser.parse(source, None) { + Ok(tree) + } else { + Err(Error::ParserCancelled) } } - pub fn parse(&mut self, source: &str) -> Result<()> { + /// Checks the source for errors and generates an abstract tree. + /// + /// The order in which this function works is: + /// + /// - parse the source into a syntax tree + /// - check the syntax tree for errors + /// - generate an abstract tree from the source and syntax tree + /// - check the abstract tree for type errors + pub fn verify(&mut self, source: &str) -> Result { fn check_for_error(node: SyntaxNode, source: &str, cursor: &mut TreeCursor) -> Result<()> { if node.is_error() { Err(Error::Syntax { @@ -90,59 +125,44 @@ impl Interpreter { } } - let syntax_tree = self.parser.parse(source, None); + let syntax_tree = self.parse(source)?; + let root = syntax_tree.root_node(); + let mut cursor = syntax_tree.root_node().walk(); - if let Some(tree) = &syntax_tree { - let root = tree.root_node(); - let mut cursor = root.walk(); + check_for_error(root, source, &mut cursor)?; - check_for_error(root, source, &mut cursor)?; - } + let abstract_tree = Root::from_syntax(syntax_tree.root_node(), source, &self.context)?; - self.syntax_tree = syntax_tree; + abstract_tree.check_type(source, &self.context)?; - Ok(()) + Ok(abstract_tree) } + /// Runs the source, returning the final statement's value or first error. + /// + /// This function [parses][Self::parse], [verifies][Self::verify] and [runs][Root::run] using + /// the same source code. pub fn run(&mut self, source: &str) -> Result { - self.parse(source)?; - - self.abstract_tree = if let Some(syntax_tree) = &self.syntax_tree { - Some(Root::from_syntax( - syntax_tree.root_node(), - source, - &self.context, - )?) - } else { - return Err(Error::ParserCancelled); - }; - - if let Some(abstract_tree) = &self.abstract_tree { - abstract_tree.check_type(source, &self.context)?; - abstract_tree.run(source, &self.context) - } else { - Ok(Value::none()) - } + self.verify(source)?.run(source, &self.context) } - pub fn syntax_tree(&self) -> Result { - if let Some(syntax_tree) = &self.syntax_tree { - Ok(syntax_tree.root_node().to_sexp()) - } else { - Err(Error::ParserCancelled) - } + /// Return an s-expression displaying a syntax tree of the source, or the ParserCancelled error + /// if the parser takes too long. + pub fn syntax_tree(&mut self, source: &str) -> Result { + Ok(self.parse(source)?.root_node().to_sexp()) } - pub fn format(&self) -> String { - if let Some(root_node) = &self.abstract_tree { - let mut formatted_source = String::new(); + /// Return formatted Dust code generated from the current abstract tree, or None if no source + /// code has been run successfully. + /// + /// You should call [verify][Interpreter::verify] before calling this function. You can only + /// create formatted source from a valid abstract tree. + pub fn format(&mut self, source: &str) -> Result { + let mut formatted_output = String::new(); - root_node.format(&mut formatted_source, 0); + self.verify(source)?.format(&mut formatted_output, 0); - formatted_source - } else { - String::with_capacity(0) - } + Ok(formatted_output) } } diff --git a/src/lib.rs b/src/lib.rs index 4cd3a04..b9b3593 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,19 +1,21 @@ #![warn(missing_docs)] - //! The Dust library is used to parse, format and run dust source code. //! //! See the [interpret] module for more information. +//! +//! You can use this library externally by calling either of the "interpret" +//! functions or by constructing your own Interpreter. pub use crate::{ abstract_tree::*, built_in_functions::BuiltInFunction, error::*, interpret::*, value::*, }; pub use tree_sitter::Node as SyntaxNode; -mod abstract_tree; +pub mod abstract_tree; pub mod built_in_functions; -mod error; -mod interpret; -mod value; +pub mod error; +pub mod interpret; +pub mod value; use tree_sitter::Language; diff --git a/src/main.rs b/src/main.rs index 7bf3ad6..2a2b3b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -89,18 +89,17 @@ fn main() { if let Some(CliCommand::Syntax { path }) = args.cli_command { let source = read_to_string(path).unwrap(); + let syntax_tree_sexp = interpreter.syntax_tree(&source).unwrap(); - interpreter.parse(&source).unwrap(); - - println!("{}", interpreter.syntax_tree().unwrap()); + println!("{syntax_tree_sexp}"); return; } if let Some(CliCommand::Format) = args.cli_command { - interpreter.parse(&source).unwrap(); + let formatted = interpreter.format(&source).unwrap(); - println!("{}", interpreter.format()); + println!("{formatted}"); return; } diff --git a/tests/format.rs b/tests/format.rs index e4f0a55..e7148f7 100644 --- a/tests/format.rs +++ b/tests/format.rs @@ -4,9 +4,7 @@ use dust_lang::*; fn format_simple_program() { let mut interpreter = Interpreter::new(Map::new()); - interpreter.run("x=1").unwrap(); - - assert_eq!(interpreter.format(), "x = 1\n"); + assert_eq!(interpreter.format("x=1"), Ok("x = 1\n".to_string())); } const FORMATTED_BLOCK: &str = "{ @@ -20,9 +18,10 @@ const FORMATTED_BLOCK: &str = "{ fn format_block() { let mut interpreter = Interpreter::new(Map::new()); - interpreter.run("{1 2 3}").unwrap(); - - assert_eq!(FORMATTED_BLOCK, interpreter.format()); + assert_eq!( + interpreter.format("{1 2 3}"), + Ok(FORMATTED_BLOCK.to_string()) + ); } const FORMATTED_MAP: &str = "{ @@ -37,9 +36,10 @@ const FORMATTED_MAP: &str = "{ fn format_map() { let mut interpreter = Interpreter::new(Map::new()); - interpreter.run("{{x=1 y = 2}}").unwrap(); - - assert_eq!(FORMATTED_MAP, interpreter.format()); + assert_eq!( + interpreter.format("{{x=1 y = 2}}"), + Ok(FORMATTED_MAP.to_string()) + ); } const FORMATTED_FUNCTION: &str = "(x ) { @@ -50,8 +50,8 @@ const FORMATTED_FUNCTION: &str = "(x ) { #[test] fn format_function() { let mut interpreter = Interpreter::new(Map::new()); - - interpreter.run("( x< int > ){x/2}").unwrap(); - - assert_eq!(FORMATTED_FUNCTION, interpreter.format()); + assert_eq!( + interpreter.format("( x< int > ){x/2}"), + Ok(FORMATTED_FUNCTION.to_string()) + ); }