Write docs; Refine library API
This commit is contained in:
parent
7c9be2151d
commit
4cbfdde4a3
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
172
src/interpret.rs
172
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<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)
|
||||
}
|
||||
}
|
||||
|
||||
|
12
src/lib.rs
12
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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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())
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user