From a9de8dbf81e8dbc08ee46e3a0fc05f5812e95c1c Mon Sep 17 00:00:00 2001 From: Louis Capitanchik <contact@louiscap.co> Date: Tue, 16 May 2023 17:23:02 +0100 Subject: [PATCH] Visitors need return and error types, start basic expressions in simple interpreter --- forge-script-lang/src/parser/ast.rs | 6 +- forge-script-lang/src/parser/grammar.rs | 8 +- forge-script-lang/src/parser/test_suite.rs | 4 + forge-script-lang/src/runtime/executor/mod.rs | 22 +- .../src/runtime/executor/printer.rs | 58 +++- .../src/runtime/executor/simple.rs | 157 +++++++++ forge-script-lang/src/runtime/value.rs | 303 ++++++++++++++++++ 7 files changed, 534 insertions(+), 24 deletions(-) create mode 100644 forge-script-lang/src/runtime/executor/simple.rs diff --git a/forge-script-lang/src/parser/ast.rs b/forge-script-lang/src/parser/ast.rs index 644913e..131a146 100644 --- a/forge-script-lang/src/parser/ast.rs +++ b/forge-script-lang/src/parser/ast.rs @@ -64,8 +64,7 @@ pub enum Expression { Void(VoidExpression), } -#[derive(Clone)] -#[cfg_attr(feature = "debug-ast", derive(Debug))] +#[derive(Clone, Eq, Copy, PartialEq, Debug)] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), @@ -89,8 +88,7 @@ impl Display for UnaryOp { } } -#[derive(Clone)] -#[cfg_attr(feature = "debug-ast", derive(Debug))] +#[derive(Clone, Eq, Copy, PartialEq, Debug)] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), diff --git a/forge-script-lang/src/parser/grammar.rs b/forge-script-lang/src/parser/grammar.rs index 59fba41..1f1a12d 100644 --- a/forge-script-lang/src/parser/grammar.rs +++ b/forge-script-lang/src/parser/grammar.rs @@ -30,8 +30,7 @@ peg::parser! { #[cache_left_rec] rule value_expression() -> ValueExpression - = "(" ex:value_expression() ")" { ValueExpression::Grouped(GroupedExpression { inner: Box::new(ex) }) } - / co:conditional() { ValueExpression::ConditionalBlock(co) } + = co:conditional() { ValueExpression::ConditionalBlock(co) } / decl:declare_variable() { ValueExpression::DeclareIdentifier(decl) } / decl:declare_function() { ValueExpression::DeclareFunction(decl) } / name:bare_identifier() "(" params:param_list()? ")" @@ -40,9 +39,14 @@ peg::parser! { { ValueExpression::Binary { lhs: Box::new(left), rhs: Box::new(right), operator: op } } / op:unary_operator() operand:value_expression() { ValueExpression::Unary { operator: op, operand: Box::new(operand) } } + / grouped() / ident:bare_identifier() { ValueExpression::Identifier(ident) } / li:literal() { ValueExpression::Literal(li) } + rule grouped() -> ValueExpression + = "(" ex:value_expression() ")" + { ValueExpression::Grouped(GroupedExpression { inner: Box::new(ex) }) } + rule print() -> Print = "print" ex:value_expression() { ex.into() } diff --git a/forge-script-lang/src/parser/test_suite.rs b/forge-script-lang/src/parser/test_suite.rs index 0b00afa..fc217e2 100644 --- a/forge-script-lang/src/parser/test_suite.rs +++ b/forge-script-lang/src/parser/test_suite.rs @@ -3,6 +3,10 @@ use crate::parse::parse_program; #[test] fn binary_ops() { parse_program("1+1").expect("Failed binary add"); + parse_program("1 + 2 + 3 + 4").expect("Failed binary add"); + parse_program("1 + (2 + 3) + 4").expect("Failed binary add"); + parse_program("(1 + 2) + 3 + 4").expect("Failed binary add"); + parse_program("1-1").expect("Failed binary sub"); parse_program("1*1").expect("Failed binary mul"); parse_program("1/1").expect("Failed binary div"); diff --git a/forge-script-lang/src/runtime/executor/mod.rs b/forge-script-lang/src/runtime/executor/mod.rs index ab3ef43..7b0bb30 100644 --- a/forge-script-lang/src/runtime/executor/mod.rs +++ b/forge-script-lang/src/runtime/executor/mod.rs @@ -1,17 +1,31 @@ mod printer; +mod simple; use crate::parser::ast::{Expression, Program, ValueExpression, VoidExpression}; +use std::error::Error; pub trait Visitor { - fn evaluate_value_expression(&mut self, expression: &ValueExpression); - fn evaluate_void_expression(&mut self, expression: &VoidExpression); - fn evaluate_expression(&mut self, expression: &Expression) { + type Output; + type Error: Error; + + fn evaluate_value_expression( + &mut self, + expression: &ValueExpression, + ) -> Result<Self::Output, Self::Error>; + fn evaluate_void_expression( + &mut self, + expression: &VoidExpression, + ) -> Result<Self::Output, Self::Error>; + fn evaluate_expression( + &mut self, + expression: &Expression, + ) -> Result<Self::Output, Self::Error> { match expression { Expression::Value(expr) => self.evaluate_value_expression(expr), Expression::Void(expr) => self.evaluate_void_expression(expr), } } - fn evaluate_program(&mut self, program: &Program); + fn evaluate_program(&mut self, program: &Program) -> Result<Self::Output, Self::Error>; } pub use printer::TreePrinter; diff --git a/forge-script-lang/src/runtime/executor/printer.rs b/forge-script-lang/src/runtime/executor/printer.rs index 4f95e61..31b5b3b 100644 --- a/forge-script-lang/src/runtime/executor/printer.rs +++ b/forge-script-lang/src/runtime/executor/printer.rs @@ -1,9 +1,12 @@ use crate::parser::ast::*; use crate::runtime::executor::Visitor; +use std::error::Error; +use std::fmt::{Display, Formatter}; pub struct TreePrinter { indent: usize, buffer: String, + should_indent: bool, } impl TreePrinter { @@ -11,6 +14,15 @@ impl TreePrinter { Self { indent: 0, buffer: String::new(), + should_indent: true, + } + } + + pub fn with_indent(indent: usize) -> Self { + Self { + indent, + buffer: String::new(), + should_indent: true, } } @@ -27,17 +39,25 @@ impl TreePrinter { } fn write(&mut self, value: impl ToString) { + if self.should_indent { + self.should_indent = false; + self.write_indent(); + } self.buffer.push_str(value.to_string().as_str()); } + fn writeln(&mut self, value: impl ToString) { - self.buffer.push_str(value.to_string().as_str()); - self.buffer.push('\n'); + self.write(value); + self.new_line(); } + fn write_indent(&mut self) { self.write(self.get_indent()); } + fn new_line(&mut self) { self.buffer.push('\n'); + self.should_indent = true; } fn format_expression_list(&self, list: &ExpressionList) -> String { @@ -72,8 +92,20 @@ impl TreePrinter { } } +#[derive(Debug, Clone, Copy)] +pub struct TreeError; +impl Display for TreeError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str("Tree Error") + } +} +impl Error for TreeError {} + impl Visitor for TreePrinter { - fn evaluate_value_expression(&mut self, expression: &ValueExpression) { + type Output = (); + type Error = TreeError; + + fn evaluate_value_expression(&mut self, expression: &ValueExpression) -> Result<(), TreeError> { match expression { ValueExpression::Unary { operand, operator } => { self.write(operator); @@ -150,10 +182,7 @@ impl Visitor for TreePrinter { .params .iter() .map(|param| { - let mut inner = TreePrinter { - indent: self.indent, - buffer: String::new(), - }; + let mut inner = TreePrinter::with_indent(self.indent); inner.evaluate_value_expression(param); inner.take_value() }) @@ -169,11 +198,7 @@ impl Visitor for TreePrinter { .params .iter() .map(|param| { - let mut inner = TreePrinter { - indent: self.indent, - buffer: String::new(), - }; - + let mut inner = TreePrinter::with_indent(self.indent); match param { DeclareIdent::WithValue(assign) => { inner.write(&assign.ident); @@ -195,9 +220,11 @@ impl Visitor for TreePrinter { self.writeln("}"); } } + + Ok(()) } - fn evaluate_void_expression(&mut self, expression: &VoidExpression) { + fn evaluate_void_expression(&mut self, expression: &VoidExpression) -> Result<(), TreeError> { match expression { VoidExpression::ConditionLoop(cond) => { self.write("while "); @@ -237,9 +264,12 @@ impl Visitor for TreePrinter { self.evaluate_value_expression(print.expr.as_ref()); } } + + Ok(()) } - fn evaluate_program(&mut self, program: &Program) { + fn evaluate_program(&mut self, program: &Program) -> Result<(), TreeError> { self.writeln(self.format_expression_list(&program.0)); + Ok(()) } } diff --git a/forge-script-lang/src/runtime/executor/simple.rs b/forge-script-lang/src/runtime/executor/simple.rs new file mode 100644 index 0000000..6dbd2c3 --- /dev/null +++ b/forge-script-lang/src/runtime/executor/simple.rs @@ -0,0 +1,157 @@ +use crate::parse::ast::{ + BinaryOp, DeclareFunction, ExpressionList, Program, UnaryOp, ValueExpression, VoidExpression, +}; +use crate::runtime::executor::Visitor; +use crate::runtime::value::{ForgeValue, UnsupportedOperation}; +use std::collections::HashMap; +use std::error::Error; +use std::fmt::{Debug, Display, Formatter}; + +#[derive(Clone, Default)] +pub struct SimpleExecutor { + data: HashMap<String, ForgeValue>, + vtable: HashMap<String, DeclareFunction>, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum RuntimeError { + Unsupported(&'static str), + BadOperands(UnsupportedOperation), +} + +impl From<UnsupportedOperation> for RuntimeError { + fn from(value: UnsupportedOperation) -> Self { + Self::BadOperands(value) + } +} + +impl Display for RuntimeError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + RuntimeError::Unsupported(expr) => { + write!( + f, + "[Runtime] Encountered an unsupported expression: {}", + expr + ) + } + RuntimeError::BadOperands(unsupported) => write!(f, "[Runtime] {}", unsupported), + } + } +} + +impl Error for RuntimeError {} + +impl SimpleExecutor { + fn evaluate_expression_list( + &mut self, + list: &ExpressionList, + ) -> Result<ForgeValue, RuntimeError> { + let mut last_val = Ok(ForgeValue::Null); + for expr in &list.expressions { + last_val = Ok(self.evaluate_expression(expr)?); + } + if list.is_void { + Ok(ForgeValue::Null) + } else { + last_val + } + } +} + +impl Visitor for SimpleExecutor { + type Output = ForgeValue; + type Error = RuntimeError; + + fn evaluate_value_expression( + &mut self, + expression: &ValueExpression, + ) -> Result<Self::Output, Self::Error> { + match expression { + ValueExpression::Unary { operator, operand } => { + let value = self.evaluate_value_expression(operand.as_ref())?; + match operator { + UnaryOp::Negate => Ok(value.invert()), + UnaryOp::Not => Ok(!value), + } + } + ValueExpression::Binary { operator, lhs, rhs } => { + let lhs = self.evaluate_value_expression(lhs.as_ref())?; + let rhs = self.evaluate_value_expression(rhs.as_ref())?; + + match operator { + BinaryOp::Add => Ok(lhs + rhs), + BinaryOp::Subtract => Ok((lhs - rhs)?), + BinaryOp::Divide => Ok((lhs / rhs)?), + BinaryOp::Multiply => Ok((lhs * rhs)?), + BinaryOp::Modulo => Ok((lhs % rhs)?), + BinaryOp::Equals => Ok((lhs == rhs).into()), + } + } + ValueExpression::Grouped(group) => self.evaluate_value_expression(group.inner.as_ref()), + ValueExpression::Block(_) => Err(RuntimeError::Unsupported("Block")), + ValueExpression::Literal(lit) => Ok(ForgeValue::from(lit.clone())), + ValueExpression::DeclareIdentifier(_) => { + Err(RuntimeError::Unsupported("DeclareIdentifier")) + } + ValueExpression::Assignment(_) => Err(RuntimeError::Unsupported("Assignment")), + ValueExpression::ConditionalBlock(_) => { + Err(RuntimeError::Unsupported("ConditionalBlock")) + } + ValueExpression::Identifier(_) => Err(RuntimeError::Unsupported("Identifier")), + ValueExpression::FunctionCall(_) => Err(RuntimeError::Unsupported("FunctionCall")), + ValueExpression::DeclareFunction(_) => { + Err(RuntimeError::Unsupported("DeclareFunction")) + } + } + } + + fn evaluate_void_expression( + &mut self, + expression: &VoidExpression, + ) -> Result<Self::Output, Self::Error> { + Err(RuntimeError::Unsupported("Void Expression")) + } + + fn evaluate_program(&mut self, program: &Program) -> Result<Self::Output, Self::Error> { + self.evaluate_expression_list(&program.0) + } +} + +#[cfg(test)] +mod interpreter_test { + use crate::parse::parse_program; + use crate::runtime::executor::simple::{RuntimeError, SimpleExecutor}; + use crate::runtime::executor::Visitor; + use crate::runtime::value::ForgeValue; + + #[test] + fn the_basics() { + let add_numbers = parse_program("1 + 1").expect("Failed to parse"); + let add_strings = parse_program("\"foo\" + \" \" + \"bar\"").expect("Failed to parse"); + let concat_string_num = parse_program("\"#\" + 1").expect("Failed to parse"); + let bad_mult = parse_program("false * 123").expect("Failed to parse"); + let mut vm = SimpleExecutor::default(); + + assert_eq!(vm.evaluate_program(&add_numbers), Ok(ForgeValue::from(2))); + assert_eq!( + vm.evaluate_program(&add_strings), + Ok(ForgeValue::from("foo bar")) + ); + assert_eq!( + vm.evaluate_program(&concat_string_num), + Ok(ForgeValue::from("#1")) + ); + assert!(matches!( + vm.evaluate_program(&bad_mult), + Err(RuntimeError::BadOperands(_)) + )) + } + + #[test] + fn combos() { + let add_numbers = parse_program("1 + -1").expect("Failed to parse"); + let mut vm = SimpleExecutor::default(); + assert_eq!(vm.evaluate_program(&add_numbers), Ok(ForgeValue::from(0))); + } +} diff --git a/forge-script-lang/src/runtime/value.rs b/forge-script-lang/src/runtime/value.rs index 089a07f..1078730 100644 --- a/forge-script-lang/src/runtime/value.rs +++ b/forge-script-lang/src/runtime/value.rs @@ -1,6 +1,8 @@ +use crate::parse::ast::BinaryOp; use crate::parser::ast::LiteralNode; use crate::runtime::numbers::Number; use std::fmt::{Display, Formatter}; +use std::ops::{Add, Div, Mul, Not, Rem, Sub}; #[derive(Clone, Debug)] #[cfg_attr( @@ -25,6 +27,20 @@ pub enum ForgeValue { } impl ForgeValue { + pub fn null() -> Self { + ForgeValue::Null + } + + pub fn invert(&self) -> Self { + match self { + ForgeValue::Number(num) => match num { + Number::Integer(val) => Number::Integer(-*val).into(), + Number::Float(val) => Number::Float(-*val).into(), + }, + val => val.clone(), + } + } + /// Perform type coercion to force this value into a bool /// /// ## True @@ -140,3 +156,290 @@ impl From<LiteralNode> for ForgeValue { } } } + +impl From<String> for ForgeValue { + fn from(value: String) -> Self { + ForgeValue::String(value) + } +} +impl<'a> From<&'a str> for ForgeValue { + fn from(value: &'a str) -> Self { + ForgeValue::String(String::from(value)) + } +} + +impl From<bool> for ForgeValue { + fn from(value: bool) -> Self { + ForgeValue::Boolean(value) + } +} + +impl From<Number> for ForgeValue { + fn from(value: Number) -> Self { + ForgeValue::Number(value) + } +} + +macro_rules! from_number { + ($($typ:ty),+) => { + $( + impl From<$typ> for ForgeValue { + fn from(value: $typ) -> Self { + ForgeValue::Number(Number::from(value)) + } + } + )+ + }; +} + +from_number!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize, f32, f64); + +impl PartialEq for ForgeValue { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (ForgeValue::String(s1), ForgeValue::String(s2)) => s1.eq(s2), + (ForgeValue::Number(n1), ForgeValue::Number(n2)) => n1.eq(n2), + (ForgeValue::Boolean(b1), ForgeValue::Boolean(b2)) => b1.eq(b2), + (ForgeValue::Null, ForgeValue::Null) => true, + _ => false, + } + } +} + +impl Add for ForgeValue { + type Output = ForgeValue; + + fn add(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (ForgeValue::String(s1), ForgeValue::String(s2)) => ForgeValue::String({ + let mut buffer = String::with_capacity(s1.len() + s2.len()); + buffer.push_str(s1.as_str()); + buffer.push_str(s2.as_str()); + buffer + }), + (ForgeValue::Number(n1), ForgeValue::Number(n2)) => ForgeValue::Number(n1 + n2), + (ForgeValue::String(val1), ForgeValue::Number(val2)) => { + ForgeValue::String(format!("{}{}", val1, val2)) + } + + (ForgeValue::Number(val1), ForgeValue::String(val2)) => { + ForgeValue::String(format!("{}{}", val1, val2)) + } + + (ForgeValue::List(first), ForgeValue::List(second)) => { + ForgeValue::List(first.iter().chain(second.iter()).cloned().collect()) + } + + (other_value, ForgeValue::List(mut list)) + | (ForgeValue::List(mut list), other_value) => { + list.push(other_value); + ForgeValue::List(list) + } + + // Boolean && null values are ignored + (ForgeValue::Number(val), ForgeValue::Null) + | (ForgeValue::Number(val), ForgeValue::Boolean(_)) + | (ForgeValue::Null, ForgeValue::Number(val)) + | (ForgeValue::Boolean(_), ForgeValue::Number(val)) => ForgeValue::Number(val), + + (ForgeValue::Boolean(val), ForgeValue::Null) + | (ForgeValue::Null, ForgeValue::Boolean(val)) => ForgeValue::Boolean(val), + + (ForgeValue::Null, ForgeValue::String(val)) + | (ForgeValue::Boolean(_), ForgeValue::String(val)) + | (ForgeValue::String(val), ForgeValue::Null) + | (ForgeValue::String(val), ForgeValue::Boolean(_)) => ForgeValue::String(val), + + (ForgeValue::Null, ForgeValue::Null) => ForgeValue::Null, + + (ForgeValue::Boolean(b1), ForgeValue::Boolean(b2)) => ForgeValue::Boolean(b1 && b2), + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct UnsupportedOperation { + operation: BinaryOp, + left_type: &'static str, + right_type: &'static str, +} + +impl Display for UnsupportedOperation { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Cannot perform {} with lhs type {} and rhs type {}", + self.operation, self.left_type, self.right_type + ) + } +} + +impl Sub for ForgeValue { + type Output = Result<ForgeValue, UnsupportedOperation>; + fn sub(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (ForgeValue::Number(n1), ForgeValue::Number(n2)) => Ok(ForgeValue::Number(n1 - n2)), + (ForgeValue::Null, ForgeValue::Null) => Ok(ForgeValue::Null), + (ForgeValue::Number(_), _) => Err(UnsupportedOperation { + operation: BinaryOp::Subtract, + left_type: "number", + right_type: "non-number", + }), + (_, _) => Err(UnsupportedOperation { + operation: BinaryOp::Subtract, + left_type: "non-number", + right_type: "non-number", + }), + } + } +} + +impl Div for ForgeValue { + type Output = Result<ForgeValue, UnsupportedOperation>; + fn div(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (ForgeValue::Number(n1), ForgeValue::Number(n2)) => Ok(ForgeValue::Number(n1 / n2)), + (ForgeValue::Null, ForgeValue::Null) => Ok(ForgeValue::Null), + (ForgeValue::Number(_), _) => Err(UnsupportedOperation { + operation: BinaryOp::Divide, + left_type: "number", + right_type: "non-number", + }), + (_, _) => Err(UnsupportedOperation { + operation: BinaryOp::Divide, + left_type: "non-number", + right_type: "non-number", + }), + } + } +} + +impl Mul for ForgeValue { + type Output = Result<ForgeValue, UnsupportedOperation>; + fn mul(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (ForgeValue::Number(n1), ForgeValue::Number(n2)) => Ok(ForgeValue::Number(n1 * n2)), + (ForgeValue::Null, ForgeValue::Null) => Ok(ForgeValue::Null), + (ForgeValue::Number(_), _) => Err(UnsupportedOperation { + operation: BinaryOp::Multiply, + left_type: "number", + right_type: "non-number", + }), + (_, _) => Err(UnsupportedOperation { + operation: BinaryOp::Multiply, + left_type: "non-number", + right_type: "non-number", + }), + } + } +} + +impl Rem for ForgeValue { + type Output = Result<ForgeValue, UnsupportedOperation>; + fn rem(self, rhs: Self) -> Self::Output { + match (self, rhs) { + (ForgeValue::Number(n1), ForgeValue::Number(n2)) => Ok(ForgeValue::Number(n1 % n2)), + (ForgeValue::Null, ForgeValue::Null) => Ok(ForgeValue::Null), + (ForgeValue::Number(_), _) => Err(UnsupportedOperation { + operation: BinaryOp::Modulo, + left_type: "number", + right_type: "non-number", + }), + (_, _) => Err(UnsupportedOperation { + operation: BinaryOp::Modulo, + left_type: "non-number", + right_type: "non-number", + }), + } + } +} + +impl Not for ForgeValue { + type Output = ForgeValue; + + fn not(self) -> Self::Output { + match self { + ForgeValue::Number(num) => (num != Number::Integer(0)).into(), + ForgeValue::Boolean(b) => (!b).into(), + ForgeValue::String(st) => (!st.is_empty()).into(), + ForgeValue::List(ls) => (!ls.is_empty()).into(), + ForgeValue::Null => ForgeValue::Null, + } + } +} + +#[cfg(test)] +mod value_tests { + use crate::runtime::value::ForgeValue; + + #[test] + fn strict_type_equality() { + assert_ne!(ForgeValue::from(22), ForgeValue::from("22")); + assert_ne!(ForgeValue::from(true), ForgeValue::from("true")); + assert_ne!(ForgeValue::from(false), ForgeValue::from("false")); + assert_ne!(ForgeValue::from(true), ForgeValue::from(1)); + assert_ne!(ForgeValue::from(false), ForgeValue::from(0)); + } + + #[test] + fn add_strings_concats() { + let first = ForgeValue::from("hello_"); + let second = ForgeValue::from("world"); + + assert_eq!( + first + second, + ForgeValue::String(String::from("hello_world")) + ); + } + + #[test] + fn add_numbers() { + let first = ForgeValue::from(250); + let second = ForgeValue::from(250); + let third = ForgeValue::from(250); + let fourth = ForgeValue::from(250.0); + + assert_eq!(first + second, ForgeValue::from(500)); + assert_eq!(third + fourth, ForgeValue::from(500.0)); + } + + #[test] + fn add_bool_noop() { + assert_eq!( + ForgeValue::from("Something") + ForgeValue::from(true), + ForgeValue::from("Something") + ); + assert_eq!( + ForgeValue::from("Something") + ForgeValue::from(false), + ForgeValue::from("Something") + ); + assert_eq!( + ForgeValue::from(1000) + ForgeValue::from(true), + ForgeValue::from(1000) + ); + assert_eq!( + ForgeValue::from(1000) + ForgeValue::from(false), + ForgeValue::from(1000) + ); + } + + #[test] + fn add_null_noop() { + assert_eq!( + ForgeValue::from("Something") + ForgeValue::null(), + ForgeValue::from("Something") + ); + assert_eq!( + ForgeValue::from(1000) + ForgeValue::null(), + ForgeValue::from(1000) + ); + assert_eq!( + ForgeValue::from(true) + ForgeValue::null(), + ForgeValue::from(true) + ); + assert_eq!( + ForgeValue::from(false) + ForgeValue::null(), + ForgeValue::from(false) + ); + } +} -- GitLab