diff --git a/forge-script-lang/src/lexer/keywords.rs b/forge-script-lang/src/lexer/keywords.rs index 7d91ae5465fd482fa2df6cc0bdd129891c84e0e0..b919a221e2c2ce0260bd252ded56c3f882917ea4 100644 --- a/forge-script-lang/src/lexer/keywords.rs +++ b/forge-script-lang/src/lexer/keywords.rs @@ -195,3 +195,15 @@ pub fn token_from(input: Span) -> IResult<Span, ScriptToken> { }, )) } + +pub fn token_typeof(input: Span) -> IResult<Span, ScriptToken> { + let (input, pos) = position(input)?; + let (input, _) = tag_ws("typeof", input)?; + Ok(( + input, + ScriptToken { + position: pos, + token_type: ScriptTokenType::Typeof, + }, + )) +} diff --git a/forge-script-lang/src/lexer/mod.rs b/forge-script-lang/src/lexer/mod.rs index 3eaf5a17bc43e30489ebefdbd3eb2fd263db56e7..76e85ed3a9224169e4ee5e153bf731c9884b1115 100644 --- a/forge-script-lang/src/lexer/mod.rs +++ b/forge-script-lang/src/lexer/mod.rs @@ -8,7 +8,7 @@ mod tokens; use keywords::{ token_alias, token_else, token_export, token_for, token_from, token_function, token_if, token_import, token_let, token_null, token_print, token_return, token_struct, token_super, - token_this, token_while, + token_this, token_typeof, token_while, }; use operators::{ token_asterisk, token_bang, token_bang_equal, token_caret, token_comma, token_dot, @@ -52,6 +52,7 @@ mod _lex { token_struct, token_super, token_from, + token_typeof, )), alt(( token_plus, @@ -72,13 +73,13 @@ mod _lex { token_semicolon, )), alt(( - token_less, - token_greater, - token_equal, token_equal_equal, token_bang_equal, token_less_equal, token_greater_equal, + token_less, + token_greater, + token_equal, )), alt(( token_float, diff --git a/forge-script-lang/src/lexer/tokens.rs b/forge-script-lang/src/lexer/tokens.rs index 33031ae283e85e71bbc2f0781f09688632ec7cf4..bb2731cc12e9d6b8ac5dd79abcb44cc130f8d34a 100644 --- a/forge-script-lang/src/lexer/tokens.rs +++ b/forge-script-lang/src/lexer/tokens.rs @@ -58,6 +58,7 @@ pub enum ScriptTokenType<'a> { Import, Alias, From, + Typeof, // Misc Eof, @@ -118,6 +119,7 @@ impl<'a> ScriptTokenType<'a> { ScriptTokenType::Alias => 2, ScriptTokenType::From => 4, ScriptTokenType::Eof => 0, + ScriptTokenType::Typeof => 6, } } } @@ -171,6 +173,7 @@ impl<'a> Display for ScriptTokenType<'a> { ScriptTokenType::Alias => write!(f, "as"), ScriptTokenType::From => write!(f, "from"), ScriptTokenType::Eof => write!(f, ""), + ScriptTokenType::Typeof => write!(f, "typeof"), } } } @@ -230,6 +233,7 @@ impl<'a> TryFrom<&'a str> for ScriptTokenType<'a> { "import" => Ok(ScriptTokenType::Import), "as" => Ok(ScriptTokenType::Alias), "from" => Ok(ScriptTokenType::From), + "typeof" => Ok(ScriptTokenType::Typeof), "false" => Ok(ScriptTokenType::Boolean(false)), "true" => Ok(ScriptTokenType::Boolean(true)), _ => Err(TokenFromStringError { source: value }), @@ -366,5 +370,9 @@ mod token_tests { ); assert_eq!(ScriptTokenType::try_from("as"), Ok(ScriptTokenType::Alias)); assert_eq!(ScriptTokenType::try_from("from"), Ok(ScriptTokenType::From)); + assert_eq!( + ScriptTokenType::try_from("typeof"), + Ok(ScriptTokenType::Typeof) + ); } } diff --git a/forge-script-lang/src/parser/ast.rs b/forge-script-lang/src/parser/ast.rs index 131a146893376f78a58c9e8d361ebddd9b9ccec1..042841a14102d2d91162b8dd054876d9f417e98e 100644 --- a/forge-script-lang/src/parser/ast.rs +++ b/forge-script-lang/src/parser/ast.rs @@ -101,6 +101,17 @@ pub enum BinaryOp { Divide, Modulo, Equals, + BoolAnd, + BoolOr, +} + +impl BinaryOp { + pub fn short_circuits(&self) -> bool { + match self { + Self::BoolAnd | Self::BoolOr => true, + _ => false, + } + } } impl Display for BinaryOp { @@ -115,6 +126,8 @@ impl Display for BinaryOp { BinaryOp::Divide => "/", BinaryOp::Modulo => "%", BinaryOp::Equals => "==", + BinaryOp::BoolAnd => "&&", + BinaryOp::BoolOr => "||", } ) } @@ -167,8 +180,14 @@ pub enum ValueExpression { Identifier(Identifier), FunctionCall(FunctionCall), DeclareFunction(DeclareFunction), + Typeof(TypeofValue), } +#[derive(Clone)] +#[cfg_attr(feature = "debug-ast", derive(Debug))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct TypeofValue(pub Box<ValueExpression>); + #[derive(Clone)] #[cfg_attr(feature = "debug-ast", derive(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 1f1a12d36cae19e5a190a3ac185897c72ff8b1ab..d3af36bcfb2a0a9bb21f711d3c65ca5dbb2cb79b 100644 --- a/forge-script-lang/src/parser/grammar.rs +++ b/forge-script-lang/src/parser/grammar.rs @@ -1,7 +1,7 @@ use crate::parser::TokenSlice; peg::parser! { - grammar script_parser<'a>() for TokenSlice<'a> { + grammar forge_parser<'a>() for TokenSlice<'a> { use crate::parser::ast::*; use crate::runtime::numbers::Number; use crate::lexer::{ScriptToken, ScriptTokenType}; @@ -9,38 +9,30 @@ peg::parser! { pub rule program() -> Program = ex:expression_list() eof() { Program(ex) } - rule expression_list() -> ExpressionList - = e:(expression() ++ ";") term:";"? { ExpressionList { expressions: e, is_void: term.is_some() } } - - // rule statement() -> Expression - // Include conditional here separately from expression to allow "if" without semi - // = e:conditional() { Expression::Value(ValueExpression::ConditionalBlock(e)) } - // / e:condition_loop() { Expression::Void(VoidExpression::ConditionLoop(e)) } - // / e:expression() ";" { e } - pub rule expression() -> Expression = ex:value_expression() { Expression::Value(ex) } / ex:void_expression() { Expression::Void(ex) } rule void_expression() -> VoidExpression = ex:print() { VoidExpression::Print(ex) } - / "import" "{" items:identifier_list() "}" "from" source:raw_string() { VoidExpression::Import(Import { source, items }) } + / "import" "{" items:identifier_list() "}" "from" source:string_value() { VoidExpression::Import(Import { source, items }) } / "export" "{" items:identifier_list() "}" { VoidExpression::Export(Export { items }) } / e:condition_loop() { VoidExpression::ConditionLoop(e) } - #[cache_left_rec] + #[cache_left_rec] rule value_expression() -> ValueExpression - = co:conditional() { ValueExpression::ConditionalBlock(co) } + = co:conditional_statement() { ValueExpression::ConditionalBlock(co) } + / t:type_of() { ValueExpression::Typeof(t) } / decl:declare_variable() { ValueExpression::DeclareIdentifier(decl) } / decl:declare_function() { ValueExpression::DeclareFunction(decl) } - / name:bare_identifier() "(" params:param_list()? ")" + / name:simple_identifier() "(" params:param_list()? ")" { ValueExpression::FunctionCall(FunctionCall { name, params: params.unwrap_or_default() }) } / left:value_expression() op:binary_operator() right:value_expression() { 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) } + / ident:simple_identifier() !"(" { ValueExpression::Identifier(ident) } / li:literal() { ValueExpression::Literal(li) } rule grouped() -> ValueExpression @@ -50,39 +42,45 @@ peg::parser! { rule print() -> Print = "print" ex:value_expression() { ex.into() } - rule declare_variable() -> DeclareIdent - = "let" ex:declare_identifier() { ex } + rule type_of() -> TypeofValue + = "typeof" ex:value_expression() { TypeofValue(Box::new(ex)) } - rule declare_identifier() -> DeclareIdent + rule declare_function() -> DeclareFunction + = "fn" ident:simple_identifier() "(" params:(function_param() ** ",") ")" body:block() + { DeclareFunction { ident, params, body } } + + rule function_param() -> DeclareIdent = assign:assignment() { DeclareIdent::WithValue(assign) } - / ident:bare_identifier() { DeclareIdent::WithoutValue(ident) } + / ident:simple_identifier() { DeclareIdent::WithoutValue(ident) } - rule declare_function() -> DeclareFunction - = "fn" ident:bare_identifier() "(" params:(declare_identifier() ** ",") ")" "{" block:expression_list()? "}" - { DeclareFunction { ident, params, body: block.unwrap_or_default() } } + rule assignment() -> Assignment + = ident:simple_identifier() "=" ex:value_expression() { Assignment { ident, value: Box::new(ex) } } + + rule declare_variable() -> DeclareIdent + = "let" assign:assignment() { DeclareIdent::WithValue(assign) } + / "let" ident:simple_identifier() { DeclareIdent::WithoutValue(ident) } rule condition_loop() -> ConditionalLoop - = "while" guard:value_expression() "{" block:expression_list()? "}" - { ConditionalLoop { block: GuardedBlock { guard: Box::new(guard), block: block.unwrap_or_default() }, fallback: None } } - / "while" guard:value_expression() "{" block:expression_list()? "}" "else" "{" fallback:expression_list()? "}" - { ConditionalLoop { block: GuardedBlock { guard: Box::new(guard), block: block.unwrap_or_default() }, fallback } } - - rule conditional() -> Conditional - // = bl:guarded_block() { Conditional { fallback: None, blocks: vec![bl] } } - = blocks:(guarded_block() ++ "else") "else" "{" fallback:expression_list() "}" { - Conditional { - blocks, - fallback: Some(fallback) - } - } - / blocks:(guarded_block() ++ "else") { Conditional { fallback: None, blocks, } } - - rule guarded_block() -> GuardedBlock + = "while" guard:value_expression() block:block() + { ConditionalLoop { block: GuardedBlock { guard: Box::new(guard), block }, fallback: None } } + / "while" guard:value_expression() block:block() "else" fallback:block() + { ConditionalLoop { block: GuardedBlock { guard: Box::new(guard), block }, fallback: Some(fallback) } } + + rule conditional_statement() -> Conditional + = blocks:(conditional_block() ++ "else") "else" fallback:block() + { Conditional { blocks, fallback: Some(fallback) } } + / blocks:(conditional_block() ++ "else") + { Conditional { blocks, fallback: None } } + + rule conditional_block() -> GuardedBlock = "if" guard:value_expression() "{" block:expression_list()? "}" { GuardedBlock { block: block.unwrap_or_default(), guard: Box::new(guard) } } - rule assignment() -> Assignment - = ident:bare_identifier() "=" expr:value_expression() { Assignment { ident, value: Box::new(expr) } } + rule block() -> ExpressionList + = "{" ex:expression_list() "}" { ex } + + rule expression_list() -> ExpressionList + = ex:(expression() ** ";") term:";"? { ExpressionList { expressions: ex, is_void: term.is_some() } } rule binary_operator() -> BinaryOp = "+" { BinaryOp::Add } @@ -90,6 +88,9 @@ peg::parser! { / "*" { BinaryOp::Multiply } / "/" { BinaryOp::Divide } / "%" { BinaryOp::Modulo } + / "==" { BinaryOp::Equals } + / "&&" { BinaryOp::BoolAnd } + / "||" { BinaryOp::BoolOr } rule unary_operator() -> UnaryOp = "!" { UnaryOp::Not } @@ -99,16 +100,16 @@ peg::parser! { = identifier() ++ "," rule param_list() -> ParameterList - = ids:value_expression() ++ "," + = value_expression() ++ "," rule identifier() -> IdentifierNode = id:alias_identifier() { IdentifierNode::Alias(id) } - / id:bare_identifier() { IdentifierNode::Direct(id) } + / id:simple_identifier() { IdentifierNode::Direct(id) } rule alias_identifier() -> IdentifierAlias - = base:bare_identifier() "as" alias:bare_identifier() { IdentifierAlias(base.0, alias.0) } + = base:simple_identifier() "as" alias:simple_identifier() { IdentifierAlias(base.0, alias.0) } - rule bare_identifier() -> Identifier + rule simple_identifier() -> Identifier = [ScriptToken { token_type: ScriptTokenType::Identifier(vl), .. }] { Identifier(String::from(*vl)) } rule literal() -> LiteralNode @@ -120,7 +121,7 @@ peg::parser! { / [ScriptToken { token_type: ScriptTokenType::Integer(vl), .. }] { LiteralNode::Number(Number::Integer(*vl)) } / [ScriptToken { token_type: ScriptTokenType::Float(vl), .. }] { LiteralNode::Number(Number::Float(*vl)) } - rule raw_string() -> String + rule string_value() -> String = [ScriptToken { token_type: ScriptTokenType::String(vl), .. }] { String::from(*vl) } / [ScriptToken { token_type: ScriptTokenType::OwnedString(vl), .. }] { vl.clone() } @@ -128,4 +129,4 @@ peg::parser! { } } -pub use script_parser::{expression, program}; +pub use forge_parser::{expression, program}; diff --git a/forge-script-lang/src/parser/test_suite.rs b/forge-script-lang/src/parser/test_suite.rs index fc217e21dc9761e0f1ac2d2c1a85e343acd4c603..9a7f4970b357bb3b0090e97e0cc10c27ef710991 100644 --- a/forge-script-lang/src/parser/test_suite.rs +++ b/forge-script-lang/src/parser/test_suite.rs @@ -73,3 +73,10 @@ fn declare_function() { parse_program("fn add(first, second) { first + second }") .expect("Failed basic function with body"); } + +#[test] +fn type_check() { + parse_program("typeof 123").expect("Failed to parse typeof literal"); + parse_program("typeof foo").expect("Failed to parse typeof identifier"); + parse_program("typeof (\"Str \" + 123)").expect("Failed to parse typeof grouped expression"); +} diff --git a/forge-script-lang/src/runtime/executor/printer.rs b/forge-script-lang/src/runtime/executor/printer.rs index 31b5b3b97d7a9d3b6617929377216fe6d6220a68..2d8d996434eca767274881002727ae1788459b3c 100644 --- a/forge-script-lang/src/runtime/executor/printer.rs +++ b/forge-script-lang/src/runtime/executor/printer.rs @@ -219,6 +219,10 @@ impl Visitor for TreePrinter { self.decrement(); self.writeln("}"); } + ValueExpression::Typeof(type_of) => { + self.write("typeof "); + self.evaluate_value_expression(type_of.0.as_ref()); + } } Ok(()) diff --git a/forge-script-lang/src/runtime/executor/simple.rs b/forge-script-lang/src/runtime/executor/simple.rs index 12a04b3dbfe43ec3d1e30acb88e57d249d40d6df..46fb5547c25a5ec63e2b479e980a4335f426215b 100644 --- a/forge-script-lang/src/runtime/executor/simple.rs +++ b/forge-script-lang/src/runtime/executor/simple.rs @@ -136,16 +136,29 @@ impl Visitor for SimpleExecutor { } } 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()), + if operator.short_circuits() { + let lhs = self.evaluate_value_expression(lhs.as_ref())?; + match (operator, lhs.as_bool()) { + (BinaryOp::BoolOr, true) => Ok(true.into()), + (BinaryOp::BoolAnd, false) => Ok(false.into()), + (BinaryOp::BoolOr, false) | (BinaryOp::BoolAnd, true) => Ok(self + .evaluate_value_expression(rhs.as_ref())? + .as_bool() + .into()), + _ => Ok(ForgeValue::Null), + } + } else { + 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()), + BinaryOp::BoolOr | BinaryOp::BoolAnd => Ok(ForgeValue::Null), + } } } ValueExpression::Grouped(group) => self.evaluate_value_expression(group.inner.as_ref()), @@ -191,6 +204,10 @@ impl Visitor for SimpleExecutor { ValueExpression::DeclareFunction(_) => { Err(RuntimeError::Unsupported("DeclareFunction")) } + ValueExpression::Typeof(type_of) => Ok(ForgeValue::from( + self.evaluate_value_expression(type_of.0.as_ref())? + .type_name(), + )), } } @@ -258,6 +275,7 @@ mod interpreter_test { #[test] fn print() { let print_4 = parse_program("print 2 + 2").expect("Failed to parse"); + let mut vm = SimpleExecutor::default(); assert_eq!(vm.evaluate_program(&print_4), Ok(ForgeValue::Null)); } @@ -285,4 +303,43 @@ mod interpreter_test { assert_eq!(vm.evaluate_program(&use_vars), Ok(ForgeValue::from(246))); assert_eq!(vm.get_variable("splop"), Some(&ForgeValue::from(246))); } + + #[test] + fn conditional_blocks() { + let tests = [ + ("let value = true; if value { 123 } else { 456 }", Ok(ForgeValue::from(123))), + ("let value = true; if !value { 123 } else { 456 }", Ok(ForgeValue::from(456))), + ("let value = 2; if value == 1 { 123 } else if value == 2 { 456 } else { \"Nothing\" }", Ok(ForgeValue::from(456))), + ]; + + for (prog, expected) in tests { + let ast = parse_program(prog).expect("Failed to parse"); + let mut vm = SimpleExecutor::default(); + assert_eq!(vm.evaluate_program(&ast), expected); + } + } + + #[test] + /// When the first term in a binary op has already decided the outcome, don't evaluate the following terms + fn short_circuit() { + let prog = parse_program("if true || let bar = 123 { 9001 }").expect("Failed to parse"); + let mut vm = SimpleExecutor::default(); + assert_eq!(vm.evaluate_program(&prog), Ok(ForgeValue::from(9001))); + assert!(vm.get_variable("bar").is_none()); + } + + #[test] + fn variable_type() { + let tests = [ + ("typeof 123", Ok(ForgeValue::from("number"))), + (r#"typeof "some string""#, Ok(ForgeValue::from("string"))), + ("typeof false", Ok(ForgeValue::from("boolean"))), + ]; + + for (prog, expected) in tests { + let ast = parse_program(prog).expect("Failed to parse"); + let mut vm = SimpleExecutor::default(); + assert_eq!(vm.evaluate_program(&ast), expected); + } + } } diff --git a/forge-script-lang/src/runtime/value.rs b/forge-script-lang/src/runtime/value.rs index 697567fa18eb04de9fc8ca08ae5f1e02ec0500bf..3619e10609bb7a55e0e46956096e9332affbc6f8 100644 --- a/forge-script-lang/src/runtime/value.rs +++ b/forge-script-lang/src/runtime/value.rs @@ -32,6 +32,16 @@ impl ForgeValue { ForgeValue::Null } + pub fn type_name(&self) -> &'static str { + match self { + ForgeValue::Number(_) => "number", + ForgeValue::Boolean(_) => "boolean", + ForgeValue::String(_) => "string", + ForgeValue::List(_) => "list", + ForgeValue::Null => "null", + } + } + pub fn invert(&self) -> Self { match self { ForgeValue::Number(num) => match num {