diff --git a/dust-lang/src/abstract_tree.rs b/dust-lang/src/abstract_tree.rs index a734513..dc27996 100644 --- a/dust-lang/src/abstract_tree.rs +++ b/dust-lang/src/abstract_tree.rs @@ -37,13 +37,19 @@ pub enum Statement { // A sequence of statements Block(Vec>), - // Logic, math and comparison expressions + // Assignment, logic, math and comparison expressions with two operands BinaryOperation { left: Box>, operator: Node, right: Box>, }, + // Logic and math expressions with one operand + UnaryOperation { + operator: Node, + operand: Box>, + }, + // Function calls BuiltInFunctionCall { function: BuiltInFunction, @@ -152,6 +158,10 @@ impl Statement { } Statement::Nil(_) => None, Statement::PropertyAccess(_, _) => None, + Statement::UnaryOperation { operator, operand } => match operator.inner { + UnaryOperator::Negate => Some(operand.inner.expected_type(context)?), + UnaryOperator::Not => Some(Type::Boolean), + }, Statement::While { .. } => None, } } @@ -331,6 +341,9 @@ impl Display for Statement { } Statement::Nil(node) => write!(f, "{node};"), Statement::PropertyAccess(left, right) => write!(f, "{left}.{right}"), + Statement::UnaryOperation { operator, operand } => { + write!(f, "{operator}{operand}") + } Statement::While { condition, body } => { write!(f, "while {condition} {body}") } @@ -383,3 +396,18 @@ impl Display for BinaryOperator { } } } + +#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] +pub enum UnaryOperator { + Negate, + Not, +} + +impl Display for UnaryOperator { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + UnaryOperator::Negate => write!(f, "-"), + UnaryOperator::Not => write!(f, "!"), + } + } +} diff --git a/dust-lang/src/analyzer.rs b/dust-lang/src/analyzer.rs index bf529f9..4b2dfe0 100644 --- a/dust-lang/src/analyzer.rs +++ b/dust-lang/src/analyzer.rs @@ -11,8 +11,8 @@ use std::{ }; use crate::{ - abstract_tree::BinaryOperator, parse, AbstractSyntaxTree, Context, DustError, Node, Span, - Statement, Type, + abstract_tree::{BinaryOperator, UnaryOperator}, + parse, AbstractSyntaxTree, Context, DustError, Node, Span, Statement, Type, }; /// Analyzes the abstract syntax tree for errors. @@ -352,6 +352,31 @@ impl<'a> Analyzer<'a> { } } } + Statement::UnaryOperation { operator, operand } => { + self.analyze_statement(operand)?; + + if let UnaryOperator::Negate = operator.inner { + if let Some(Type::Integer | Type::Float | Type::Number) = + operand.inner.expected_type(self.context) + { + // Operand is valid + } else { + return Err(AnalyzerError::ExpectedBoolean { + actual: operand.as_ref().clone(), + }); + } + } + + if let UnaryOperator::Not = operator.inner { + if let Some(Type::Boolean) = operand.inner.expected_type(self.context) { + // Operand is valid + } else { + return Err(AnalyzerError::ExpectedBoolean { + actual: operand.as_ref().clone(), + }); + } + } + } Statement::While { condition, body } => { self.analyze_statement(condition)?; self.analyze_statement(body)?; diff --git a/dust-lang/src/context.rs b/dust-lang/src/context.rs index 231cc18..2b34550 100644 --- a/dust-lang/src/context.rs +++ b/dust-lang/src/context.rs @@ -133,7 +133,7 @@ mod tests { use super::*; #[test] - fn context_removes_used_variables() { + fn context_removes_variables() { env_logger::builder().is_test(true).try_init().unwrap(); let source = " @@ -150,7 +150,7 @@ mod tests { } #[test] - fn context_removes_variables_after_loop() { + fn garbage_collector_does_not_break_loops() { let source = " y = 1 z = 0 diff --git a/dust-lang/src/lexer.rs b/dust-lang/src/lexer.rs index 9dfaa88..421b0ec 100644 --- a/dust-lang/src/lexer.rs +++ b/dust-lang/src/lexer.rs @@ -253,6 +253,11 @@ impl Lexer { }); } } + '!' => { + self.position += 1; + + (Token::Bang, (self.position - 1, self.position)) + } _ => { self.position += 1; @@ -479,6 +484,41 @@ impl Display for LexError { mod tests { use super::*; + #[test] + fn negate_expression() { + let input = "x = -42; -x"; + + assert_eq!( + lex(input), + Ok(vec![ + (Token::Identifier("x"), (0, 1)), + (Token::Equal, (2, 3)), + (Token::Integer("-42"), (4, 7)), + (Token::Semicolon, (7, 8)), + (Token::Minus, (9, 10)), + (Token::Identifier("x"), (10, 11)), + (Token::Eof, (11, 11)) + ]) + ); + } + + #[test] + fn not_expression() { + let input = "!true; !false"; + + assert_eq!( + lex(input), + Ok(vec![ + (Token::Bang, (0, 1)), + (Token::Boolean("true"), (1, 5)), + (Token::Semicolon, (5, 6)), + (Token::Bang, (7, 8)), + (Token::Boolean("false"), (8, 13)), + (Token::Eof, (13, 13)) + ]) + ); + } + #[test] fn if_else() { let input = "if x < 10 { x + 1 } else { x }"; diff --git a/dust-lang/src/lib.rs b/dust-lang/src/lib.rs index e84af21..415f3dd 100644 --- a/dust-lang/src/lib.rs +++ b/dust-lang/src/lib.rs @@ -14,7 +14,7 @@ pub mod r#type; pub mod value; pub mod vm; -pub use abstract_tree::{AbstractSyntaxTree, BinaryOperator, Node, Statement}; +pub use abstract_tree::{AbstractSyntaxTree, BinaryOperator, Node, Statement, UnaryOperator}; pub use analyzer::{analyze, Analyzer, AnalyzerError}; pub use built_in_function::{BuiltInFunction, BuiltInFunctionError}; pub use context::{Context, VariableData}; diff --git a/dust-lang/src/parser.rs b/dust-lang/src/parser.rs index 8e1e8c9..36f9f77 100644 --- a/dust-lang/src/parser.rs +++ b/dust-lang/src/parser.rs @@ -13,7 +13,7 @@ use std::{ use crate::{ AbstractSyntaxTree, BinaryOperator, BuiltInFunction, DustError, Identifier, LexError, Lexer, - Node, Span, Statement, Token, TokenOwned, Value, + Node, Span, Statement, Token, TokenOwned, UnaryOperator, Value, }; /// Parses the input into an abstract syntax tree. @@ -163,6 +163,34 @@ impl<'src> Parser<'src> { fn parse_primary(&mut self) -> Result, ParseError> { match self.current { + (Token::Bang, position) => { + self.next_token()?; + + let operand = Box::new(self.parse_statement(0)?); + let operand_end = operand.position.1; + + Ok(Node::new( + Statement::UnaryOperation { + operator: Node::new(UnaryOperator::Not, position), + operand, + }, + (position.0, operand_end), + )) + } + (Token::Minus, position) => { + self.next_token()?; + + let operand = Box::new(self.parse_statement(0)?); + let operand_end = operand.position.1; + + Ok(Node::new( + Statement::UnaryOperation { + operator: Node::new(UnaryOperator::Negate, position), + operand, + }, + (position.0, operand_end), + )) + } (Token::Boolean(text), position) => { self.next_token()?; @@ -799,10 +827,83 @@ impl Display for ParseError { #[cfg(test)] mod tests { - use crate::{abstract_tree::BinaryOperator, Identifier}; + use crate::{abstract_tree::BinaryOperator, Identifier, UnaryOperator}; use super::*; + #[test] + fn negate_variable() { + let input = "a = 1; -a"; + + assert_eq!( + parse(input), + Ok(AbstractSyntaxTree { + nodes: [ + Node::new( + Statement::Nil(Box::new(Node::new( + Statement::BinaryOperation { + left: Box::new(Node::new( + Statement::Identifier(Identifier::new("a")), + (0, 1) + )), + operator: Node::new(BinaryOperator::Assign, (2, 3)), + right: Box::new(Node::new( + Statement::Constant(Value::integer(1)), + (4, 5) + )), + }, + (0, 5) + ))), + (0, 5) + ), + Node::new( + Statement::UnaryOperation { + operator: Node::new(UnaryOperator::Negate, (0, 1)), + operand: Box::new(Node::new( + Statement::Identifier(Identifier::new("a")), + (1, 2) + )), + }, + (0, 2) + ) + ] + .into() + }) + ); + } + + #[test] + fn negate_expression() { + let input = "-(1 + 1)"; + + assert_eq!( + parse(input), + Ok(AbstractSyntaxTree { + nodes: [Node::new( + Statement::UnaryOperation { + operator: Node::new(UnaryOperator::Negate, (0, 1)), + operand: Box::new(Node::new( + Statement::BinaryOperation { + left: Box::new(Node::new( + Statement::Constant(Value::integer(1)), + (2, 3) + )), + operator: Node::new(BinaryOperator::Add, (4, 5)), + right: Box::new(Node::new( + Statement::Constant(Value::integer(1)), + (6, 7) + )), + }, + (1, 8) + )), + }, + (0, 8) + )] + .into() + }) + ); + } + #[test] fn r#if() { let input = "if x { y }"; diff --git a/dust-lang/src/token.rs b/dust-lang/src/token.rs index 09a8e30..882d210 100644 --- a/dust-lang/src/token.rs +++ b/dust-lang/src/token.rs @@ -28,6 +28,7 @@ pub enum Token<'src> { WriteLine, // Symbols + Bang, Comma, Dot, DoubleAmpersand, @@ -56,6 +57,7 @@ pub enum Token<'src> { impl<'src> Token<'src> { pub fn to_owned(&self) -> TokenOwned { match self { + Token::Bang => TokenOwned::Bang, Token::Boolean(boolean) => TokenOwned::Boolean(boolean.to_string()), Token::Comma => TokenOwned::Comma, Token::Dot => TokenOwned::Dot, @@ -104,6 +106,7 @@ impl<'src> Token<'src> { Token::Integer(integer_text) => integer_text, Token::String(text) => text, + Token::Bang => "!", Token::Comma => ",", Token::Dot => ".", Token::DoubleAmpersand => "&&", @@ -147,18 +150,19 @@ impl<'src> Token<'src> { pub fn precedence(&self) -> u8 { match self { - Token::Dot => 12, - Token::Star | Token::Slash | Token::Percent => 10, - Token::Plus | Token::Minus => 9, + Token::Dot => 9, + Token::Star | Token::Slash | Token::Percent => 8, + Token::Minus => 7, + Token::Plus => 6, Token::DoubleEqual | Token::Less | Token::LessEqual | Token::Greater - | Token::GreaterEqual => 8, - Token::DoubleAmpersand => 7, - Token::DoublePipe => 6, - Token::Equal | Token::PlusEqual => 5, - Token::Semicolon => 4, + | Token::GreaterEqual => 5, + Token::DoubleAmpersand => 4, + Token::DoublePipe => 3, + Token::Equal | Token::PlusEqual => 2, + Token::Semicolon => 1, _ => 0, } } @@ -168,7 +172,11 @@ impl<'src> Token<'src> { } pub fn is_right_associative(&self) -> bool { - matches!(self, Token::Semicolon) + matches!(self, Token::Equal | Token::PlusEqual) + } + + pub fn is_prefix(&self) -> bool { + matches!(self, Token::Bang | Token::Minus) } pub fn is_postfix(&self) -> bool { @@ -185,6 +193,7 @@ impl<'src> Display for Token<'src> { impl<'src> PartialEq for Token<'src> { fn eq(&self, other: &Self) -> bool { match (self, other) { + (Token::Bang, Token::Bang) => true, (Token::Boolean(left), Token::Boolean(right)) => left == right, (Token::Comma, Token::Comma) => true, (Token::Dot, Token::Dot) => true, @@ -254,6 +263,7 @@ pub enum TokenOwned { WriteLine, // Symbols + Bang, Comma, Dot, DoubleAmpersand, @@ -282,6 +292,7 @@ pub enum TokenOwned { impl Display for TokenOwned { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { + TokenOwned::Bang => Token::Bang.fmt(f), TokenOwned::Boolean(boolean) => write!(f, "{boolean}"), TokenOwned::Comma => Token::Comma.fmt(f), TokenOwned::Dot => Token::Dot.fmt(f), diff --git a/dust-lang/src/value.rs b/dust-lang/src/value.rs index 15ba230..07b3c5d 100644 --- a/dust-lang/src/value.rs +++ b/dust-lang/src/value.rs @@ -105,6 +105,14 @@ impl Value { } } + pub fn as_float(&self) -> Option { + if let ValueInner::Float(float) = self.0.as_ref() { + Some(*float) + } else { + None + } + } + pub fn as_function(&self) -> Option<&Function> { if let ValueInner::Function(function) = self.0.as_ref() { Some(function) diff --git a/dust-lang/src/vm.rs b/dust-lang/src/vm.rs index 768d3bd..b4075b3 100644 --- a/dust-lang/src/vm.rs +++ b/dust-lang/src/vm.rs @@ -6,8 +6,8 @@ use std::{ use crate::{ abstract_tree::BinaryOperator, parse, value::ValueInner, AbstractSyntaxTree, Analyzer, - BuiltInFunctionError, Context, DustError, Identifier, Node, ParseError, Span, Statement, Value, - ValueError, + BuiltInFunctionError, Context, DustError, Identifier, Node, ParseError, Span, Statement, + UnaryOperator, Value, ValueError, }; pub fn run(source: &str) -> Result, DustError> { @@ -500,6 +500,33 @@ impl Vm { position: right_span, }) } + Statement::UnaryOperation { operator, operand } => { + let position = operand.position; + let value = if let Some(value) = self.run_statement(*operand, context)? { + value + } else { + return Err(VmError::ExpectedValue { position }); + }; + + match operator.inner { + UnaryOperator::Negate => { + if let Some(value) = value.as_integer() { + Ok(Some(Value::integer(-value))) + } else if let Some(value) = value.as_float() { + Ok(Some(Value::float(-value))) + } else { + Err(VmError::ExpectedNumber { position }) + } + } + UnaryOperator::Not => { + if let Some(value) = value.as_boolean() { + Ok(Some(Value::boolean(!value))) + } else { + Err(VmError::ExpectedBoolean { position }) + } + } + } + } Statement::While { condition, body } => { let mut return_value = None; @@ -555,6 +582,9 @@ pub enum VmError { ExpectedInteger { position: Span, }, + ExpectedNumber { + position: Span, + }, ExpectedFunction { actual: Value, position: Span, @@ -588,6 +618,7 @@ impl VmError { Self::ExpectedInteger { position } => *position, Self::ExpectedFunction { position, .. } => *position, Self::ExpectedList { position } => *position, + Self::ExpectedNumber { position } => *position, Self::ExpectedValue { position } => *position, Self::UndefinedVariable { identifier } => identifier.position, Self::UndefinedProperty { @@ -637,6 +668,13 @@ impl Display for VmError { Self::ExpectedList { position } => { write!(f, "Expected a list at position: {:?}", position) } + Self::ExpectedNumber { position } => { + write!( + f, + "Expected an integer or float at position: {:?}", + position + ) + } Self::ExpectedValue { position } => { write!(f, "Expected a value at position: {:?}", position) } @@ -656,6 +694,20 @@ impl Display for VmError { mod tests { use super::*; + #[test] + fn negate_expression() { + let input = "x = -42; -x"; + + assert_eq!(run(input), Ok(Some(Value::integer(42)))); + } + + #[test] + fn not_expression() { + let input = "!(1 == 2 || 3 == 4 || 5 == 6)"; + + assert_eq!(run(input), Ok(Some(Value::boolean(true)))); + } + #[test] fn list_index() { let input = "[1, 42, 3].1";