diff --git a/Cargo.toml b/Cargo.toml index f49b4fa..f485f88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,10 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["shulkerbox"] +default = ["lua", "shulkerbox"] shulkerbox = ["dep:shulkerbox"] serde = ["dep:serde"] +lua = ["dep:mlua"] [dependencies] chksum-md5 = "0.0.0" @@ -16,6 +17,7 @@ colored = "2.1.0" derive_more = { version = "0.99.17", default-features = false, features = ["deref", "from", "deref_mut"] } enum-as-inner = "0.6.0" getset = "0.1.2" +mlua = { version = "0.9.7", features = ["luau"], optional = true } serde = { version = "1.0.197", features = ["derive", "rc"], optional = true } shulkerbox = { path = "../shulkerbox", optional = true} strum = { version = "0.26.2", features = ["derive"] } diff --git a/grammar.md b/grammar.md index ad442f5..0bd3974 100644 --- a/grammar.md +++ b/grammar.md @@ -59,36 +59,77 @@ Conditional: ; ``` -### ParenthizedCondition +### Condition +```ebnf +Condition: + PrimaryCondition + BinaryCondition + ; +``` + +#### PrimaryCondition +```ebnf +PrimaryCondition: + ConditionalPrefix + | ParenthesizedCondition + | StringLiteral + ; +``` + +#### ConditionalPrefix +```ebnf +ConditionalPrefix: + ConditionalPrefixOperator PrimaryCondition + ; +``` + +#### ConditionalPrefixOperator +``` ebnf +ConditionalPrefixOperator: '!'; +``` + +#### BinaryCondition +``` ebnf +BinaryCondition: + Condition ConditionalBinaryOperator Condition + ; +``` + +#### ConditionalBinaryOperator +``` ebnf +ConditionalBinaryOperator: + '&&' + | '||' + ; +``` + +#### ParenthizedCondition ```ebnf ParenthizedCondition: '(' Condition ')' ; ``` -### Condition -```ebnf -Condition: - StringLiteral -``` ### Grouping ``` ebnf Grouping: 'group' Block -; + ; ``` ### Expression ```ebnf Expression: Primary + ; ``` ### Primary ``` ebnf Primary: FunctionCall + ; ``` ### FunctionCall @@ -96,4 +137,11 @@ Primary: FunctionCall: Identifier '(' (Expression (',' Expression)*)? ')' ; +``` + +### LuaCode +```ebnf +LuaCode: + 'lua' '(' (Expression (',' Expression)*)? ')' '{' (.*?)* '}' + ; ``` \ No newline at end of file diff --git a/src/base/source_file.rs b/src/base/source_file.rs index a1fec39..38a3871 100644 --- a/src/base/source_file.rs +++ b/src/base/source_file.rs @@ -51,6 +51,12 @@ impl SourceFile { &self.content } + /// Get the path of the source file. + #[must_use] + pub fn path(&self) -> &Path { + &self.path + } + /// Get the line of the source file at the given line number. /// /// Numbering starts at 1. @@ -95,8 +101,6 @@ impl SourceFile { #[must_use] pub fn get_location(&self, byte_index: usize) -> Option { if self.content.is_char_boundary(byte_index) { - None - } else { // get the line number by binary searching the line ranges let line = self .lines @@ -125,6 +129,8 @@ impl SourceFile { line: line + 1, column, }) + } else { + None } } } diff --git a/src/lexical/token.rs b/src/lexical/token.rs index 8279a7e..95164c0 100644 --- a/src/lexical/token.rs +++ b/src/lexical/token.rs @@ -23,6 +23,7 @@ pub enum KeywordKind { Else, Group, Run, + Lua, } impl ToString for KeywordKind { @@ -66,6 +67,7 @@ impl KeywordKind { Self::Else => "else", Self::Group => "group", Self::Run => "run", + Self::Lua => "lua", } } } diff --git a/src/lexical/token_stream.rs b/src/lexical/token_stream.rs index e8d99f5..cb5189b 100644 --- a/src/lexical/token_stream.rs +++ b/src/lexical/token_stream.rs @@ -3,8 +3,12 @@ use std::{fmt::Debug, sync::Arc}; use derive_more::{Deref, From}; +use enum_as_inner::EnumAsInner; -use crate::base::{source_file::SourceFile, Handler}; +use crate::base::{ + source_file::{SourceElement, SourceFile, Span}, + Handler, +}; use super::{ error::{self, UndelimitedDelimiter}, @@ -164,13 +168,26 @@ impl TokenStream { /// Is an enumeration of either a [`Token`] or a [`Delimited`]. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, From)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, From, EnumAsInner)] #[allow(missing_docs)] pub enum TokenTree { Token(Token), Delimited(Delimited), } +impl SourceElement for TokenTree { + fn span(&self) -> Span { + match self { + Self::Token(token) => token.span().to_owned(), + Self::Delimited(delimited) => delimited + .open + .span() + .join(&delimited.close.span) + .expect("Invalid delimited span"), + } + } +} + /// Is an enumeration of the different types of delimiters in the [`Delimited`]. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/src/syntax/parser.rs b/src/syntax/parser.rs index 9f3ef93..976aff4 100644 --- a/src/syntax/parser.rs +++ b/src/syntax/parser.rs @@ -247,6 +247,12 @@ impl<'a> Frame<'a> { self.get_reading(self.token_provider.token_stream().get(self.current_index)) } + /// Returns a raw [`Token`] pointing by the `current_index` of the [`Frame`]. + #[must_use] + pub fn peek_raw(&self) -> Option<&TokenTree> { + self.token_provider.token_stream().get(self.current_index) + } + /// Returns the next significant [`Token`] after the `current_index` of the [`Frame`]. #[must_use] pub fn peek_significant(&self) -> Reading { diff --git a/src/syntax/syntax_tree/condition.rs b/src/syntax/syntax_tree/condition.rs index 0f4c035..b0d7dd0 100644 --- a/src/syntax/syntax_tree/condition.rs +++ b/src/syntax/syntax_tree/condition.rs @@ -23,9 +23,9 @@ use crate::{ /// Syntax Synopsis: /// /// ``` ebnf -/// Expression: -/// Prefix -/// | Parenthesized +/// PrimaryCondition: +/// ConditionalPrefix +/// | ParenthesizedCondition /// | StringLiteral /// ``` #[allow(missing_docs)] @@ -88,7 +88,7 @@ impl BinaryCondition { /// Syntax Synopsis: /// /// ``` ebnf -/// BinaryOperator: +/// ConditionalBinaryOperator: /// '&&' /// | '||' /// ; @@ -165,7 +165,7 @@ impl SourceElement for ParenthesizedCondition { /// Syntax Synopsis: /// /// ``` ebnf -/// PrefixOperator: '!'; +/// ConditionalPrefixOperator: '!'; /// ``` #[allow(missing_docs)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -185,8 +185,8 @@ impl SourceElement for ConditionalPrefixOperator { /// Syntax Synopsis: /// /// ```ebnf -/// Prefix: -/// ConditionalPrefixOperator StringLiteral +/// ConditionalPrefix: +/// ConditionalPrefixOperator PrimaryCondition /// ; /// ``` #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -216,7 +216,10 @@ impl ConditionalPrefix { /// Syntax Synopsis: /// /// ``` ebnf -/// Condition: PrimaryCondition; +/// Condition: +/// PrimaryCondition +/// | BinaryCondition +/// ; /// ``` #[allow(missing_docs)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/src/syntax/syntax_tree/expression.rs b/src/syntax/syntax_tree/expression.rs index 350ca54..6676ded 100644 --- a/src/syntax/syntax_tree/expression.rs +++ b/src/syntax/syntax_tree/expression.rs @@ -9,7 +9,7 @@ use crate::{ Handler, }, lexical::{ - token::{Identifier, Punctuation, StringLiteral, Token}, + token::{Identifier, Keyword, KeywordKind, Punctuation, StringLiteral, Token}, token_stream::Delimiter, }, syntax::{ @@ -47,12 +47,13 @@ impl SourceElement for Expression { /// Primary: /// FunctionCall /// ``` -#[allow(missing_docs)] +#[allow(missing_docs, clippy::large_enum_variant)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, EnumAsInner)] pub enum Primary { FunctionCall(FunctionCall), StringLiteral(StringLiteral), + Lua(LuaCode), } impl SourceElement for Primary { @@ -60,6 +61,7 @@ impl SourceElement for Primary { match self { Self::FunctionCall(function_call) => function_call.span(), Self::StringLiteral(string_literal) => string_literal.span(), + Self::Lua(lua_code) => lua_code.span(), } } } @@ -97,6 +99,60 @@ impl SourceElement for FunctionCall { } } +/// Syntax Synopsis: +/// +/// ```ebnf +/// LuaCode: +/// 'lua' '(' (Expression (',' Expression)*)? ')' '{' (.*?)* '}' +/// ``` +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Getters)] +pub struct LuaCode { + /// The `lua` keyword. + #[get = "pub"] + lua_keyword: Keyword, + /// The left parenthesis of the lua code. + #[get = "pub"] + left_parenthesis: Punctuation, + /// The arguments of the lua code. + #[get = "pub"] + variables: Option>, + /// The right parenthesis of the lua code. + #[get = "pub"] + right_parenthesis: Punctuation, + /// The left brace of the lua code. + #[get = "pub"] + left_brace: Punctuation, + /// The lua code contents. + #[get = "pub"] + code: String, + /// The right brace of the lua code. + #[get = "pub"] + right_brace: Punctuation, +} + +impl SourceElement for LuaCode { + fn span(&self) -> Span { + self.lua_keyword + .span() + .join(&self.right_brace.span) + .expect("Invalid lua code span") + } +} + +impl LuaCode { + /// Dissolves the [`LuaCode`] into its components. + #[must_use] + pub fn dissolve(self) -> (Keyword, Punctuation, String, Punctuation) { + ( + self.lua_keyword, + self.left_brace, + self.code, + self.right_brace, + ) + } +} + impl<'a> Parser<'a> { /// Parses an [`Expression`] pub fn parse_expression(&mut self, handler: &impl Handler) -> Option { @@ -141,6 +197,65 @@ impl<'a> Parser<'a> { Some(Primary::StringLiteral(literal)) } + // lua code expression + Reading::Atomic(Token::Keyword(lua_keyword)) + if lua_keyword.keyword == KeywordKind::Lua => + { + // eat the lua keyword + self.forward(); + + // parse the variable list + let variables = self.parse_enclosed_list( + Delimiter::Parenthesis, + ',', + |parser| match parser.next_significant_token() { + Reading::Atomic(Token::Identifier(identifier)) => { + parser.forward(); + Some(identifier) + } + _ => None, + }, + handler, + )?; + + self.stop_at_significant(); + + let tree = self.step_into( + Delimiter::Brace, + |parser| { + let first = parser.next_token(); + let mut last = parser.next_token(); + + while !parser.is_end() { + last = parser.next_token(); + } + + let combined = first + .into_token() + .and_then(|first| { + first.span().join(&last.into_token().map_or_else( + || first.span().to_owned(), + |last| last.span().to_owned(), + )) + }) + .expect("Invalid lua code span"); + + Some(combined.str().trim().to_owned()) + }, + handler, + )?; + + Some(Primary::Lua(LuaCode { + lua_keyword, + left_parenthesis: variables.open, + variables: variables.list, + right_parenthesis: variables.close, + left_brace: tree.open, + code: tree.tree?, + right_brace: tree.close, + })) + } + unexpected => { // make progress self.forward(); diff --git a/src/transpile/error.rs b/src/transpile/error.rs index 6ccaa45..c6a888a 100644 --- a/src/transpile/error.rs +++ b/src/transpile/error.rs @@ -10,6 +10,10 @@ pub enum TranspileError { MissingFunctionDeclaration(String), #[error("Unexpected expression: {}", .0.span().str())] UnexpectedExpression(Expression), + #[error("Lua code evaluation is disabled.")] + LuaDisabled, + #[error("Lua runtime error: {}", .0)] + LuaRuntimeError(String), } /// The result of a transpilation operation. diff --git a/src/transpile/lua.rs b/src/transpile/lua.rs new file mode 100644 index 0000000..264d974 --- /dev/null +++ b/src/transpile/lua.rs @@ -0,0 +1,95 @@ +//! Executes the Lua code and returns the resulting command. + +#[cfg(feature = "lua")] +mod enabled { + use mlua::Lua; + + use crate::{ + base::{source_file::SourceElement, Handler}, + syntax::syntax_tree::expression::LuaCode, + transpile::error::{TranspileError, TranspileResult}, + }; + + impl LuaCode { + /// Evaluates the Lua code and returns the resulting command. + /// + /// # Errors + /// - If Lua code evaluation is disabled. + pub fn eval_string( + &self, + handler: &impl Handler, + ) -> TranspileResult { + let lua = Lua::new(); + + let name = { + let span = self.span(); + let file = span.source_file(); + let path = file.path(); + + let start = span.start_location(); + let end = span.end_location().unwrap_or_else(|| { + let content_size = file.content().len(); + file.get_location(content_size - 1) + .expect("Failed to get location") + }); + + format!( + "{}:{}:{}-{}:{}", + path.display(), + start.line, + start.column, + end.line, + end.column + ) + }; + + let lua_result = lua + .load(self.code()) + .set_name(name) + .eval::() + .map_err(|err| { + let err = TranspileError::from(err); + handler.receive(err.clone()); + err + })?; + + Ok(lua_result) + } + } + + impl From for TranspileError { + fn from(value: mlua::Error) -> Self { + let string = value.to_string(); + Self::LuaRuntimeError( + string + .strip_prefix("runtime error: ") + .unwrap_or(&string) + .to_string(), + ) + } + } +} + +#[cfg(not(feature = "lua"))] +mod disabled { + use crate::{ + base::Handler, + syntax::syntax_tree::expression::LuaCode, + transpile::error::{TranspileError, TranspileResult}, + }; + + impl LuaCode { + /// Will always return an error because Lua code evaluation is disabled. + /// Enable the feature `lua` to enable Lua code evaluation. + /// + /// # Errors + /// - If Lua code evaluation is disabled. + pub fn eval_string( + &self, + handler: &impl Handler, + ) -> TranspileResult { + handler.receive(TranspileError::LuaDisabled); + Err(TranspileError::LuaDisabled) + } + } +} diff --git a/src/transpile/mod.rs b/src/transpile/mod.rs index 23b4849..478089d 100644 --- a/src/transpile/mod.rs +++ b/src/transpile/mod.rs @@ -4,5 +4,7 @@ #[cfg(feature = "shulkerbox")] pub mod conversions; pub mod error; +#[doc(hidden)] +pub mod lua; #[cfg(feature = "shulkerbox")] pub mod transpiler; diff --git a/src/transpile/transpiler.rs b/src/transpile/transpiler.rs index 8331c40..9c62700 100644 --- a/src/transpile/transpiler.rs +++ b/src/transpile/transpiler.rs @@ -233,6 +233,9 @@ impl Transpiler { Expression::Primary(Primary::StringLiteral(string)) => { Ok(Some(Command::Raw(string.str_content().to_string()))) } + Expression::Primary(Primary::Lua(code)) => { + Ok(Some(Command::Raw(code.eval_string(handler)?))) + } }, Statement::Block(_) => { unreachable!("Only literal commands are allowed in functions at this time.")