Write docs; Refine library API

This commit is contained in:
Jeff 2024-01-30 18:13:30 -05:00
parent 7c9be2151d
commit 4cbfdde4a3
7 changed files with 164 additions and 100 deletions

View File

@ -5,6 +5,7 @@ use crate::{
SyntaxNode, SyntaxPosition, Type, TypeSpecification, Value, SyntaxNode, SyntaxPosition, Type, TypeSpecification, Value,
}; };
/// Variable assignment, including add-assign and subtract-assign operations.
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)] #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub struct Assignment { pub struct Assignment {
identifier: Identifier, identifier: Identifier,

View File

@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
use crate::{AbstractTree, Error, Format, Map, Result, SyntaxNode, Type, Value}; 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)] #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub enum AssignmentOperator { pub enum AssignmentOperator {
Equal, Equal,

View File

@ -14,36 +14,57 @@ static JSON: OnceLock<Value> = OnceLock::new();
static RANDOM: OnceLock<Value> = OnceLock::new(); static RANDOM: OnceLock<Value> = OnceLock::new();
static STRING: OnceLock<Value> = OnceLock::new(); static STRING: OnceLock<Value> = OnceLock::new();
/// Returns the entire built-in value API.
pub fn built_in_values() -> impl Iterator<Item = BuiltInValue> { pub fn built_in_values() -> impl Iterator<Item = BuiltInValue> {
all() all()
} }
/// A variable with a hard-coded key that is globally available.
#[derive(Sequence, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[derive(Sequence, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum BuiltInValue { pub enum BuiltInValue {
/// The arguments used to launch the current program.
Args, Args,
/// Create an error if two values are not equal.
AssertEqual, AssertEqual,
/// File system tools.
Fs, Fs,
/// JSON format tools.
Json, Json,
/// Get the length of a collection.
Length, Length,
/// Print a value to stdout.
Output, Output,
/// Random value generators.
Random, Random,
/// String utilities.
Str, Str,
} }
impl BuiltInValue { impl BuiltInValue {
/// Returns the hard-coded key used to identify the value.
pub fn name(&self) -> &'static str { pub fn name(&self) -> &'static str {
match self { match self {
BuiltInValue::Args => "args", BuiltInValue::Args => "args",
BuiltInValue::AssertEqual => "assert_equal", BuiltInValue::AssertEqual => "assert_equal",
BuiltInValue::Fs => "fs", BuiltInValue::Fs => "fs",
BuiltInValue::Json => "json", BuiltInValue::Json => "json",
BuiltInValue::Length => "length", BuiltInValue::Length => BuiltInFunction::Length.name(),
BuiltInValue::Output => "output", BuiltInValue::Output => "output",
BuiltInValue::Random => "random", BuiltInValue::Random => "random",
BuiltInValue::Str => "str", 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 { pub fn description(&self) -> &'static str {
match self { match self {
BuiltInValue::Args => "The command line arguments sent to this program.", 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 { pub fn r#type(&self) -> Type {
match self { match self {
BuiltInValue::Args => Type::list(Type::String), 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 { pub fn get(&self) -> &Value {
match self { match self {
BuiltInValue::Args => ARGS.get_or_init(|| { BuiltInValue::Args => ARGS.get_or_init(|| {
@ -178,3 +204,18 @@ impl Format for BuiltInValue {
output.push_str(&self.get().to_string()); 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);
}
}
}

View File

@ -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" //! This module has three tools to run Dust code.
//! functions or by constructing your own Interpreter. //!
use tree_sitter::{Parser, Tree as TSTree, TreeCursor}; //! - [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 /// Interpret the given source code. Returns the value of last statement or the
/// first error encountered. /// first error encountered.
/// ///
/// # Examples /// See the [module-level docs][self] for more info.
///
/// ```rust
/// # use dust_lang::*;
/// assert_eq!(interpret("1 + 2 + 3"), Ok(Value::Integer(6)));
/// ```
pub fn interpret(source: &str) -> Result<Value> { pub fn interpret(source: &str) -> Result<Value> {
interpret_with_context(source, Map::new()) interpret_with_context(source, Map::new())
} }
@ -26,23 +54,7 @@ pub fn interpret(source: &str) -> Result<Value> {
/// for the `<map>` type. Any value can be set, including functions and nested /// for the `<map>` type. Any value can be set, including functions and nested
/// maps. /// maps.
/// ///
/// # Examples /// See the [module-level docs][self] for more info.
///
/// ```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))
/// );
/// ```
pub fn interpret_with_context(source: &str, context: Map) -> Result<Value> { pub fn interpret_with_context(source: &str, context: Map) -> Result<Value> {
let mut interpreter = Interpreter::new(context); let mut interpreter = Interpreter::new(context);
let value = interpreter.run(source)?; let value = interpreter.run(source)?;
@ -51,14 +63,17 @@ pub fn interpret_with_context(source: &str, context: Map) -> Result<Value> {
} }
/// A source code interpreter for the Dust language. /// 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 { pub struct Interpreter {
parser: Parser, parser: Parser,
context: Map, context: Map,
syntax_tree: Option<TSTree>,
abstract_tree: Option<Root>,
} }
impl Interpreter { impl Interpreter {
/// Creates a new interpreter with the given variable context.
pub fn new(context: Map) -> Self { pub fn new(context: Map) -> Self {
let mut parser = Parser::new(); let mut parser = Parser::new();
@ -66,15 +81,35 @@ impl Interpreter {
.set_language(language()) .set_language(language())
.expect("Language version is incompatible with tree sitter version."); .expect("Language version is incompatible with tree sitter version.");
Interpreter { parser.set_logger(Some(Box::new(|log_type, message| {
parser, log::info!("{}", message)
context, })));
syntax_tree: None,
abstract_tree: None, 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<SyntaxTree> {
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<Root> {
fn check_for_error(node: SyntaxNode, source: &str, cursor: &mut TreeCursor) -> Result<()> { fn check_for_error(node: SyntaxNode, source: &str, cursor: &mut TreeCursor) -> Result<()> {
if node.is_error() { if node.is_error() {
Err(Error::Syntax { 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();
if let Some(tree) = &syntax_tree { let mut cursor = syntax_tree.root_node().walk();
let root = tree.root_node();
let mut cursor = root.walk();
check_for_error(root, source, &mut cursor)?; check_for_error(root, source, &mut cursor)?;
}
self.syntax_tree = syntax_tree; let abstract_tree = Root::from_syntax(syntax_tree.root_node(), source, &self.context)?;
Ok(())
}
pub fn run(&mut self, source: &str) -> Result<Value> {
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.check_type(source, &self.context)?;
abstract_tree.run(source, &self.context)
} else { Ok(abstract_tree)
Ok(Value::none())
}
} }
pub fn syntax_tree(&self) -> Result<String> { /// Runs the source, returning the final statement's value or first error.
if let Some(syntax_tree) = &self.syntax_tree { ///
Ok(syntax_tree.root_node().to_sexp()) /// This function [parses][Self::parse], [verifies][Self::verify] and [runs][Root::run] using
} else { /// the same source code.
Err(Error::ParserCancelled) pub fn run(&mut self, source: &str) -> Result<Value> {
} self.verify(source)?.run(source, &self.context)
} }
pub fn format(&self) -> String { /// Return an s-expression displaying a syntax tree of the source, or the ParserCancelled error
if let Some(root_node) = &self.abstract_tree { /// if the parser takes too long.
let mut formatted_source = String::new(); pub fn syntax_tree(&mut self, source: &str) -> Result<String> {
Ok(self.parse(source)?.root_node().to_sexp())
root_node.format(&mut formatted_source, 0);
formatted_source
} else {
String::with_capacity(0)
} }
/// 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<String> {
let mut formatted_output = String::new();
self.verify(source)?.format(&mut formatted_output, 0);
Ok(formatted_output)
} }
} }

View File

@ -1,19 +1,21 @@
#![warn(missing_docs)] #![warn(missing_docs)]
//! The Dust library is used to parse, format and run dust source code. //! The Dust library is used to parse, format and run dust source code.
//! //!
//! See the [interpret] module for more information. //! 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::{ pub use crate::{
abstract_tree::*, built_in_functions::BuiltInFunction, error::*, interpret::*, value::*, abstract_tree::*, built_in_functions::BuiltInFunction, error::*, interpret::*, value::*,
}; };
pub use tree_sitter::Node as SyntaxNode; pub use tree_sitter::Node as SyntaxNode;
mod abstract_tree; pub mod abstract_tree;
pub mod built_in_functions; pub mod built_in_functions;
mod error; pub mod error;
mod interpret; pub mod interpret;
mod value; pub mod value;
use tree_sitter::Language; use tree_sitter::Language;

View File

@ -89,18 +89,17 @@ fn main() {
if let Some(CliCommand::Syntax { path }) = args.cli_command { if let Some(CliCommand::Syntax { path }) = args.cli_command {
let source = read_to_string(path).unwrap(); let source = read_to_string(path).unwrap();
let syntax_tree_sexp = interpreter.syntax_tree(&source).unwrap();
interpreter.parse(&source).unwrap(); println!("{syntax_tree_sexp}");
println!("{}", interpreter.syntax_tree().unwrap());
return; return;
} }
if let Some(CliCommand::Format) = args.cli_command { if let Some(CliCommand::Format) = args.cli_command {
interpreter.parse(&source).unwrap(); let formatted = interpreter.format(&source).unwrap();
println!("{}", interpreter.format()); println!("{formatted}");
return; return;
} }

View File

@ -4,9 +4,7 @@ use dust_lang::*;
fn format_simple_program() { fn format_simple_program() {
let mut interpreter = Interpreter::new(Map::new()); let mut interpreter = Interpreter::new(Map::new());
interpreter.run("x=1").unwrap(); assert_eq!(interpreter.format("x=1"), Ok("x = 1\n".to_string()));
assert_eq!(interpreter.format(), "x = 1\n");
} }
const FORMATTED_BLOCK: &str = "{ const FORMATTED_BLOCK: &str = "{
@ -20,9 +18,10 @@ const FORMATTED_BLOCK: &str = "{
fn format_block() { fn format_block() {
let mut interpreter = Interpreter::new(Map::new()); let mut interpreter = Interpreter::new(Map::new());
interpreter.run("{1 2 3}").unwrap(); assert_eq!(
interpreter.format("{1 2 3}"),
assert_eq!(FORMATTED_BLOCK, interpreter.format()); Ok(FORMATTED_BLOCK.to_string())
);
} }
const FORMATTED_MAP: &str = "{ const FORMATTED_MAP: &str = "{
@ -37,9 +36,10 @@ const FORMATTED_MAP: &str = "{
fn format_map() { fn format_map() {
let mut interpreter = Interpreter::new(Map::new()); let mut interpreter = Interpreter::new(Map::new());
interpreter.run("{{x=1 y <int> = 2}}").unwrap(); assert_eq!(
interpreter.format("{{x=1 y <int> = 2}}"),
assert_eq!(FORMATTED_MAP, interpreter.format()); Ok(FORMATTED_MAP.to_string())
);
} }
const FORMATTED_FUNCTION: &str = "(x <int>) <num> { const FORMATTED_FUNCTION: &str = "(x <int>) <num> {
@ -50,8 +50,8 @@ const FORMATTED_FUNCTION: &str = "(x <int>) <num> {
#[test] #[test]
fn format_function() { fn format_function() {
let mut interpreter = Interpreter::new(Map::new()); let mut interpreter = Interpreter::new(Map::new());
assert_eq!(
interpreter.run("( x< int > )<num>{x/2}").unwrap(); interpreter.format("( x< int > )<num>{x/2}"),
Ok(FORMATTED_FUNCTION.to_string())
assert_eq!(FORMATTED_FUNCTION, interpreter.format()); );
} }