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,
};
/// Variable assignment, including add-assign and subtract-assign operations.
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub struct Assignment {
identifier: Identifier,

View File

@ -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,

View File

@ -14,36 +14,57 @@ static JSON: OnceLock<Value> = OnceLock::new();
static RANDOM: 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> {
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);
}
}
}

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"
//! 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<Value> {
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
/// 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<Value> {
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<Value> {
}
/// 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<TSTree>,
abstract_tree: Option<Root>,
}
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<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<()> {
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<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.run(source, &self.context)
} else {
Ok(Value::none())
}
self.verify(source)?.run(source, &self.context)
}
pub fn syntax_tree(&self) -> Result<String> {
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<String> {
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<String> {
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)
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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 <int> = 2}}").unwrap();
assert_eq!(FORMATTED_MAP, interpreter.format());
assert_eq!(
interpreter.format("{{x=1 y <int> = 2}}"),
Ok(FORMATTED_MAP.to_string())
);
}
const FORMATTED_FUNCTION: &str = "(x <int>) <num> {
@ -50,8 +50,8 @@ const FORMATTED_FUNCTION: &str = "(x <int>) <num> {
#[test]
fn format_function() {
let mut interpreter = Interpreter::new(Map::new());
interpreter.run("( x< int > )<num>{x/2}").unwrap();
assert_eq!(FORMATTED_FUNCTION, interpreter.format());
assert_eq!(
interpreter.format("( x< int > )<num>{x/2}"),
Ok(FORMATTED_FUNCTION.to_string())
);
}