1
0

Continue syntax overhaul

This commit is contained in:
Jeff 2023-10-06 13:32:58 -04:00
parent 059e55c7aa
commit 3e87d8b322
34 changed files with 907 additions and 3365 deletions

View File

@ -0,0 +1,36 @@
use serde::{Deserialize, Serialize};
use tree_sitter::Node;
use crate::{AbstractTree, Result, Value, VariableMap};
use super::{identifier::Identifier, statement::Statement};
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub struct Assignment {
identifier: Identifier,
statement: Statement,
}
impl AbstractTree for Assignment {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
let identifier_node = node.child(0).unwrap();
let identifier = Identifier::from_syntax_node(identifier_node, source)?;
let statement_node = node.child(2).unwrap();
let statement = Statement::from_syntax_node(statement_node, source)?;
Ok(Assignment {
identifier,
statement,
})
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
let key = self.identifier.clone().take_inner();
let value = self.statement.run(context)?;
context.set_value(key, value)?;
Ok(Value::Empty)
}
}

View File

@ -0,0 +1,58 @@
use serde::{Deserialize, Serialize};
use tree_sitter::Node;
use crate::{tool::ToolCall, AbstractTree, Error, Identifier, Result, Value, VariableMap};
use super::{function_call::FunctionCall, logic::Logic, math::Math};
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub enum Expression {
Identifier(Identifier),
Value(Value),
Math(Box<Math>),
Logic(Box<Logic>),
FunctionCall(FunctionCall),
ToolCall(Box<ToolCall>),
}
impl AbstractTree for Expression {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
debug_assert_eq!("expression", node.kind());
let child = node.child(0).unwrap();
let expression = match child.kind() {
"value" => Expression::Value(Value::from_syntax_node(child, source)?),
"identifier" => Self::Identifier(Identifier::from_syntax_node(child, source)?),
"math" => Expression::Math(Box::new(Math::from_syntax_node(child, source)?)),
"logic" => Expression::Logic(Box::new(Logic::from_syntax_node(child, source)?)),
"function_call" => {
Expression::FunctionCall(FunctionCall::from_syntax_node(child, source)?)
}
"tool_call" => {
Expression::ToolCall(Box::new(ToolCall::from_syntax_node(child, source)?))
}
_ => {
return Err(Error::UnexpectedSyntax {
expected: "value, identifier, math or function_call",
actual: child.kind(),
location: child.start_position(),
relevant_source: source[node.byte_range()].to_string(),
})
}
};
Ok(expression)
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
match self {
Expression::Value(value) => Ok(value.clone()),
Expression::Identifier(identifier) => identifier.run(context),
Expression::Math(math) => math.run(context),
Expression::Logic(logic) => logic.run(context),
Expression::FunctionCall(function_call) => function_call.run(context),
Expression::ToolCall(tool_call) => tool_call.run(context),
}
}
}

View File

@ -0,0 +1,50 @@
use serde::{Deserialize, Serialize};
use tree_sitter::Node;
use crate::{AbstractTree, Result, Value, VariableMap};
use super::{expression::Expression, identifier::Identifier};
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub struct FunctionCall {
identifier: Identifier,
expressions: Vec<Expression>,
}
impl AbstractTree for FunctionCall {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
debug_assert_eq!("function_call", node.kind());
let identifier_node = node.child(1).unwrap();
let identifier = Identifier::from_syntax_node(identifier_node, source)?;
let mut expressions = Vec::new();
let mut current_index = 2;
while current_index < node.child_count() - 1 {
let expression_node = node.child(current_index).unwrap();
let expression = Expression::from_syntax_node(expression_node, source)?;
expressions.push(expression);
current_index += 1;
}
Ok(FunctionCall {
identifier,
expressions,
})
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
let identifier = &self.identifier;
let definition = if let Some(value) = context.get_value(identifier.inner())? {
value
} else {
return Err(crate::Error::FunctionIdentifierNotFound(identifier.clone()));
};
let mut arguments = Vec::with_capacity(self.expressions.len());
Ok(Value::List(arguments))
}
}

View File

@ -0,0 +1,35 @@
use serde::{Deserialize, Serialize};
use tree_sitter::Node;
use crate::{AbstractTree, Result, Value, VariableMap};
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub struct Identifier(String);
impl Identifier {
pub fn new(inner: String) -> Self {
Identifier(inner)
}
pub fn take_inner(self) -> String {
self.0
}
pub fn inner(&self) -> &String {
&self.0
}
}
impl AbstractTree for Identifier {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
let identifier = &source[node.byte_range()];
Ok(Identifier(identifier.to_string()))
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
let value = context.get_value(&self.0)?.unwrap_or_default();
Ok(value)
}
}

View File

@ -0,0 +1,48 @@
use serde::{Deserialize, Serialize};
use tree_sitter::Node;
use crate::{AbstractTree, Expression, Result, Statement, Value, VariableMap};
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub struct IfElse {
if_expression: Expression,
then_statement: Statement,
else_statement: Option<Statement>,
}
impl AbstractTree for IfElse {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
let if_node = node.child(1).unwrap();
let if_expression = Expression::from_syntax_node(if_node, source)?;
let then_node = node.child(3).unwrap();
let then_statement = Statement::from_syntax_node(then_node, source)?;
let else_node = node.child(5);
let else_statement = if let Some(node) = else_node {
Some(Statement::from_syntax_node(node, source)?)
} else {
None
};
println!("{if_node:?} {then_node:?} {else_node:?}");
Ok(IfElse {
if_expression,
then_statement,
else_statement,
})
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
let if_boolean = self.if_expression.run(context)?.as_boolean()?;
if if_boolean {
self.then_statement.run(context)
} else if let Some(statement) = &self.else_statement {
statement.run(context)
} else {
Ok(Value::Empty)
}
}
}

45
src/abstract_tree/item.rs Normal file
View File

@ -0,0 +1,45 @@
//! Top-level unit of Dust code.
use tree_sitter::Node;
use crate::{AbstractTree, Error, Result, Statement, Value, VariableMap};
/// An abstractiton of an independent unit of source code, or a comment.
///
/// Items are either comments, which do nothing, or statements, which can be run
/// to produce a single value or interact with a context by creating or
/// referencing variables.
#[derive(Debug)]
pub enum Item {
Comment(String),
Statement(Statement),
}
impl AbstractTree for Item {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
let child = node.child(0).unwrap();
if child.kind() == "comment" {
let byte_range = child.byte_range();
let comment_text = &source[byte_range];
Ok(Item::Comment(comment_text.to_string()))
} else if child.kind() == "statement" {
Ok(Item::Statement(Statement::from_syntax_node(child, source)?))
} else {
Err(Error::UnexpectedSyntax {
expected: "comment or statement",
actual: child.kind(),
location: child.start_position(),
relevant_source: source[node.byte_range()].to_string(),
})
}
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
match self {
Item::Comment(text) => Ok(Value::String(text.clone())),
Item::Statement(statement) => statement.run(context),
}
}
}

View File

@ -0,0 +1,61 @@
use serde::{Deserialize, Serialize};
use tree_sitter::Node;
use crate::{AbstractTree, Error, Expression, Result, Value, VariableMap};
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub struct Logic {
left: Expression,
operator: LogicOperator,
right: Expression,
}
impl AbstractTree for Logic {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
let left_node = node.child(0).unwrap();
let left = Expression::from_syntax_node(left_node, source)?;
let operator_node = node.child(1).unwrap().child(0).unwrap();
let operator = match operator_node.kind() {
"==" => LogicOperator::Equal,
"&&" => LogicOperator::And,
"||" => LogicOperator::Or,
_ => {
return Err(Error::UnexpectedSyntax {
expected: "==, && or ||",
actual: operator_node.kind(),
location: operator_node.start_position(),
relevant_source: source[operator_node.byte_range()].to_string(),
})
}
};
let right_node = node.child(2).unwrap();
let right = Expression::from_syntax_node(right_node, source)?;
Ok(Logic {
left,
operator,
right,
})
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
let left_value = self.left.run(context)?;
let right_value = self.right.run(context)?;
let outcome = match self.operator {
LogicOperator::Equal => left_value == right_value,
LogicOperator::And => left_value.as_boolean()? && right_value.as_boolean()?,
LogicOperator::Or => left_value.as_boolean()? || right_value.as_boolean()?,
};
Ok(Value::Boolean(outcome))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub enum LogicOperator {
Equal,
And,
Or,
}

View File

@ -0,0 +1,22 @@
//! Pattern matching.
//!
//! Note that this module is called "match" but is escaped as "r#match" because
//! "match" is a keyword in Rust.
use serde::{Deserialize, Serialize};
use tree_sitter::Node;
use crate::{AbstractTree, Result, Value, VariableMap};
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub struct Match {}
impl AbstractTree for Match {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
todo!()
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
todo!()
}
}

81
src/abstract_tree/math.rs Normal file
View File

@ -0,0 +1,81 @@
use serde::{Deserialize, Serialize};
use tree_sitter::Node;
use crate::{AbstractTree, Error, Expression, Result, Value, VariableMap};
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub struct Math {
left: Expression,
operator: MathOperator,
right: Expression,
}
impl AbstractTree for Math {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
let left_node = node.child(0).unwrap();
let left = Expression::from_syntax_node(left_node, source)?;
let operator_node = node.child(1).unwrap().child(0).unwrap();
let operator = match operator_node.kind() {
"+" => MathOperator::Add,
"-" => MathOperator::Subtract,
"*" => MathOperator::Multiply,
"/" => MathOperator::Divide,
"%" => MathOperator::Modulo,
_ => {
return Err(Error::UnexpectedSyntax {
expected: "+, -, *, / or %",
actual: operator_node.kind(),
location: operator_node.start_position(),
relevant_source: source[operator_node.byte_range()].to_string(),
})
}
};
let right_node = node.child(2).unwrap();
let right = Expression::from_syntax_node(right_node, source)?;
Ok(Math {
left,
operator,
right,
})
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
match self.operator {
MathOperator::Add | MathOperator::Subtract | MathOperator::Multiply => {
let left_value = self.left.run(context)?.as_int()?;
let right_value = self.right.run(context)?.as_int()?;
let outcome = match &self.operator {
MathOperator::Add => left_value + right_value,
MathOperator::Subtract => left_value - right_value,
MathOperator::Multiply => left_value * right_value,
_ => panic!("Unreachable"),
};
Ok(Value::Integer(outcome))
}
MathOperator::Divide | MathOperator::Modulo => {
let left_value = self.left.run(context)?.as_number()?;
let right_value = self.right.run(context)?.as_number()?;
let outcome = match self.operator {
MathOperator::Divide => left_value / right_value,
MathOperator::Modulo => left_value % right_value,
_ => panic!("Unreachable"),
};
Ok(Value::Float(outcome))
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub enum MathOperator {
Add,
Subtract,
Multiply,
Divide,
Modulo,
}

38
src/abstract_tree/mod.rs Normal file
View File

@ -0,0 +1,38 @@
pub mod assignment;
pub mod expression;
pub mod function_call;
pub mod identifier;
pub mod if_else;
pub mod item;
pub mod logic;
pub mod r#match;
pub mod math;
pub mod statement;
pub mod tool;
pub use {
assignment::*, expression::*, function_call::*, identifier::*, if_else::*, item::*, logic::*,
math::*, r#match::*, statement::*,
};
use tree_sitter::Node;
use crate::{Result, Value, VariableMap};
/// This trait is implemented by the Evaluator's internal types to form an
/// executable tree that resolves to a single value.
pub trait AbstractTree: Sized {
/// Interpret the syntax tree at the given node and return the abstraction.
///
/// This function is used to convert nodes in the Tree Sitter concrete
/// syntax tree into executable nodes in an abstract tree. This function is
/// where the tree should be traversed by accessing sibling and child nodes.
/// Each node in the CST should be traversed only once.
///
/// If necessary, the source code can be accessed directly by getting the
/// node's byte range.
fn from_syntax_node(node: Node, source: &str) -> Result<Self>;
/// Execute dust code by traversing the tree
fn run(&self, context: &mut VariableMap) -> Result<Value>;
}

View File

@ -0,0 +1,59 @@
use serde::{Deserialize, Serialize};
use tree_sitter::Node;
use crate::{
tool::ToolCall, AbstractTree, Assignment, Error, Expression, IfElse, Match, Result, Value,
VariableMap,
};
/// Abstract representation of a statement.
///
/// Items are either comments, which do nothing, or statements, which can be run
/// to produce a single value or interact with their context.
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub enum Statement {
Assignment(Box<Assignment>),
Expression(Expression),
IfElse(Box<IfElse>),
Match(Match),
Tool(ToolCall),
}
impl AbstractTree for Statement {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
debug_assert_eq!("statement", node.kind());
let child = node.child(0).unwrap();
match child.kind() {
"assignment" => Ok(Statement::Assignment(Box::new(
Assignment::from_syntax_node(child, source)?,
))),
"expression" => Ok(Self::Expression(Expression::from_syntax_node(
child, source,
)?)),
"if_else" => Ok(Statement::IfElse(Box::new(IfElse::from_syntax_node(
child, source,
)?))),
"tool" => Ok(Statement::IfElse(Box::new(IfElse::from_syntax_node(
child, source,
)?))),
_ => Err(Error::UnexpectedSyntax {
expected: "assignment, expression, if...else or tool",
actual: child.kind(),
location: child.start_position(),
relevant_source: source[node.byte_range()].to_string(),
}),
}
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
match self {
Statement::Assignment(assignment) => assignment.run(context),
Statement::Expression(expression) => expression.run(context),
Statement::IfElse(if_else) => if_else.run(context),
Statement::Match(r#match) => r#match.run(context),
Statement::Tool(tool) => tool.run(context),
}
}
}

43
src/abstract_tree/tool.rs Normal file
View File

@ -0,0 +1,43 @@
use serde::{Deserialize, Serialize};
use tree_sitter::Node;
use crate::{AbstractTree, Error, Expression, Result, Value, VariableMap};
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub enum ToolCall {
Output(Expression),
}
impl AbstractTree for ToolCall {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
let tool_node = node.child(1).unwrap();
let tool_name = tool_node.kind();
match tool_name {
"output" => {
let expression_node = node.child(1).unwrap();
let expression = Expression::from_syntax_node(expression_node, source)?;
Ok(ToolCall::Output(expression))
}
_ => Err(Error::UnexpectedSyntax {
expected: "output",
actual: tool_name,
location: tool_node.start_position(),
relevant_source: tool_node.kind().to_string(),
}),
}
}
fn run(&self, context: &mut VariableMap) -> Result<crate::Value> {
match self {
ToolCall::Output(expression) => {
let value = expression.run(context)?;
println!("{value}")
}
}
Ok(Value::Empty)
}
}

View File

@ -3,7 +3,7 @@
//! To deal with errors from dependencies, either create a new error variant //! To deal with errors from dependencies, either create a new error variant
//! or use the MacroFailure variant if the error can only occur inside a macro. //! or use the MacroFailure variant if the error can only occur inside a macro.
use crate::{value::value_type::ValueType, value::Value}; use crate::{value::value_type::ValueType, value::Value, Identifier};
use std::{fmt, io, time::SystemTimeError}; use std::{fmt, io, time::SystemTimeError};
@ -125,7 +125,7 @@ pub enum Error {
VariableIdentifierNotFound(String), VariableIdentifierNotFound(String),
/// A `FunctionIdentifier` operation did not find its value in the context. /// A `FunctionIdentifier` operation did not find its value in the context.
FunctionIdentifierNotFound(String), FunctionIdentifierNotFound(Identifier),
/// A value has the wrong type. /// A value has the wrong type.
/// Only use this if there is no other error that describes the expected and /// Only use this if there is no other error that describes the expected and

314
src/evaluator.rs Normal file
View File

@ -0,0 +1,314 @@
//! The top level of Dust's API with functions to interpret Dust code.
//!
//! You can use this library externally by calling either of the "eval"
//! functions or by constructing your own Evaluator.
use std::fmt::{self, Debug, Formatter};
use tree_sitter::{Parser, Tree as TSTree};
use crate::{abstract_tree::item::Item, language, AbstractTree, Result, Value, VariableMap};
/// Evaluate the given source code.
///
/// Returns a vector of results from evaluating the source code. Each comment
/// and statemtent will have its own result.
///
/// # Examples
///
/// ```rust
/// # use dust::*;
/// assert_eq!(evaluate("1 + 2 + 3"), vec![Ok(Value::Integer(6))]);
/// ```
pub fn evaluate(source: &str) -> Vec<Result<Value>> {
let mut context = VariableMap::new();
evaluate_with_context(source, &mut context)
}
/// Evaluate the given source code with the given context.
///
/// # Examples
///
/// ```rust
/// # use dust::*;
/// let mut context = VariableMap::new();
///
/// context.set_value("one".into(), 1.into());
/// context.set_value("two".into(), 2.into());
/// context.set_value("three".into(), 3.into());
///
/// let dust_code = "four = 4 one + two + three + four";
///
/// assert_eq!(
/// evaluate_with_context(dust_code, &mut context),
/// vec![Ok(Value::Empty), Ok(Value::Integer(10))]
/// );
/// ```
pub fn evaluate_with_context(source: &str, context: &mut VariableMap) -> Vec<Result<Value>> {
let mut parser = Parser::new();
parser.set_language(language()).unwrap();
Evaluator::new(parser, context, source).run()
}
/// A collection of statements and comments interpreted from a syntax tree.
///
/// The Evaluator turns a tree sitter concrete syntax tree into a vector of
/// abstract trees called [Item][]s that can be run to execute the source code.
pub struct Evaluator<'context, 'code> {
_parser: Parser,
context: &'context mut VariableMap,
source: &'code str,
syntax_tree: TSTree,
}
impl Debug for Evaluator<'_, '_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "Evaluator context: {}", self.context)
}
}
impl<'context, 'code> Evaluator<'context, 'code> {
fn new(mut parser: Parser, context: &'context mut VariableMap, source: &'code str) -> Self {
let syntax_tree = parser.parse(source, None).unwrap();
Evaluator {
_parser: parser,
context,
source,
syntax_tree,
}
}
fn run(self) -> Vec<Result<Value>> {
let mut cursor = self.syntax_tree.walk();
let root_node = cursor.node();
let item_count = root_node.child_count();
let mut results = Vec::with_capacity(item_count);
println!("{}", root_node.to_sexp());
for item_node in root_node.children(&mut cursor) {
let item_result = Item::from_syntax_node(item_node, self.source);
match item_result {
Ok(item) => {
let eval_result = item.run(self.context);
results.push(eval_result);
}
Err(error) => results.push(Err(error)),
}
}
results
}
}
#[cfg(test)]
mod tests {
use crate::{
abstract_tree::{expression::Expression, identifier::Identifier, statement::Statement},
tool::ToolCall,
value::variable_map,
Function, FunctionCall, Table,
};
use super::*;
#[test]
fn evaluate_empty() {
assert_eq!(evaluate("x = 9"), vec![Ok(Value::Empty)]);
assert_eq!(evaluate("x = 1 + 1"), vec![Ok(Value::Empty)]);
}
#[test]
fn evaluate_integer() {
assert_eq!(evaluate("1"), vec![Ok(Value::Integer(1))]);
assert_eq!(evaluate("123"), vec![Ok(Value::Integer(123))]);
assert_eq!(evaluate("-666"), vec![Ok(Value::Integer(-666))]);
}
#[test]
fn evaluate_float() {
assert_eq!(evaluate("0.1"), vec![Ok(Value::Float(0.1))]);
assert_eq!(evaluate("12.3"), vec![Ok(Value::Float(12.3))]);
assert_eq!(evaluate("-6.66"), vec![Ok(Value::Float(-6.66))]);
}
#[test]
fn evaluate_string() {
assert_eq!(
evaluate("\"one\""),
vec![Ok(Value::String("one".to_string()))]
);
assert_eq!(
evaluate("'one'"),
vec![Ok(Value::String("one".to_string()))]
);
assert_eq!(
evaluate("`one`"),
vec![Ok(Value::String("one".to_string()))]
);
assert_eq!(
evaluate("`'one'`"),
vec![Ok(Value::String("'one'".to_string()))]
);
assert_eq!(
evaluate("'`one`'"),
vec![Ok(Value::String("`one`".to_string()))]
);
assert_eq!(
evaluate("\"'one'\""),
vec![Ok(Value::String("'one'".to_string()))]
);
}
#[test]
fn evaluate_list() {
assert_eq!(
evaluate("[1, 2, 'foobar']"),
vec![Ok(Value::List(vec![
Value::Integer(1),
Value::Integer(2),
Value::String("foobar".to_string()),
]))]
);
}
#[test]
fn evaluate_map() {
let mut map = VariableMap::new();
map.set_value("x".to_string(), Value::Integer(1)).unwrap();
map.set_value("foo".to_string(), Value::String("bar".to_string()))
.unwrap();
assert_eq!(evaluate("{ x = 1 foo = 'bar' }"), vec![Ok(Value::Map(map))]);
}
#[test]
fn evaluate_table() {
let mut table = Table::new(vec!["messages".to_string(), "numbers".to_string()]);
table
.insert(vec![Value::String("hiya".to_string()), Value::Integer(42)])
.unwrap();
table
.insert(vec![Value::String("foo".to_string()), Value::Integer(57)])
.unwrap();
table
.insert(vec![Value::String("bar".to_string()), Value::Float(99.99)])
.unwrap();
assert_eq!(
evaluate(
"
table <messages, numbers> {
['hiya', 42]
['foo', 57]
['bar', 99.99]
}
"
),
vec![Ok(Value::Table(table))]
);
}
#[test]
fn evaluate_if_then() {
assert_eq!(
evaluate("if true then 'true'"),
vec![Ok(Value::String("true".to_string()))]
);
}
#[test]
fn evaluate_if_then_else() {
assert_eq!(
evaluate("if false then 1 else 2"),
vec![Ok(Value::Integer(2))]
);
assert_eq!(
evaluate("if true then 1.0 else 42.0"),
vec![Ok(Value::Float(1.0))]
);
}
#[test]
fn evaluate_if_else_else_if_else() {
assert_eq!(
evaluate(
"
if false
then 'no'
else if 1 + 1 == 3
then 'nope'
else
'ok'
"
),
vec![Ok(Value::String("ok".to_string()))]
);
}
#[test]
fn evaluate_if_else_else_if_else_if_else_if_else() {
assert_eq!(
evaluate(
"
if false
then 'no'
else if 1 + 1 == 1
then 'nope'
else if 9 / 2 == 4
then 'nope'
else if 'foo' == 'bar'
then 'nope'
else 'ok'
"
),
vec![Ok(Value::String("ok".to_string()))]
);
}
#[test]
fn evaluate_function() {
let function = Function::new(
vec![Identifier::new("message".to_string())],
vec![Statement::Expression(Expression::Identifier(
Identifier::new("message".to_string()),
))],
);
assert_eq!(
evaluate("function <message> { message }"),
vec![Ok(Value::Function(function))]
);
}
#[test]
fn evaluate_function_call() {
let mut context = VariableMap::new();
let function = Function::new(
vec![Identifier::new("message".to_string())],
vec![Statement::Expression(Expression::Identifier(
Identifier::new("message".to_string()),
))],
);
context
.set_value("foobar".to_string(), Value::Function(function))
.unwrap();
assert_eq!(
evaluate("(foobar 'Hiya')"),
vec![Ok(Value::String("Hiya".to_string()))]
);
}
#[test]
fn evaluate_tool_call() {
assert_eq!(evaluate("(output 'Hiya')"), vec![Ok(Value::Empty)]);
}
}

View File

@ -1,720 +0,0 @@
//! The top level of Dust's API with functions to interpret Dust code.
//!
//! You can use this library externally by calling either of the "eval"
//! functions or by constructing your own Evaluator.
use std::fmt::{self, Debug, Formatter};
use serde::{Deserialize, Serialize};
use tree_sitter::{Node, Parser, Tree as TSTree};
use crate::{language, Error, Result, Value, VariableMap};
/// Evaluate the given source code.
///
/// Returns a vector of results from evaluating the source code. Each comment
/// and statemtent will have its own result.
///
/// # Examples
///
/// ```rust
/// # use dust::*;
/// assert_eq!(evaluate("1 + 2 + 3"), vec![Ok(Value::Integer(6))]);
/// ```
pub fn evaluate(source: &str) -> Vec<Result<Value>> {
let mut context = VariableMap::new();
evaluate_with_context(source, &mut context)
}
/// Evaluate the given source code with the given context.
///
/// # Examples
///
/// ```rust
/// # use dust::*;
/// let mut context = VariableMap::new();
///
/// context.set_value("one".into(), 1.into());
/// context.set_value("two".into(), 2.into());
/// context.set_value("three".into(), 3.into());
///
/// let dust_code = "four = 4 one + two + three + four";
///
/// assert_eq!(
/// evaluate_with_context(dust_code, &mut context),
/// vec![Ok(Value::Empty), Ok(Value::Integer(10))]
/// );
/// ```
pub fn evaluate_with_context(source: &str, context: &mut VariableMap) -> Vec<Result<Value>> {
let mut parser = Parser::new();
parser.set_language(language()).unwrap();
Evaluator::new(parser, context, source).run()
}
/// This trait is implemented by the Evaluator's internal types.
pub trait EvaluatorTree: Sized {
/// Interpret the syntax tree at the given node and return the abstraction.
///
/// This function is used to convert nodes in the Tree Sitter concrete
/// syntax tree into executable nodes in an abstract tree. This function is
/// where the tree should be traversed by accessing sibling and child nodes.
/// Each node in the CST should be traversed only once.
///
/// If necessary, the source code can be accessed directly by getting the
/// node's byte range.
fn from_syntax_node(node: Node, source: &str) -> Result<Self>;
/// Execute dust code by traversing the tree
fn run(&self, context: &mut VariableMap) -> Result<Value>;
}
/// A collection of statements and comments interpreted from a syntax tree.
///
/// The Evaluator turns a tree sitter concrete syntax tree into a vector of
/// abstract trees called [Item][]s that can be run to execute the source code.
pub struct Evaluator<'context, 'code> {
_parser: Parser,
context: &'context mut VariableMap,
source: &'code str,
tree: TSTree,
}
impl Debug for Evaluator<'_, '_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "Evaluator context: {}", self.context)
}
}
impl<'context, 'code> Evaluator<'context, 'code> {
fn new(mut parser: Parser, context: &'context mut VariableMap, source: &'code str) -> Self {
let tree = parser.parse(source, None).unwrap();
Evaluator {
_parser: parser,
context,
source,
tree,
}
}
fn run(self) -> Vec<Result<Value>> {
let mut cursor = self.tree.walk();
let root_node = cursor.node();
let item_count = root_node.child_count();
let mut results = Vec::with_capacity(item_count);
println!("{}", root_node.to_sexp());
for item_node in root_node.children(&mut cursor) {
let item_result = Item::from_syntax_node(item_node, self.source);
match item_result {
Ok(item) => {
let eval_result = item.run(self.context);
results.push(eval_result);
}
Err(error) => results.push(Err(error)),
}
}
results
}
}
/// An abstractiton of an independent unit of source code.
///
/// Items are either comments, which do nothing, or statements, which can be run
/// to produce a single value or interact with a context by creating or
/// referencing variables.
#[derive(Debug)]
pub enum Item {
Comment(String),
Statement(Statement),
}
impl EvaluatorTree for Item {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
let child = node.child(0).unwrap();
if child.kind() == "comment" {
let byte_range = child.byte_range();
let comment_text = &source[byte_range];
Ok(Item::Comment(comment_text.to_string()))
} else if child.kind() == "statement" {
Ok(Item::Statement(Statement::from_syntax_node(child, source)?))
} else {
Err(Error::UnexpectedSyntax {
expected: "comment or statement",
actual: child.kind(),
location: child.start_position(),
relevant_source: source[node.byte_range()].to_string(),
})
}
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
match self {
Item::Comment(text) => Ok(Value::String(text.clone())),
Item::Statement(statement) => statement.run(context),
}
}
}
/// Abstract representation of a statement.
///
/// Items are either comments, which do nothing, or statements, which can be run
/// to produce a single value or interact with their context.
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub enum Statement {
Assignment(Box<Assignment>),
Expression(Expression),
IfElse(Box<IfElse>),
Match(Match),
}
impl EvaluatorTree for Statement {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
debug_assert_eq!("statement", node.kind());
let child = node.child(0).unwrap();
match child.kind() {
"assignment" => Ok(Statement::Assignment(Box::new(
Assignment::from_syntax_node(child, source)?,
))),
"expression" => Ok(Self::Expression(Expression::from_syntax_node(
child, source,
)?)),
"if_else" => Ok(Statement::IfElse(Box::new(IfElse::from_syntax_node(
child, source,
)?))),
_ => Err(Error::UnexpectedSyntax {
expected: "assignment, expression or if...else",
actual: child.kind(),
location: child.start_position(),
relevant_source: source[node.byte_range()].to_string(),
}),
}
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
match self {
Statement::Assignment(assignment) => assignment.run(context),
Statement::Expression(expression) => expression.run(context),
Statement::IfElse(if_else) => if_else.run(context),
Statement::Match(r#match) => r#match.run(context),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub enum Expression {
Identifier(Identifier),
Value(Value),
Math(Box<Math>),
Logic(Box<Logic>),
FunctionCall(FunctionCall),
}
impl EvaluatorTree for Expression {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
let child = node.child(0).unwrap();
let expression = match child.kind() {
"value" => Expression::Value(Value::from_syntax_node(child, source)?),
"identifier" => Self::Identifier(Identifier::from_syntax_node(child, source)?),
"math" => Expression::Math(Box::new(Math::from_syntax_node(child, source)?)),
"logic" => Expression::Logic(Box::new(Logic::from_syntax_node(child, source)?)),
"function_call" => {
Expression::FunctionCall(FunctionCall::from_syntax_node(child, source)?)
}
_ => {
return Err(Error::UnexpectedSyntax {
expected: "value, identifier, math or function_call",
actual: child.kind(),
location: child.start_position(),
relevant_source: source[node.byte_range()].to_string(),
})
}
};
Ok(expression)
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
match self {
Expression::Value(value) => Ok(value.clone()),
Expression::Identifier(identifier) => identifier.run(context),
Expression::Math(math) => math.run(context),
Expression::FunctionCall(function_call) => function_call.run(context),
Expression::Logic(logic) => logic.run(context),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub struct Identifier(String);
impl Identifier {
pub fn new(inner: String) -> Self {
Identifier(inner)
}
pub fn take_inner(self) -> String {
self.0
}
pub fn inner(&self) -> &String {
&self.0
}
}
impl EvaluatorTree for Identifier {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
let identifier = &source[node.byte_range()];
Ok(Identifier(identifier.to_string()))
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
let value = context.get_value(&self.0)?.unwrap_or_default();
Ok(value)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub struct IfElse {
if_expression: Expression,
then_statement: Statement,
else_statement: Option<Statement>,
}
impl EvaluatorTree for IfElse {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
let if_node = node.child(1).unwrap();
let if_expression = Expression::from_syntax_node(if_node, source)?;
let then_node = node.child(3).unwrap();
let then_statement = Statement::from_syntax_node(then_node, source)?;
let else_node = node.child(5);
let else_statement = if let Some(node) = else_node {
Some(Statement::from_syntax_node(node, source)?)
} else {
None
};
println!("{if_node:?} {then_node:?} {else_node:?}");
Ok(IfElse {
if_expression,
then_statement,
else_statement,
})
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
let if_boolean = self.if_expression.run(context)?.as_boolean()?;
if if_boolean {
self.then_statement.run(context)
} else if let Some(statement) = &self.else_statement {
statement.run(context)
} else {
Ok(Value::Empty)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub struct Match {}
impl EvaluatorTree for Match {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
todo!()
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
todo!()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub struct Assignment {
identifier: Identifier,
statement: Statement,
}
impl EvaluatorTree for Assignment {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
let identifier_node = node.child(0).unwrap();
let identifier = Identifier::from_syntax_node(identifier_node, source)?;
let statement_node = node.child(2).unwrap();
let statement = Statement::from_syntax_node(statement_node, source)?;
Ok(Assignment {
identifier,
statement,
})
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
let key = self.identifier.clone().take_inner();
let value = self.statement.run(context)?;
context.set_value(key, value)?;
Ok(Value::Empty)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub struct Math {
left: Expression,
operator: MathOperator,
right: Expression,
}
impl EvaluatorTree for Math {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
let left_node = node.child(0).unwrap();
let left = Expression::from_syntax_node(left_node, source)?;
let operator_node = node.child(1).unwrap().child(0).unwrap();
let operator = match operator_node.kind() {
"+" => MathOperator::Add,
"-" => MathOperator::Subtract,
"*" => MathOperator::Multiply,
"/" => MathOperator::Divide,
"%" => MathOperator::Modulo,
_ => {
return Err(Error::UnexpectedSyntax {
expected: "+, -, *, / or %",
actual: operator_node.kind(),
location: operator_node.start_position(),
relevant_source: source[operator_node.byte_range()].to_string(),
})
}
};
let right_node = node.child(2).unwrap();
let right = Expression::from_syntax_node(right_node, source)?;
Ok(Math {
left,
operator,
right,
})
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
match self.operator {
MathOperator::Add | MathOperator::Subtract | MathOperator::Multiply => {
let left_value = self.left.run(context)?.as_int()?;
let right_value = self.right.run(context)?.as_int()?;
let outcome = match &self.operator {
MathOperator::Add => left_value + right_value,
MathOperator::Subtract => left_value - right_value,
MathOperator::Multiply => left_value * right_value,
_ => panic!("Unreachable"),
};
Ok(Value::Integer(outcome))
}
MathOperator::Divide | MathOperator::Modulo => {
let left_value = self.left.run(context)?.as_number()?;
let right_value = self.right.run(context)?.as_number()?;
let outcome = match self.operator {
MathOperator::Divide => left_value / right_value,
MathOperator::Modulo => left_value % right_value,
_ => panic!("Unreachable"),
};
Ok(Value::Float(outcome))
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub enum MathOperator {
Add,
Subtract,
Multiply,
Divide,
Modulo,
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub struct Logic {
left: Expression,
operator: LogicOperator,
right: Expression,
}
impl EvaluatorTree for Logic {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
let left_node = node.child(0).unwrap();
let left = Expression::from_syntax_node(left_node, source)?;
let operator_node = node.child(1).unwrap().child(0).unwrap();
let operator = match operator_node.kind() {
"==" => LogicOperator::Equal,
"&&" => LogicOperator::And,
"||" => LogicOperator::Or,
_ => {
return Err(Error::UnexpectedSyntax {
expected: "==, && or ||",
actual: operator_node.kind(),
location: operator_node.start_position(),
relevant_source: source[operator_node.byte_range()].to_string(),
})
}
};
let right_node = node.child(2).unwrap();
let right = Expression::from_syntax_node(right_node, source)?;
Ok(Logic {
left,
operator,
right,
})
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
let left_value = self.left.run(context)?;
let right_value = self.right.run(context)?;
let outcome = match self.operator {
LogicOperator::Equal => left_value == right_value,
LogicOperator::And => left_value.as_boolean()? && right_value.as_boolean()?,
LogicOperator::Or => left_value.as_boolean()? || right_value.as_boolean()?,
};
Ok(Value::Boolean(outcome))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub enum LogicOperator {
Equal,
And,
Or,
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub struct FunctionCall {
identifier: Identifier,
expressions: Vec<Expression>,
}
impl EvaluatorTree for FunctionCall {
fn from_syntax_node(node: Node, source: &str) -> Result<Self> {
let identifier_node = node.child(0).unwrap();
let identifier = Identifier::from_syntax_node(identifier_node, source)?;
let mut expressions = Vec::new();
todo!();
Ok(FunctionCall {
identifier,
expressions,
})
}
fn run(&self, context: &mut VariableMap) -> Result<Value> {
let mut arguments = Vec::with_capacity(self.expressions.len());
for expression in &self.expressions {
let value = expression.run(context)?;
arguments.push(value);
}
context.call_function(self.identifier.inner(), &Value::List(arguments))
}
}
#[cfg(test)]
mod tests {
use crate::{Function, Table};
use super::*;
#[test]
fn evaluate_empty() {
assert_eq!(evaluate("x = 9"), vec![Ok(Value::Empty)]);
assert_eq!(evaluate("x = 1 + 1"), vec![Ok(Value::Empty)]);
}
#[test]
fn evaluate_integer() {
assert_eq!(evaluate("1"), vec![Ok(Value::Integer(1))]);
assert_eq!(evaluate("123"), vec![Ok(Value::Integer(123))]);
assert_eq!(evaluate("-666"), vec![Ok(Value::Integer(-666))]);
}
#[test]
fn evaluate_float() {
assert_eq!(evaluate("0.1"), vec![Ok(Value::Float(0.1))]);
assert_eq!(evaluate("12.3"), vec![Ok(Value::Float(12.3))]);
assert_eq!(evaluate("-6.66"), vec![Ok(Value::Float(-6.66))]);
}
#[test]
fn evaluate_string() {
assert_eq!(
evaluate("\"one\""),
vec![Ok(Value::String("one".to_string()))]
);
assert_eq!(
evaluate("'one'"),
vec![Ok(Value::String("one".to_string()))]
);
assert_eq!(
evaluate("`one`"),
vec![Ok(Value::String("one".to_string()))]
);
assert_eq!(
evaluate("`'one'`"),
vec![Ok(Value::String("'one'".to_string()))]
);
assert_eq!(
evaluate("'`one`'"),
vec![Ok(Value::String("`one`".to_string()))]
);
assert_eq!(
evaluate("\"'one'\""),
vec![Ok(Value::String("'one'".to_string()))]
);
}
#[test]
fn evaluate_list() {
assert_eq!(
evaluate("[1, 2, 'foobar']"),
vec![Ok(Value::List(vec![
Value::Integer(1),
Value::Integer(2),
Value::String("foobar".to_string()),
]))]
);
}
#[test]
fn evaluate_map() {
let mut map = VariableMap::new();
map.set_value("x".to_string(), Value::Integer(1)).unwrap();
map.set_value("foo".to_string(), Value::String("bar".to_string()))
.unwrap();
assert_eq!(evaluate("{ x = 1 foo = 'bar' }"), vec![Ok(Value::Map(map))]);
}
#[test]
fn evaluate_table() {
let mut table = Table::new(vec!["messages".to_string(), "numbers".to_string()]);
table
.insert(vec![Value::String("hiya".to_string()), Value::Integer(42)])
.unwrap();
table
.insert(vec![Value::String("foo".to_string()), Value::Integer(57)])
.unwrap();
table
.insert(vec![Value::String("bar".to_string()), Value::Float(99.99)])
.unwrap();
assert_eq!(
evaluate(
"
table <messages, numbers> {
['hiya', 42]
['foo', 57]
['bar', 99.99]
}
"
),
vec![Ok(Value::Table(table))]
);
}
#[test]
fn evaluate_if_then() {
assert_eq!(
evaluate("if true then 'true'"),
vec![Ok(Value::String("true".to_string()))]
);
}
#[test]
fn evaluate_if_then_else() {
assert_eq!(
evaluate("if false then 1 else 2"),
vec![Ok(Value::Integer(2))]
);
assert_eq!(
evaluate("if true then 1.0 else 42.0"),
vec![Ok(Value::Float(1.0))]
);
}
#[test]
fn evaluate_if_else_else_if_else() {
assert_eq!(
evaluate(
"
if false
then 'no'
else if 1 + 1 == 3
then 'nope'
else
'ok'
"
),
vec![Ok(Value::String("ok".to_string()))]
);
}
#[test]
fn evaluate_if_else_else_if_else_if_else_if_else() {
assert_eq!(
evaluate(
"
if false
then 'no'
else if 1 + 1 == 1
then 'nope'
else if 9 / 2 == 4
then 'nope'
else if 'foo' == 'bar'
then 'nope'
else 'ok'
"
),
vec![Ok(Value::String("ok".to_string()))]
);
}
#[test]
fn evaluate_function() {
let function = Function::new(
vec![Identifier::new("output".to_string())],
vec![Statement::Expression(Expression::Identifier(
Identifier::new("output".to_string()),
))],
);
assert_eq!(
evaluate("function <output> { output }"),
vec![Ok(Value::Function(function))]
);
}
}

View File

@ -5,16 +5,18 @@
//! interpreting Dust code. Most of the language's features are implemented in the [tools] module. //! interpreting Dust code. Most of the language's features are implemented in the [tools] module.
pub use crate::{ pub use crate::{
abstract_tree::*,
error::*, error::*,
interface::*, evaluator::*,
value::{ value::{
function::Function, table::Table, time::Time, value_type::ValueType, function::Function, table::Table, time::Time, value_type::ValueType,
variable_map::VariableMap, Value, variable_map::VariableMap, Value,
}, },
}; };
mod abstract_tree;
mod error; mod error;
mod interface; mod evaluator;
mod value; mod value;
use tree_sitter::Language; use tree_sitter::Language;

View File

@ -1,416 +0,0 @@
//! Macros for collection values: strings, lists, maps and tables.
//!
//! Tests for this module are written in Dust and can be found at tests/collections.ds.
use crate::{Error, Result, Table, Tool, ToolInfo, Value, ValueType, VariableMap};
pub struct Sort;
impl Tool for Sort {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "sort",
description: "Apply default ordering to a list or table.",
group: "collections",
inputs: vec![ValueType::List, ValueType::Table],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
if let Ok(mut list) = argument.as_list().cloned() {
list.sort();
Ok(Value::List(list))
} else if let Ok(mut table) = argument.as_table().cloned() {
table.sort();
Ok(Value::Table(table))
} else {
Err(crate::Error::ExpectedList {
actual: argument.clone(),
})
}
}
}
pub struct Transform;
impl Tool for Transform {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "transform",
description: "Alter a list by calling a function on each value.",
group: "collections",
inputs: vec![ValueType::ListExact(vec![
ValueType::List,
ValueType::Function,
])],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = self.check_type(argument)?.as_list()?;
let list = argument[0].as_list()?;
let function = argument[1].as_function()?;
let mut mapped_list = Vec::with_capacity(list.len());
for value in list {
let mut context = VariableMap::new();
context.set_value("input".to_string(), value.clone())?;
let mapped_value = function.run_with_context(&mut context)?;
mapped_list.push(mapped_value);
}
Ok(Value::List(mapped_list))
}
}
pub struct String;
impl Tool for String {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "string",
description: "Stringify a value.",
group: "collections",
inputs: vec![
ValueType::String,
ValueType::Function,
ValueType::Float,
ValueType::Integer,
ValueType::Boolean,
],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = self.check_type(argument)?;
let string = match argument {
Value::String(string) => string.clone(),
Value::Function(function) => function.to_string(),
Value::Float(float) => float.to_string(),
Value::Integer(integer) => integer.to_string(),
Value::Boolean(boolean) => boolean.to_string(),
_ => return self.fail(argument),
};
Ok(Value::String(string))
}
}
pub struct Replace;
impl Tool for Replace {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "replace",
description: "Replace all occurences of a substring in a string.",
group: "collections",
inputs: vec![ValueType::ListExact(vec![
ValueType::String,
ValueType::String,
ValueType::String,
])],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = self.check_type(argument)?.as_list()?;
let target = argument[0].as_string()?;
let to_remove = argument[1].as_string()?;
let replacement = argument[2].as_string()?;
let result = target.replace(to_remove, replacement);
Ok(Value::String(result))
}
}
pub struct Count;
impl Tool for Count {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "count",
description: "Return the number of items in a collection.",
group: "collections",
inputs: vec![
ValueType::String,
ValueType::List,
ValueType::Map,
ValueType::Table,
],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = self.check_type(argument)?;
let len = match argument {
Value::String(string) => string.chars().count(),
Value::List(list) => list.len(),
Value::Map(map) => map.len(),
Value::Table(table) => table.len(),
Value::Function(_)
| Value::Float(_)
| Value::Integer(_)
| Value::Boolean(_)
| Value::Time(_)
| Value::Empty => return self.fail(argument),
};
Ok(Value::Integer(len as i64))
}
}
pub struct CreateTable;
impl Tool for CreateTable {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "create_table",
description: "Define a new table with a list of column names and list of rows.",
group: "collections",
inputs: vec![ValueType::ListExact(vec![
ValueType::ListOf(Box::new(ValueType::String)),
ValueType::ListOf(Box::new(ValueType::List)),
])],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = self.check_type(argument)?.as_list()?;
let column_name_inputs = argument[0].as_list()?;
let mut column_names = Vec::with_capacity(column_name_inputs.len());
for name in column_name_inputs {
column_names.push(name.as_string()?.clone());
}
let column_count = column_names.len();
let rows = argument[1].as_list()?;
let mut table = Table::new(column_names);
for row in rows {
let row = row.as_fixed_len_list(column_count)?;
table.insert(row.clone()).unwrap();
}
Ok(Value::Table(table))
}
}
pub struct Rows;
impl Tool for Rows {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "rows",
description: "Extract a table's rows as a list.",
group: "collections",
inputs: vec![ValueType::Table],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = self.check_type(argument)?;
if let Value::Table(table) = argument {
let rows = table
.rows()
.iter()
.map(|row| Value::List(row.clone()))
.collect();
Ok(Value::List(rows))
} else {
self.fail(argument)
}
}
}
pub struct Insert;
impl Tool for Insert {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "insert",
description: "Add new rows to a table.",
group: "collections",
inputs: vec![ValueType::ListExact(vec![
ValueType::Table,
ValueType::ListOf(Box::new(ValueType::List)),
])],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_list()?;
let new_rows = argument[1].as_list()?;
let mut table = argument[0].as_table()?.clone();
table.reserve(new_rows.len());
for row in new_rows {
let row = row.as_list()?.clone();
table.insert(row)?;
}
Ok(Value::Table(table))
}
}
pub struct Select;
impl Tool for Select {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "select",
description: "Extract one or more values based on their key.",
group: "collections",
inputs: vec![
ValueType::ListExact(vec![ValueType::Table, ValueType::String]),
ValueType::ListExact(vec![
ValueType::Table,
ValueType::ListOf(Box::new(ValueType::String)),
]),
],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let arguments = argument.as_fixed_len_list(2)?;
let collection = &arguments[0];
if let Value::List(list) = collection {
let mut selected = Vec::new();
let index = arguments[1].as_int()?;
let value = list.get(index as usize);
if let Some(value) = value {
selected.push(value.clone());
return Ok(Value::List(selected));
} else {
return Ok(Value::List(selected));
}
}
let mut column_names = Vec::new();
if let Value::List(columns) = &arguments[1] {
for column in columns {
let name = column.as_string()?;
column_names.push(name.clone());
}
} else if let Value::String(column) = &arguments[1] {
column_names.push(column.clone());
} else {
todo!()
};
if let Value::Map(map) = collection {
let mut selected = VariableMap::new();
for (key, value) in map.inner() {
if column_names.contains(key) {
selected.set_value(key.to_string(), value.clone())?;
}
}
return Ok(Value::Map(selected));
}
if let Value::Table(table) = collection {
let selected = table.select(&column_names);
return Ok(Value::Table(selected));
}
todo!()
}
}
pub struct ForEach;
impl Tool for ForEach {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "for_each",
description: "Run an operation on every item in a collection.",
group: "collections",
inputs: vec![],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_list()?;
Error::expected_minimum_function_argument_amount(
self.info().identifier,
2,
argument.len(),
)?;
let table = argument[0].as_table()?;
let columns = argument[1].as_list()?;
let mut column_names = Vec::new();
for column in columns {
let name = column.as_string()?;
column_names.push(name.clone());
}
let selected = table.select(&column_names);
Ok(Value::Table(selected))
}
}
pub struct Where;
impl Tool for Where {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "where",
description: "Keep rows matching a predicate.",
group: "collections",
inputs: vec![ValueType::ListExact(vec![
ValueType::Table,
ValueType::Function,
])],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = self.check_type(argument)?.as_list()?;
let table = &argument[0].as_table()?;
let function = argument[1].as_function()?;
let mut context = VariableMap::new();
let mut new_table = Table::new(table.column_names().clone());
for row in table.rows() {
for (column_index, cell) in row.iter().enumerate() {
let column_name = table.column_names().get(column_index).unwrap();
context.set_value(column_name.to_string(), cell.clone())?;
}
let keep_row = function.run_with_context(&mut context)?.as_boolean()?;
if keep_row {
new_table.insert(row.clone())?;
}
}
Ok(Value::Table(new_table))
}
}

View File

@ -1,119 +0,0 @@
use std::process::Command;
use crate::{Result, Tool, ToolInfo, Value, ValueType};
pub struct Sh;
impl Tool for Sh {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "sh",
description: "Pass input to the Bourne Shell.",
group: "command",
inputs: vec![ValueType::String],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_string()?;
Command::new("sh").arg("-c").arg(argument).spawn()?.wait()?;
Ok(Value::Empty)
}
}
pub struct Bash;
impl Tool for Bash {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "bash",
description: "Pass input to the Bourne Again Shell.",
group: "command",
inputs: vec![ValueType::String],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_string()?;
Command::new("bash")
.arg("-c")
.arg(argument)
.spawn()?
.wait()?;
Ok(Value::Empty)
}
}
pub struct Fish;
impl Tool for Fish {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "fish",
description: "Pass input to the fish shell.",
group: "command",
inputs: vec![ValueType::String],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_string()?;
Command::new("fish")
.arg("-c")
.arg(argument)
.spawn()?
.wait()?;
Ok(Value::Empty)
}
}
pub struct Zsh;
impl Tool for Zsh {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "zsh",
description: "Pass input to the Z shell.",
group: "command",
inputs: vec![ValueType::String],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_string()?;
Command::new("zsh")
.arg("-c")
.arg(argument)
.spawn()?
.wait()?;
Ok(Value::Empty)
}
}
pub struct Raw;
impl Tool for Raw {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "raw",
description: "Run input as a command without a shell",
group: "command",
inputs: vec![ValueType::String],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_string()?;
Command::new(argument).spawn()?.wait()?;
Ok(Value::Empty)
}
}

View File

@ -1,171 +0,0 @@
//! Convert values to and from data formats like JSON and TOML.
use crate::{Result, Table, Tool, ToolInfo, Value, ValueType};
pub struct FromToml;
impl Tool for FromToml {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "from_toml",
description: "Create a value from a TOML string.",
group: "data",
inputs: vec![ValueType::String],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_string()?;
let value = toml::from_str(&argument)?;
Ok(value)
}
}
pub struct FromJson;
impl Tool for FromJson {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "from_json",
description: "Get a whale value from a JSON string.",
group: "data",
inputs: vec![ValueType::String],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_string()?;
let value = serde_json::from_str(argument)?;
Ok(value)
}
}
pub struct ToJson;
impl Tool for ToJson {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "to_json",
description: "Create a JSON string from a whale value.",
group: "data",
inputs: vec![ValueType::Any],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let json = serde_json::to_string(argument)?;
Ok(Value::String(json))
}
}
pub struct FromCsv;
impl Tool for FromCsv {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "from_csv",
description: "Create a whale value from a CSV string.",
group: "data",
inputs: vec![ValueType::String],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let csv = argument.as_string()?;
let mut reader = csv::Reader::from_reader(csv.as_bytes());
let headers = reader
.headers()?
.iter()
.map(|header| header.trim().trim_matches('"').to_string())
.collect();
let mut table = Table::new(headers);
for result in reader.records() {
let row = result?
.iter()
.map(|column| {
let column = column.trim().trim_matches('"').trim_matches('\'');
if let Ok(integer) = column.parse::<i64>() {
Value::Integer(integer)
} else if let Ok(float) = column.parse::<f64>() {
Value::Float(float)
} else {
Value::String(column.to_string())
}
})
.collect();
table.insert(row)?;
}
Ok(Value::Table(table))
}
}
pub struct ToCsv;
impl Tool for ToCsv {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "to_csv",
description: "Convert a value to a string of comma-separated values.",
group: "data",
inputs: vec![ValueType::Any],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let mut buffer = Vec::new();
let mut writer = csv::Writer::from_writer(&mut buffer);
match argument {
Value::String(string) => {
writer.write_record([string])?;
}
Value::Float(float) => {
writer.write_record(&[float.to_string()])?;
}
Value::Integer(integer) => {
writer.write_record(&[integer.to_string()])?;
}
Value::Boolean(boolean) => {
writer.write_record(&[boolean.to_string()])?;
}
Value::List(list) => {
let string_list = list.iter().map(|value| value.to_string());
writer.write_record(string_list)?;
}
Value::Empty => {}
Value::Map(map) => {
writer.write_record(map.inner().keys())?;
writer.write_record(map.inner().values().map(|value| value.to_string()))?;
}
Value::Table(table) => {
writer.write_record(table.column_names())?;
for row in table.rows() {
let row_string = row.iter().map(|value| value.to_string());
writer.write_record(row_string)?;
}
}
Value::Function(_) => todo!(),
Value::Time(time) => {
writer.write_record(&[time.to_string()])?;
}
}
writer.flush()?;
Ok(Value::String(
String::from_utf8_lossy(writer.get_ref()).to_string(),
))
}
}

View File

@ -1,123 +0,0 @@
use std::process::Command;
use sysinfo::{DiskExt, System, SystemExt};
use crate::{Result, Table, Tool, ToolInfo, Value, ValueType};
pub struct ListDisks;
impl Tool for ListDisks {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "list_disks",
description: "List all block devices.",
group: "disks",
inputs: vec![ValueType::Empty],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
argument.as_empty()?;
let mut sys = System::new_all();
sys.refresh_all();
let mut disk_table = Table::new(vec![
"name".to_string(),
"kind".to_string(),
"file system".to_string(),
"mount point".to_string(),
"total space".to_string(),
"available space".to_string(),
"is removable".to_string(),
]);
for disk in sys.disks() {
let name = disk.name().to_string_lossy().to_string();
let kind = disk.kind();
let file_system = String::from_utf8_lossy(disk.file_system()).to_string();
let mount_point = disk.mount_point().to_str().unwrap().to_string();
let total_space = disk.total_space() as i64;
let available_space = disk.available_space() as i64;
let is_removable = disk.is_removable();
let row = vec![
Value::String(name),
Value::String(format!("{kind:?}")),
Value::String(file_system),
Value::String(mount_point),
Value::Integer(total_space),
Value::Integer(available_space),
Value::Boolean(is_removable),
];
disk_table.insert(row)?;
}
Ok(Value::Table(disk_table))
}
}
pub struct Partition;
impl Tool for Partition {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "partition",
description: "Partition a disk, clearing its content.",
group: "disks",
inputs: vec![ValueType::Map],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_map()?;
let path = argument
.get_value("path")?
.unwrap_or(Value::Empty)
.as_string()?
.clone();
let label = argument
.get_value("label")?
.unwrap_or(Value::Empty)
.as_string()?
.clone();
let name = argument
.get_value("name")?
.unwrap_or(Value::Empty)
.as_string()?
.clone();
let filesystem = argument
.get_value("filesystem")?
.unwrap_or(Value::Empty)
.as_string()?
.clone();
let range = argument
.get_value("range")?
.unwrap_or(Value::Empty)
.as_list()?
.clone();
if range.len() != 2 {
return Err(crate::Error::ExpectedFixedLenList {
expected_len: 2,
actual: Value::List(range),
});
}
let range_start = range[0].as_string()?;
let range_end = range[1].as_string()?;
let script = format!(
"sudo parted {path} mklabel {label} mkpart {name} {filesystem} {range_start} {range_end}"
);
Command::new("fish")
.arg("-c")
.arg(&script)
.spawn()?
.wait()?;
Ok(Value::Empty)
}
}

View File

@ -1,487 +0,0 @@
//! Dust commands for managing files and directories.
use std::{
fs::{self, OpenOptions},
io::{Read, Write as IoWrite},
path::PathBuf,
};
use crate::{Error, Result, Table, Time, Tool, ToolInfo, Value, ValueType};
pub struct Append;
impl Tool for Append {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "append",
description: "Append data to a file.",
group: "filesystem",
inputs: vec![ValueType::ListExact(vec![
ValueType::String,
ValueType::Any,
])],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let arguments = argument.as_fixed_len_list(2)?;
let path = arguments[0].as_string()?;
let content = arguments[1].as_string()?;
let mut file = OpenOptions::new().append(true).open(path)?;
file.write_all(content.as_bytes())?;
Ok(Value::Empty)
}
}
pub struct CreateDir;
impl Tool for CreateDir {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "create_dir",
description: "Create one or more directories.",
group: "filesystem",
inputs: vec![
ValueType::String,
ValueType::ListOf(Box::new(ValueType::String)),
],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let path = argument.as_string()?;
fs::create_dir_all(path)?;
Ok(Value::Empty)
}
}
pub struct FileMetadata;
impl Tool for FileMetadata {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "file_metadata",
description: "Get metadata for files.",
group: "filesystem",
inputs: vec![
ValueType::String,
ValueType::ListOf(Box::new(ValueType::String)),
],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let path_string = argument.as_string()?;
let metadata = PathBuf::from(path_string).metadata()?;
let created = metadata.accessed()?.elapsed()?.as_secs() / 60;
let accessed = metadata.accessed()?.elapsed()?.as_secs() / 60;
let modified = metadata.modified()?.elapsed()?.as_secs() / 60;
let read_only = metadata.permissions().readonly();
let size = metadata.len();
let mut file_table = Table::new(vec![
"path".to_string(),
"size".to_string(),
"created".to_string(),
"accessed".to_string(),
"modified".to_string(),
"read only".to_string(),
]);
file_table.insert(vec![
Value::String(path_string.clone()),
Value::Integer(size as i64),
Value::Integer(created as i64),
Value::Integer(accessed as i64),
Value::Integer(modified as i64),
Value::Boolean(read_only),
])?;
Ok(Value::Table(file_table))
}
}
pub struct ReadDir;
impl Tool for ReadDir {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "read_dir",
description: "Read the content of a directory.",
group: "filesystem",
inputs: vec![ValueType::String, ValueType::Empty],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let path = if let Ok(path) = argument.as_string() {
path
} else if argument.is_empty() {
"."
} else {
return Err(Error::TypeError {
expected: &[ValueType::Empty, ValueType::String],
actual: argument.clone(),
});
};
let dir = fs::read_dir(path)?;
let mut file_table = Table::new(vec![
"path".to_string(),
"size".to_string(),
"created".to_string(),
"accessed".to_string(),
"modified".to_string(),
"read only".to_string(),
]);
for entry in dir {
let entry = entry?;
let file_type = entry.file_type()?;
let file_name = if file_type.is_dir() {
let name = entry.file_name().into_string().unwrap_or_default();
format!("{name}/")
} else {
entry.file_name().into_string().unwrap_or_default()
};
let metadata = entry.path().metadata()?;
let created_timestamp = metadata.accessed()?;
let created = Time::from(created_timestamp);
let accessed_timestamp = metadata.accessed()?;
let accessed = Time::from(accessed_timestamp);
let modified_timestamp = metadata.modified()?;
let modified = Time::from(modified_timestamp);
let read_only = metadata.permissions().readonly();
let size = metadata.len();
file_table.insert(vec![
Value::String(file_name),
Value::Integer(size as i64),
Value::Time(created),
Value::Time(accessed),
Value::Time(modified),
Value::Boolean(read_only),
])?;
}
Ok(Value::Table(file_table))
}
}
pub struct ReadFile;
impl Tool for ReadFile {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "read_file",
description: "Read file contents.",
group: "filesystem",
inputs: vec![ValueType::String],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let path = argument.as_string()?;
let mut contents = String::new();
OpenOptions::new()
.read(true)
.create(false)
.open(path)?
.read_to_string(&mut contents)?;
Ok(Value::String(contents))
}
}
pub struct RemoveDir;
impl Tool for RemoveDir {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "remove_dir",
description: "Remove directories.",
group: "filesystem",
inputs: vec![
ValueType::String,
ValueType::ListOf(Box::new(ValueType::String)),
],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let path = argument.as_string()?;
fs::remove_dir(path)?;
Ok(Value::Empty)
}
}
pub struct MoveDir;
impl Tool for MoveDir {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "move_dir",
description: "Move a directory to a new path.",
group: "filesystem",
inputs: vec![ValueType::ListExact(vec![
ValueType::String,
ValueType::String,
])],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_list()?;
Error::expect_function_argument_amount(self.info().identifier, argument.len(), 2)?;
let current_path = argument[0].as_string()?;
let target_path = argument[1].as_string()?;
let file_list = ReadDir.run(&Value::String(current_path.clone()))?;
for path in file_list.as_list()? {
let path = PathBuf::from(path.as_string()?);
let new_path = PathBuf::from(&target_path).join(&path);
if path.is_file() {
fs::copy(&path, target_path)?;
}
if path.is_symlink() && path.symlink_metadata()?.is_file() {
fs::copy(&path, new_path)?;
}
}
Ok(Value::Empty)
}
}
pub struct Trash;
impl Tool for Trash {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "trash",
description: "Move a file or directory to the trash.",
group: "filesystem",
inputs: vec![
ValueType::String,
ValueType::ListOf(Box::new(ValueType::String)),
],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let path = argument.as_string()?;
trash::delete(path)?;
Ok(Value::Empty)
}
}
pub struct Write;
impl Tool for Write {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "write",
description: "Write data to a file.",
group: "filesystem",
inputs: vec![ValueType::ListExact(vec![
ValueType::String,
ValueType::Any,
])],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let strings = argument.as_list()?;
Error::expect_function_argument_amount(self.info().identifier, strings.len(), 2)?;
let path = strings.first().unwrap().as_string()?;
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)?;
for content in &strings[1..] {
let content = content.to_string();
file.write_all(content.as_bytes())?;
}
Ok(Value::Empty)
}
}
pub struct RemoveFile;
impl Tool for RemoveFile {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "remove_file",
description: "Permanently delete a file.",
group: "filesystem",
inputs: vec![
ValueType::String,
ValueType::ListOf(Box::new(ValueType::String)),
],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
if let Ok(path) = argument.as_string() {
fs::remove_file(path)?;
return Ok(Value::Empty);
}
if let Ok(path_list) = argument.as_list() {
for path in path_list {
let path = path.as_string()?;
fs::remove_file(path)?;
}
return Ok(Value::Empty);
}
Err(Error::expected_string(argument.clone()))
}
}
pub struct Watch;
impl Tool for Watch {
fn info(&self) -> crate::ToolInfo<'static> {
crate::ToolInfo {
identifier: "watch",
description: "Wait until a file changes.",
group: "filesystem",
inputs: vec![ValueType::String],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_string()?;
let path = PathBuf::from(argument);
let modified_old = path.metadata()?.modified()?;
let wait_time = loop {
let modified_new = path.metadata()?.modified()?;
if modified_old != modified_new {
break modified_new
.duration_since(modified_old)
.unwrap_or_default()
.as_millis() as i64;
}
};
Ok(Value::Integer(wait_time))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_dir() {
let path = PathBuf::from("./target/create_dir/");
let path_value = Value::String(path.to_string_lossy().to_string());
let _ = std::fs::remove_file(&path);
CreateDir.run(&path_value).unwrap();
assert!(path.is_dir());
}
#[test]
fn create_dir_nested() {
let path = PathBuf::from("./target/create_dir/nested");
let path_value = Value::String(path.to_string_lossy().to_string());
let _ = std::fs::remove_file(&path);
CreateDir.run(&path_value).unwrap();
assert!(path.is_dir());
}
#[test]
fn write() {
let path = PathBuf::from("./target/write.txt");
let path_value = Value::String(path.to_string_lossy().to_string());
let message = "hiya".to_string();
let message_value = Value::String(message.clone());
let _ = std::fs::remove_file(&path);
Write
.run(&Value::List(vec![path_value, message_value]))
.unwrap();
assert!(path.is_file());
}
#[test]
fn append() {
let path = PathBuf::from("./target/append.txt");
let path_value = Value::String(path.to_string_lossy().to_string());
let message = "hiya".to_string();
let message_value = Value::String(message.clone());
let _ = std::fs::remove_file(&path);
Write
.run(&Value::List(vec![
path_value.clone(),
message_value.clone(),
]))
.unwrap();
Append
.run(&Value::List(vec![path_value, message_value]))
.unwrap();
let read = fs::read_to_string(&path).unwrap();
assert_eq!("hiyahiya", read);
}
#[test]
fn read_file() {
let path = PathBuf::from("./target/read_file.txt");
let path_value = Value::String(path.to_string_lossy().to_string());
let message = "hiya".to_string();
let message_value = Value::String(message.clone());
let _ = std::fs::remove_file(&path);
Write
.run(&Value::List(vec![path_value.clone(), message_value]))
.unwrap();
let test = ReadFile.run(&path_value).unwrap();
let read = fs::read_to_string(&path).unwrap();
assert_eq!(test, Value::String(read));
}
#[test]
fn remove_file() {
let path = PathBuf::from("./target/remove_file.txt");
let path_value = Value::String(path.to_string_lossy().to_string());
let _ = std::fs::File::create(&path);
RemoveFile.run(&path_value).unwrap();
assert!(!path.exists());
}
}

View File

@ -1,160 +0,0 @@
use std::{thread::sleep, time::Duration};
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use crate::{Result, Table, Tool, ToolInfo, Value, ValueType, TOOL_LIST};
pub struct Help;
impl Tool for Help {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "help",
description: "Get help using dust.",
group: "general",
inputs: vec![ValueType::Empty, ValueType::String],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
self.check_type(argument)?;
let mut table = Table::new(vec![
"tool".to_string(),
"description".to_string(),
"group".to_string(),
"inputs".to_string(),
]);
for tool in TOOL_LIST {
let tool_group = tool.info().group.to_string();
if let Ok(group) = argument.as_string() {
if &tool_group != group {
continue;
}
}
let row = vec![
Value::String(tool.info().identifier.to_string()),
Value::String(tool.info().description.to_string()),
Value::String(tool_group),
Value::List(
tool.info()
.inputs
.iter()
.map(|value_type| Value::String(value_type.to_string()))
.collect(),
),
];
table.insert(row)?;
}
Ok(Value::Table(table))
}
}
pub struct Output;
impl Tool for Output {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "output",
description: "Print a value.",
group: "general",
inputs: vec![ValueType::Any],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
println!("{argument}");
Ok(Value::Empty)
}
}
pub struct Repeat;
impl Tool for Repeat {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "repeat",
description: "Run a function the given number of times.",
group: "general",
inputs: vec![ValueType::ListExact(vec![
ValueType::Function,
ValueType::Integer,
])],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_list()?;
let function = argument[0].as_function()?;
let count = argument[1].as_int()?;
let mut result_list = Vec::with_capacity(count as usize);
for _ in 0..count {
let result = function.run()?;
result_list.push(result);
}
Ok(Value::List(result_list))
}
}
pub struct Run;
impl Tool for Run {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "run",
description: "Run functions in parallel.",
group: "general",
inputs: vec![ValueType::ListOf(Box::new(ValueType::Function))],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument_list = argument.as_list()?;
let results = argument_list
.par_iter()
.map(|value| {
let function = if let Ok(function) = value.as_function() {
function
} else {
return value.clone();
};
match function.run() {
Ok(value) => value,
Err(error) => Value::String(error.to_string()),
}
})
.collect();
Ok(Value::List(results))
}
}
pub struct Wait;
impl Tool for Wait {
fn info(&self) -> crate::ToolInfo<'static> {
ToolInfo {
identifier: "wait",
description: "Wait for the given number of milliseconds.",
group: "general",
inputs: vec![ValueType::Integer],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_int()?;
sleep(Duration::from_millis(argument as u64));
Ok(Value::Empty)
}
}

View File

@ -1,324 +0,0 @@
use eframe::{
egui::{
plot::{Bar, BarChart, Line, Plot as EguiPlot, PlotPoints},
CentralPanel, Context, Direction, Layout, RichText, ScrollArea, Ui,
},
emath::Align,
epaint::{Color32, Stroke},
run_native, NativeOptions,
};
use egui_extras::{Column, StripBuilder, TableBuilder};
use crate::{Error, Result, Table, Tool, ToolInfo, Value, ValueType, VariableMap};
pub struct BarGraph;
impl Tool for BarGraph {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "bar_graph",
description: "Render a list of values as a bar graph.",
group: "gui",
inputs: vec![ValueType::ListOf(Box::new(ValueType::List))],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_list()?;
let mut bars = Vec::new();
for (index, value) in argument.iter().enumerate() {
let list = value.as_list()?;
let mut name = None;
let mut height = None;
for value in list {
match value {
Value::Float(float) => {
if height.is_none() {
height = Some(float);
}
}
Value::Integer(_integer) => {}
Value::String(string) => name = Some(string),
Value::Boolean(_)
| Value::List(_)
| Value::Map(_)
| Value::Table(_)
| Value::Time(_)
| Value::Function(_)
| Value::Empty => continue,
}
}
let height =
match height {
Some(height) => *height,
None => return Err(Error::CustomMessage(
"Could not create bar graph. No float value was found to use as a height."
.to_string(),
)),
};
let bar = Bar::new(index as f64, height).name(name.unwrap_or(&"".to_string()));
bars.push(bar);
}
run_native(
"bar_graph",
NativeOptions::default(),
Box::new(|_cc| Box::new(BarGraphGui::new(bars))),
)
.unwrap();
Ok(Value::Empty)
}
}
struct BarGraphGui {
bars: Vec<Bar>,
}
impl BarGraphGui {
fn new(data: Vec<Bar>) -> Self {
Self { bars: data }
}
}
impl eframe::App for BarGraphGui {
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
CentralPanel::default().show(ctx, |ui| {
EguiPlot::new("bar_graph").show(ui, |plot_ui| {
plot_ui.bar_chart(BarChart::new(self.bars.clone()).color(Color32::RED));
});
});
}
}
pub struct Plot;
impl Tool for Plot {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "plot",
description: "Render a list of numbers as a scatter plot graph.",
group: "gui",
inputs: vec![
ValueType::ListOf(Box::new(ValueType::Float)),
ValueType::ListOf(Box::new(ValueType::Integer)),
],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_list()?;
let mut floats = Vec::new();
for value in argument {
if let Ok(float) = value.as_float() {
floats.push(float);
} else if let Ok(integer) = value.as_int() {
floats.push(integer as f64);
} else {
return Err(Error::expected_number(value.clone()));
}
}
run_native(
"plot",
NativeOptions {
resizable: true,
centered: true,
..Default::default()
},
Box::new(|_cc| Box::new(PlotGui::new(floats))),
)
.unwrap();
Ok(Value::Empty)
}
}
struct PlotGui {
data: Vec<f64>,
}
impl PlotGui {
fn new(data: Vec<f64>) -> Self {
Self { data }
}
}
impl eframe::App for PlotGui {
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
CentralPanel::default().show(ctx, |ui| {
EguiPlot::new("plot").show(ui, |plot_ui| {
let points = self
.data
.iter()
.enumerate()
.map(|(index, value)| [index as f64, *value])
.collect::<PlotPoints>();
let line = Line::new(points);
plot_ui.line(line);
})
});
}
}
pub struct GuiApp {
text_edit_buffer: String,
_whale_context: VariableMap,
eval_result: Result<Value>,
}
impl GuiApp {
pub fn new(result: Result<Value>) -> Self {
GuiApp {
text_edit_buffer: String::new(),
_whale_context: VariableMap::new(),
eval_result: result,
}
}
fn _table_ui(&mut self, table: &Table, ui: &mut Ui) {
TableBuilder::new(ui)
.resizable(true)
.striped(true)
.columns(Column::remainder(), table.column_names().len())
.header(30.0, |mut row| {
for name in table.column_names() {
row.col(|ui| {
ui.label(name);
});
}
})
.body(|body| {
body.rows(20.0, table.rows().len(), |index, mut row| {
let row_data = table.rows().get(index).unwrap();
for cell_data in row_data {
row.col(|ui| {
ui.label(cell_data.to_string());
});
}
});
});
}
}
impl eframe::App for GuiApp {
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
CentralPanel::default().show(ctx, |ui| {
ui.with_layout(
Layout {
main_dir: Direction::TopDown,
main_wrap: false,
main_align: Align::Center,
main_justify: false,
cross_align: Align::Center,
cross_justify: true,
},
|ui| {
ui.text_edit_multiline(&mut self.text_edit_buffer);
ui.horizontal(|ui| {
let clear = ui.button("clear");
let submit = ui.button("submit");
if clear.clicked() {
self.text_edit_buffer.clear();
}
if submit.clicked() {
todo!()
}
});
},
);
ui.separator();
StripBuilder::new(ui)
.sizes(egui_extras::Size::remainder(), 1)
.vertical(|mut strip| {
strip.cell(|ui| {
let rectangle = ui.available_rect_before_wrap();
let corner = 5.0;
let border = Stroke::new(1.0, Color32::DARK_GREEN);
let item_size = 20.0;
ui.painter().rect_stroke(rectangle, corner, border);
match &self.eval_result {
Ok(value) => match value {
Value::String(string) => {
ui.label(RichText::new(string).size(item_size));
}
Value::Float(float) => {
ui.label(RichText::new(float.to_string()).size(item_size));
}
Value::Integer(integer) => {
ui.label(RichText::new(integer.to_string()).size(item_size));
}
Value::Boolean(boolean) => {
ui.label(RichText::new(boolean.to_string()).size(item_size));
}
Value::List(list) => {
for value in list {
ui.label(RichText::new(value.to_string()).size(item_size));
}
}
Value::Map(_) => todo!(),
Value::Table(table) => {
ScrollArea::both().show(ui, |ui| {
TableBuilder::new(ui)
.resizable(true)
.striped(true)
.vscroll(true)
.columns(
Column::remainder(),
table.column_names().len(),
)
.header(20.0, |mut row| {
for name in table.column_names() {
row.col(|ui| {
ui.label(name);
});
}
})
.body(|body| {
body.rows(
20.0,
table.rows().len(),
|index, mut row| {
let row_data =
table.rows().get(index).unwrap();
for value in row_data {
row.col(|ui| {
ui.label(value.to_string());
});
}
},
);
});
});
}
Value::Function(_) => todo!(),
Value::Empty => {}
Value::Time(_) => todo!(),
},
Err(error) => {
let rectangle = ui.available_rect_before_wrap();
let corner = 5.0;
let border = Stroke::new(1.0, Color32::DARK_RED);
ui.painter().rect_stroke(rectangle, corner, border);
ui.label(error.to_string());
}
}
});
});
});
}
}

View File

@ -1,162 +0,0 @@
use crate::{Error, Tool, ToolInfo, Result, Value, ValueType };
pub struct Assert;
impl Tool for Assert {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "assert",
description: "Panic if a boolean is false.",
group: "test",
inputs: vec![
ValueType::Boolean,
ValueType::Function
],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let boolean = argument.as_boolean()?;
if boolean {
Ok(Value::Empty)
} else {
Err(Error::AssertFailed)
}
}
}
pub struct AssertEqual;
impl Tool for AssertEqual {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "assert_equal",
description: "Panic if two values do not match.",
group: "test",
inputs: vec![ValueType::ListExact(vec![ValueType::Any, ValueType::Any])],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let arguments = argument.as_fixed_len_list(2)?;
if arguments[0] == arguments[1] {
Ok(Value::Empty)
} else {
Err(Error::AssertEqualFailed {
expected: arguments[0].clone(),
actual: arguments[1].clone()
})
}
}
}
pub struct If;
impl Tool for If {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "if",
description: "Evaluates the first argument. If true, it does the second argument.",
group: "logic",
inputs: vec![
ValueType::ListExact(vec![
ValueType::Boolean,
ValueType::Any,
]),
ValueType::ListExact(vec![
ValueType::Function,
ValueType::Any,
])],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_fixed_len_list(2)?;
let (condition, if_true) = (&argument[0], &argument[1]);
let condition = if let Ok(function) = condition.as_function() {
function.run()?.as_boolean()?
} else {
condition.as_boolean()?
};
if condition {
Ok(if_true.clone())
} else {
Ok(Value::Empty)
}
}
}
pub struct IfElse;
impl Tool for IfElse {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "if_else",
description: "Evaluates the first argument. If true, it does the second argument. If false, it does the third argument",
group: "logic",
inputs: vec![
ValueType::ListExact(vec![
ValueType::Boolean,
ValueType::Any,
ValueType::Any,
]),
ValueType::ListExact(vec![
ValueType::Function,
ValueType::Any,
ValueType::Any,
])],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_fixed_len_list(3)?;
let (condition, if_true, if_false) = (&argument[0], &argument[1], &argument[2]);
let condition_is_true = if let Ok(boolean) = condition.as_boolean() {
boolean
} else if let Ok(function) = condition.as_function() {
function.run()?.as_boolean()?
} else {
return Err(Error::TypeError {
expected: &[ValueType::Boolean, ValueType::Function],
actual: condition.clone(),
});
};
let should_yield = if condition_is_true { if_true } else { if_false };
if let Ok(function) = should_yield.as_function() {
function.run()
} else {
Ok(should_yield.clone())
}
}
}
pub struct Loop;
impl Tool for Loop {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "loop",
description: "Repeats a function until the program ends.",
group: "general",
inputs: vec![ValueType::Function],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let function = argument.as_function()?;
function.run()?;
Loop.run(argument)
}
}

View File

@ -1,377 +0,0 @@
//! Dust's built-in commands.
//!
//! When a tool in invoked in Dust, the input is checked against the inputs listed in its ToolInfo.
//! The input should then be double-checked by `Tool::check_input` when you implement `run`. The
//! purpose of the second check is to weed out mistakes in how the inputs were described in the
//! ToolInfo. The errors from the second check should only come up during development and should not //! be seen by the user.
//!
//! ## Writing macros
//!
//! - Snake case identifier, this is enforced by a test
//! - The description should be brief, it will display in the shell
//! - Recycle code that is already written and tested
//! - Write non-trivial tests, do not write tests just for the sake of writing them
//!
//! ## Usage
//!
//! Commands can be used in Rust by passing a Value to the run method.
//!
//! ```
//! # use dust_lib::{tools::collections::Count, Tool, Value};
//! let value = Value::List(vec![
//! Value::Integer(1),
//! Value::Integer(2),
//! Value::Integer(3),
//! ]);
//! let count = Count
//! .run(&value)
//! .unwrap()
//! .as_int()
//! .unwrap();
//!
//! assert_eq!(count, 3);
//! ```
use crate::{Result, Value, ValueType};
pub mod collections;
pub mod command;
pub mod data_formats;
pub mod disks;
pub mod filesystem;
pub mod general;
pub mod gui;
pub mod logic;
pub mod network;
pub mod random;
pub mod system;
pub mod time;
/// Master list of all tools.
///
/// This list is used to match identifiers with tools and to provide info to the shell.
pub const TOOL_LIST: [&'static dyn Tool; 52] = [
&collections::Count,
&collections::CreateTable,
&collections::Insert,
&collections::Rows,
&collections::Select,
&collections::String,
&collections::Sort,
&collections::Replace,
&collections::Transform,
&collections::Where,
&command::Bash,
&command::Fish,
&command::Raw,
&command::Sh,
&command::Zsh,
&data_formats::FromCsv,
&data_formats::ToCsv,
&data_formats::FromJson,
&data_formats::ToJson,
&disks::ListDisks,
&disks::Partition,
&filesystem::Append,
&filesystem::CreateDir,
&filesystem::FileMetadata,
&filesystem::MoveDir,
&filesystem::ReadDir,
&filesystem::ReadFile,
&filesystem::RemoveDir,
&filesystem::Trash,
&filesystem::Watch,
&filesystem::Write,
&general::Help,
&general::Run,
&general::Output,
&general::Repeat,
&general::Wait,
&gui::BarGraph,
&gui::Plot,
&logic::If,
&logic::IfElse,
&logic::Loop,
&network::Download,
&random::Random,
&random::RandomBoolean,
&random::RandomFloat,
&random::RandomInteger,
&random::RandomString,
&system::Users,
&logic::Assert,
&logic::AssertEqual,
&time::Local,
&time::Now,
];
/// A whale macro function.
pub trait Tool: Sync + Send {
fn info(&self) -> ToolInfo<'static>;
fn run(&self, argument: &Value) -> Result<Value>;
fn check_type<'a>(&self, argument: &'a Value) -> Result<&'a Value> {
if self
.info()
.inputs
.iter()
.any(|value_type| &argument.value_type() == value_type)
{
Ok(argument)
} else {
Err(crate::Error::TypeCheckFailure {
tool_info: self.info(),
argument: argument.clone(),
})
}
}
fn fail(&self, argument: &Value) -> Result<Value> {
Err(crate::Error::TypeCheckFailure {
tool_info: self.info(),
argument: argument.clone(),
})
}
}
/// Information needed for each macro.
#[derive(Clone, Debug, PartialEq)]
pub struct ToolInfo<'a> {
/// Text pattern that triggers this macro.
pub identifier: &'a str,
/// User-facing information about how the macro works.
pub description: &'a str,
/// Category used to sort macros in the shell.
pub group: &'a str,
pub inputs: Vec<ValueType>,
}
// pub struct SystemInfo;
// impl Macro for SystemInfo {
// fn info(&self) -> MacroInfo<'static> {
// MacroInfo {
// identifier: "system_info",
// description: "Get information on the system.",
// }
// }
// fn run(&self, argument: &Value) -> crate::Result<Value> {
// argument.as_empty()?;
// let mut map = VariableMap::new();
// map.set_value("hostname", Value::String(hostname()?))?;
// Ok(Value::Map(map))
// }
// }
// pub struct Map;
// impl Macro for Map {
// fn info(&self) -> MacroInfo<'static> {
// MacroInfo {
// identifier: "map",
// description: "Create a map from a value.",
// }
// }
// fn run(&self, argument: &Value) -> Result<Value> {
// match argument {
// Value::String(_) => todo!(),
// Value::Float(_) => todo!(),
// Value::Integer(_) => todo!(),
// Value::Boolean(_) => todo!(),
// Value::List(_) => todo!(),
// Value::Map(_) => todo!(),
// Value::Table(table) => Ok(Value::Map(VariableMap::from(table))),
// Value::Function(_) => todo!(),
// Value::Empty => todo!(),
// }
// }
// }
// pub struct Status;
// impl Macro for Status {
// fn info(&self) -> MacroInfo<'static> {
// MacroInfo {
// identifier: "git_status",
// description: "Get the repository status for the current directory.",
// }
// }
// fn run(&self, argument: &Value) -> Result<Value> {
// argument.as_empty()?;
// let repo = Repository::open(".")?;
// let mut table = Table::new(vec![
// "path".to_string(),
// "status".to_string(),
// "staged".to_string(),
// ]);
// for entry in repo.statuses(None)?.into_iter() {
// let (status, staged) = {
// if entry.status().is_wt_new() {
// ("created".to_string(), false)
// } else if entry.status().is_wt_deleted() {
// ("deleted".to_string(), false)
// } else if entry.status().is_wt_modified() {
// ("modified".to_string(), false)
// } else if entry.status().is_index_new() {
// ("created".to_string(), true)
// } else if entry.status().is_index_deleted() {
// ("deleted".to_string(), true)
// } else if entry.status().is_index_modified() {
// ("modified".to_string(), true)
// } else if entry.status().is_ignored() {
// continue;
// } else {
// ("".to_string(), false)
// }
// };
// let path = entry.path().unwrap().to_string();
// table.insert(vec![
// Value::String(path),
// Value::String(status),
// Value::Boolean(staged),
// ])?;
// }
// Ok(Value::Table(table))
// }
// }
// pub struct DocumentConvert;
// impl Macro for DocumentConvert {
// fn info(&self) -> MacroInfo<'static> {
// MacroInfo {
// identifier: "convert_document",
// description: "Convert a file's contents to a format and set the extension.",
// }
// }
// fn run(&self, argument: &Value) -> Result<Value> {
// let argument = argument.as_list()?;
// if argument.len() != 3 {
// return Err(Error::WrongFunctionArgumentAmount {
// expected: 3,
// actual: argument.len(),
// });
// }
// let (path, from, to) = (
// argument[0].as_string()?,
// argument[1].as_string()?,
// argument[2].as_string()?,
// );
// let mut file_name = PathBuf::from(&path);
// file_name.set_extension(to);
// let new_file_name = file_name.to_str().unwrap();
// let script = format!("pandoc --from {from} --to {to} --output {new_file_name} {path}");
// Command::new("fish").arg("-c").arg(script).spawn()?.wait()?;
// Ok(Value::Empty)
// }
// }
// pub struct Trash;
// impl Macro for Trash {
// fn info(&self) -> MacroInfo<'static> {
// MacroInfo {
// identifier: "trash_dir",
// description: "Move a directory to the trash.",
// }
// }
// fn run(&self, argument: &Value) -> Result<Value> {
// let path = argument.as_string()?;
// trash::delete(path)?;
// Ok(Value::Empty)
// }
// }
// pub struct Get;
// impl Macro for Get {
// fn info(&self) -> MacroInfo<'static> {
// MacroInfo {
// identifier: "get",
// description: "Extract a value from a collection.",
// }
// }
// fn run(&self, argument: &Value) -> Result<Value> {
// let argument_list = argument.as_list()?;
// let collection = &argument_list[0];
// let index = &argument_list[1];
// if let Ok(list) = collection.as_list() {
// let index = index.as_int()?;
// let value = list.get(index as usize).unwrap_or(&Value::Empty);
// return Ok(value.clone());
// }
// if let Ok(table) = collection.as_table() {
// let index = index.as_int()?;
// let get_row = table.get(index as usize);
// if let Some(row) = get_row {
// return Ok(Value::List(row.clone()));
// }
// }
// Err(Error::TypeError {
// expected: &[
// ValueType::List,
// ValueType::Map,
// ValueType::Table,
// ValueType::String,
// ],
// actual: collection.clone(),
// })
// }
// }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tool_identifier_formatting() {
for function in TOOL_LIST {
let identifier = function.info().identifier;
assert_eq!(identifier.to_lowercase(), identifier);
assert!(identifier.is_ascii());
assert!(!identifier.is_empty());
assert!(!identifier.contains(' '));
assert!(!identifier.contains(':'));
assert!(!identifier.contains('.'));
assert!(!identifier.contains('-'));
}
}
#[test]
fn tool_inputs_exist() {
for function in TOOL_LIST {
let identifier = function.info().identifier;
let input_count = function.info().inputs.len();
assert!(input_count > 0, "{} has no inputs declared", identifier);
}
}
}

View File

@ -1,23 +0,0 @@
//! Macros for network access.
use crate::{Result, Tool, ToolInfo, Value, ValueType};
pub struct Download;
impl Tool for Download {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "download",
description: "Fetch a network resource.",
group: "network",
inputs: vec![ValueType::String],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = argument.as_string()?;
let output = reqwest::blocking::get(argument)?.text()?;
Ok(Value::String(output))
}
}

View File

@ -1,161 +0,0 @@
use std::convert::TryInto;
use rand::{random, thread_rng, Rng};
use crate::{Error, Result, Tool, ToolInfo, Value, ValueType};
pub struct RandomBoolean;
impl Tool for RandomBoolean {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "random_boolean",
description: "Create a random boolean.",
group: "random",
inputs: vec![ValueType::Empty],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = self.check_type(argument)?;
if let Value::Empty = argument {
let boolean = rand::thread_rng().gen();
Ok(Value::Boolean(boolean))
} else {
self.fail(argument)
}
}
}
pub struct RandomInteger;
impl Tool for RandomInteger {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "random_integer",
description: "Create a random integer.",
group: "random",
inputs: vec![
ValueType::Empty,
ValueType::Integer,
ValueType::ListExact(vec![ValueType::Integer, ValueType::Integer]),
],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
match argument {
Value::Integer(max) => {
let integer = rand::thread_rng().gen_range(0..*max);
Ok(Value::Integer(integer))
}
Value::List(min_max) => {
Error::expect_function_argument_amount(self.info().identifier, min_max.len(), 2)?;
let min = min_max[0].as_int()?;
let max = min_max[1].as_int()? + 1;
let integer = rand::thread_rng().gen_range(min..max);
Ok(Value::Integer(integer))
}
Value::Empty => Ok(crate::Value::Integer(random())),
_ => self.fail(argument),
}
}
}
pub struct RandomString;
impl Tool for RandomString {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "random_string",
description: "Generate a random string.",
group: "random",
inputs: vec![ValueType::Empty, ValueType::Integer],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = self.check_type(argument)?;
if let Value::Integer(length) = argument {
let length: usize = length.unsigned_abs().try_into().unwrap_or(0);
let mut random = String::with_capacity(length);
for _ in 0..length {
let random_char = thread_rng().gen_range('A'..='z').to_string();
random.push_str(&random_char);
}
return Ok(Value::String(random));
}
if let Value::Empty = argument {
let mut random = String::with_capacity(10);
for _ in 0..10 {
let random_char = thread_rng().gen_range('A'..='z').to_string();
random.push_str(&random_char);
}
return Ok(Value::String(random));
}
self.fail(argument)
}
}
pub struct RandomFloat;
impl Tool for RandomFloat {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "random_float",
description: "Generate a random floating point value between 0 and 1.",
group: "random",
inputs: vec![ValueType::Empty],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = self.check_type(argument)?;
if argument.is_empty() {
Ok(Value::Float(random()))
} else {
self.fail(argument)
}
}
}
pub struct Random;
impl Tool for Random {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "random",
description: "Select a random item from a list.",
group: "random",
inputs: vec![ValueType::List],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
let argument = self.check_type(argument)?;
if let Value::List(list) = argument {
let random_index = thread_rng().gen_range(0..list.len());
let random_item = list.get(random_index).unwrap();
Ok(random_item.clone())
} else {
self.fail(argument)
}
}
}

View File

@ -1,28 +0,0 @@
use sysinfo::{RefreshKind, System, SystemExt, UserExt};
use crate::{Result, Tool, ToolInfo, Value, ValueType};
pub struct Users;
impl Tool for Users {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "users",
description: "Get a list of the system's users.",
group: "system",
inputs: vec![ValueType::Empty],
}
}
fn run(&self, argument: &Value) -> Result<Value> {
argument.as_empty()?;
let users = System::new_with_specifics(RefreshKind::new().with_users_list())
.users()
.iter()
.map(|user| Value::String(user.name().to_string()))
.collect();
Ok(Value::List(users))
}
}

View File

@ -1,43 +0,0 @@
use std::time::Instant;
use crate::{Result, Time, Tool, ToolInfo, Value, ValueType};
pub struct Now;
impl Tool for Now {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "now",
description: "Return the current time.",
group: "time",
inputs: vec![ValueType::Empty],
}
}
fn run(&self, argument: &crate::Value) -> Result<Value> {
argument.as_empty()?;
let time = Time::monotonic(Instant::now());
Ok(Value::Time(time))
}
}
pub struct Local;
impl Tool for Local {
fn info(&self) -> ToolInfo<'static> {
ToolInfo {
identifier: "local",
description: "Show a time value adjusted for the current time zone.",
group: "time",
inputs: vec![ValueType::Time],
}
}
fn run(&self, argument: &crate::Value) -> Result<Value> {
let argument = argument.as_time()?;
Ok(Value::String(argument.as_local()))
}
}

View File

@ -2,7 +2,7 @@ use std::fmt::{self, Display, Formatter};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{EvaluatorTree, Identifier, Result, Statement, Value, VariableMap}; use crate::{Identifier, Statement};
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)] #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, PartialOrd, Ord)]
pub struct Function { pub struct Function {
@ -23,7 +23,7 @@ impl Display for Function {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!( write!(
f, f,
"function < {:?} > {{ {:?} }}", "function < {:?} > {{ {:?} }}", // TODO: Correct this output
self.identifiers, self.statements self.identifiers, self.statements
) )
} }

View File

@ -1,7 +1,7 @@
//! Types that represent runtime values. //! Types that represent runtime values.
use crate::{ use crate::{
error::{Error, Result}, error::{Error, Result},
EvaluatorTree, Function, Identifier, Statement, Table, Time, ValueType, VariableMap, AbstractTree, Function, Identifier, Statement, Table, Time, ValueType, VariableMap,
}; };
use json::JsonValue; use json::JsonValue;

View File

@ -4,7 +4,7 @@ use std::{
fmt::{self, Display, Formatter}, fmt::{self, Display, Formatter},
}; };
use crate::{value::Value, Error, Result, Table}; use crate::{value::Value, AbstractTree, Error, Result, Table};
/// A collection dust variables comprised of key-value pairs. /// A collection dust variables comprised of key-value pairs.
/// ///
@ -23,42 +23,6 @@ impl VariableMap {
} }
} }
/// Invokes built-in tools or user-defined functions based on the identifier and passes the
/// argument. Returns an error a tool is called with the wrong inputs or if the identifier does
/// not match any tools or functions.
pub fn call_function(&self, identifier: &str, argument: &Value) -> Result<Value> {
// for macro_item in TOOL_LIST {
// let valid_input_types = macro_item.info().inputs;
// if identifier == macro_item.info().identifier {
// let input_type = argument.value_type();
// if valid_input_types.contains(&input_type) {
// return macro_item.run(argument);
// } else {
// return Err(Error::MacroArgumentType {
// macro_info: macro_item.info(),
// actual: argument.clone(),
// });
// }
// }
// }
// for (key, value) in &self.variables {
// if identifier == key {
// if let Ok(function) = value.as_function() {
// let mut context = self.clone();
// context.set_value("input".to_string(), argument.clone())?;
// return function.run_with_context(&mut context);
// }
// }
// }
Err(Error::FunctionIdentifierNotFound(identifier.to_string()))
}
/// Returns a Value assigned to the identifer, allowing dot notation to retrieve Values that are /// nested in Lists or Maps. Returns None if there is no variable with a key matching the /// identifier. Returns an error if a Map or List is indexed incorrectly. /// Returns a Value assigned to the identifer, allowing dot notation to retrieve Values that are /// nested in Lists or Maps. Returns None if there is no variable with a key matching the /// identifier. Returns an error if a Map or List is indexed incorrectly.
pub fn get_value(&self, identifier: &str) -> Result<Option<Value>> { pub fn get_value(&self, identifier: &str) -> Result<Option<Value>> {
let split = identifier.rsplit_once('.'); let split = identifier.rsplit_once('.');

View File

@ -6,7 +6,7 @@ use dust::*;
fn collections() { fn collections() {
let file_contents = read_to_string("examples/collections.ds").unwrap(); let file_contents = read_to_string("examples/collections.ds").unwrap();
for result in eval(&file_contents) { for result in evaluate(&file_contents) {
result.unwrap(); result.unwrap();
} }
} }
@ -15,7 +15,7 @@ fn collections() {
fn list() { fn list() {
let file_contents = read_to_string("examples/list.ds").unwrap(); let file_contents = read_to_string("examples/list.ds").unwrap();
for result in eval(&file_contents) { for result in evaluate(&file_contents) {
result.unwrap(); result.unwrap();
} }
} }
@ -24,7 +24,7 @@ fn list() {
fn table() { fn table() {
let file_contents = read_to_string("examples/table.ds").unwrap(); let file_contents = read_to_string("examples/table.ds").unwrap();
for result in eval(&file_contents) { for result in evaluate(&file_contents) {
result.unwrap(); result.unwrap();
} }
} }
@ -33,7 +33,7 @@ fn table() {
fn variables() { fn variables() {
let file_contents = read_to_string("examples/variables.ds").unwrap(); let file_contents = read_to_string("examples/variables.ds").unwrap();
for result in eval(&file_contents) { for result in evaluate(&file_contents) {
result.unwrap(); result.unwrap();
} }
} }
@ -42,7 +42,7 @@ fn variables() {
fn scope() { fn scope() {
let file_contents = read_to_string("examples/scope.ds").unwrap(); let file_contents = read_to_string("examples/scope.ds").unwrap();
for result in eval(&file_contents) { for result in evaluate(&file_contents) {
result.unwrap(); result.unwrap();
} }
} }
@ -51,7 +51,7 @@ fn scope() {
fn data_formats() { fn data_formats() {
let file_contents = read_to_string("examples/data_formats.ds").unwrap(); let file_contents = read_to_string("examples/data_formats.ds").unwrap();
for result in eval(&file_contents) { for result in evaluate(&file_contents) {
result.unwrap(); result.unwrap();
} }
} }

@ -1 +1 @@
Subproject commit 7398f073ed8c844c442f21b0fd62579b14f36dd3 Subproject commit 03c566485155076983ff78689c62839c59b47327