Add Lua support to transpiler

This commit is contained in:
Moritz Hölting 2024-04-06 17:23:20 +02:00
parent 9f8b31e2aa
commit 01040964af
12 changed files with 325 additions and 22 deletions

View File

@ -6,9 +6,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
default = ["shulkerbox"] default = ["lua", "shulkerbox"]
shulkerbox = ["dep:shulkerbox"] shulkerbox = ["dep:shulkerbox"]
serde = ["dep:serde"] serde = ["dep:serde"]
lua = ["dep:mlua"]
[dependencies] [dependencies]
chksum-md5 = "0.0.0" 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"] } derive_more = { version = "0.99.17", default-features = false, features = ["deref", "from", "deref_mut"] }
enum-as-inner = "0.6.0" enum-as-inner = "0.6.0"
getset = "0.1.2" getset = "0.1.2"
mlua = { version = "0.9.7", features = ["luau"], optional = true }
serde = { version = "1.0.197", features = ["derive", "rc"], optional = true } serde = { version = "1.0.197", features = ["derive", "rc"], optional = true }
shulkerbox = { path = "../shulkerbox", optional = true} shulkerbox = { path = "../shulkerbox", optional = true}
strum = { version = "0.26.2", features = ["derive"] } strum = { version = "0.26.2", features = ["derive"] }

View File

@ -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 ```ebnf
ParenthizedCondition: ParenthizedCondition:
'(' Condition ')' '(' Condition ')'
; ;
``` ```
### Condition
```ebnf
Condition:
StringLiteral
```
### Grouping ### Grouping
``` ebnf ``` ebnf
Grouping: Grouping:
'group' Block 'group' Block
; ;
``` ```
### Expression ### Expression
```ebnf ```ebnf
Expression: Expression:
Primary Primary
;
``` ```
### Primary ### Primary
``` ebnf ``` ebnf
Primary: Primary:
FunctionCall FunctionCall
;
``` ```
### FunctionCall ### FunctionCall
@ -96,4 +137,11 @@ Primary:
FunctionCall: FunctionCall:
Identifier '(' (Expression (',' Expression)*)? ')' Identifier '(' (Expression (',' Expression)*)? ')'
; ;
```
### LuaCode
```ebnf
LuaCode:
'lua' '(' (Expression (',' Expression)*)? ')' '{' (.*?)* '}'
;
``` ```

View File

@ -51,6 +51,12 @@ impl SourceFile {
&self.content &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. /// Get the line of the source file at the given line number.
/// ///
/// Numbering starts at 1. /// Numbering starts at 1.
@ -95,8 +101,6 @@ impl SourceFile {
#[must_use] #[must_use]
pub fn get_location(&self, byte_index: usize) -> Option<Location> { pub fn get_location(&self, byte_index: usize) -> Option<Location> {
if self.content.is_char_boundary(byte_index) { if self.content.is_char_boundary(byte_index) {
None
} else {
// get the line number by binary searching the line ranges // get the line number by binary searching the line ranges
let line = self let line = self
.lines .lines
@ -125,6 +129,8 @@ impl SourceFile {
line: line + 1, line: line + 1,
column, column,
}) })
} else {
None
} }
} }
} }

View File

@ -23,6 +23,7 @@ pub enum KeywordKind {
Else, Else,
Group, Group,
Run, Run,
Lua,
} }
impl ToString for KeywordKind { impl ToString for KeywordKind {
@ -66,6 +67,7 @@ impl KeywordKind {
Self::Else => "else", Self::Else => "else",
Self::Group => "group", Self::Group => "group",
Self::Run => "run", Self::Run => "run",
Self::Lua => "lua",
} }
} }
} }

View File

@ -3,8 +3,12 @@
use std::{fmt::Debug, sync::Arc}; use std::{fmt::Debug, sync::Arc};
use derive_more::{Deref, From}; 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::{ use super::{
error::{self, UndelimitedDelimiter}, error::{self, UndelimitedDelimiter},
@ -164,13 +168,26 @@ impl TokenStream {
/// Is an enumeration of either a [`Token`] or a [`Delimited`]. /// Is an enumeration of either a [`Token`] or a [`Delimited`].
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[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)] #[allow(missing_docs)]
pub enum TokenTree { pub enum TokenTree {
Token(Token), Token(Token),
Delimited(Delimited), 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`]. /// Is an enumeration of the different types of delimiters in the [`Delimited`].
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]

View File

@ -247,6 +247,12 @@ impl<'a> Frame<'a> {
self.get_reading(self.token_provider.token_stream().get(self.current_index)) 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`]. /// Returns the next significant [`Token`] after the `current_index` of the [`Frame`].
#[must_use] #[must_use]
pub fn peek_significant(&self) -> Reading { pub fn peek_significant(&self) -> Reading {

View File

@ -23,9 +23,9 @@ use crate::{
/// Syntax Synopsis: /// Syntax Synopsis:
/// ///
/// ``` ebnf /// ``` ebnf
/// Expression: /// PrimaryCondition:
/// Prefix /// ConditionalPrefix
/// | Parenthesized /// | ParenthesizedCondition
/// | StringLiteral /// | StringLiteral
/// ``` /// ```
#[allow(missing_docs)] #[allow(missing_docs)]
@ -88,7 +88,7 @@ impl BinaryCondition {
/// Syntax Synopsis: /// Syntax Synopsis:
/// ///
/// ``` ebnf /// ``` ebnf
/// BinaryOperator: /// ConditionalBinaryOperator:
/// '&&' /// '&&'
/// | '||' /// | '||'
/// ; /// ;
@ -165,7 +165,7 @@ impl SourceElement for ParenthesizedCondition {
/// Syntax Synopsis: /// Syntax Synopsis:
/// ///
/// ``` ebnf /// ``` ebnf
/// PrefixOperator: '!'; /// ConditionalPrefixOperator: '!';
/// ``` /// ```
#[allow(missing_docs)] #[allow(missing_docs)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -185,8 +185,8 @@ impl SourceElement for ConditionalPrefixOperator {
/// Syntax Synopsis: /// Syntax Synopsis:
/// ///
/// ```ebnf /// ```ebnf
/// Prefix: /// ConditionalPrefix:
/// ConditionalPrefixOperator StringLiteral /// ConditionalPrefixOperator PrimaryCondition
/// ; /// ;
/// ``` /// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -216,7 +216,10 @@ impl ConditionalPrefix {
/// Syntax Synopsis: /// Syntax Synopsis:
/// ///
/// ``` ebnf /// ``` ebnf
/// Condition: PrimaryCondition; /// Condition:
/// PrimaryCondition
/// | BinaryCondition
/// ;
/// ``` /// ```
#[allow(missing_docs)] #[allow(missing_docs)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]

View File

@ -9,7 +9,7 @@ use crate::{
Handler, Handler,
}, },
lexical::{ lexical::{
token::{Identifier, Punctuation, StringLiteral, Token}, token::{Identifier, Keyword, KeywordKind, Punctuation, StringLiteral, Token},
token_stream::Delimiter, token_stream::Delimiter,
}, },
syntax::{ syntax::{
@ -47,12 +47,13 @@ impl SourceElement for Expression {
/// Primary: /// Primary:
/// FunctionCall /// FunctionCall
/// ``` /// ```
#[allow(missing_docs)] #[allow(missing_docs, clippy::large_enum_variant)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, EnumAsInner)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, EnumAsInner)]
pub enum Primary { pub enum Primary {
FunctionCall(FunctionCall), FunctionCall(FunctionCall),
StringLiteral(StringLiteral), StringLiteral(StringLiteral),
Lua(LuaCode),
} }
impl SourceElement for Primary { impl SourceElement for Primary {
@ -60,6 +61,7 @@ impl SourceElement for Primary {
match self { match self {
Self::FunctionCall(function_call) => function_call.span(), Self::FunctionCall(function_call) => function_call.span(),
Self::StringLiteral(string_literal) => string_literal.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<ConnectedList<Identifier, Punctuation>>,
/// 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> { impl<'a> Parser<'a> {
/// Parses an [`Expression`] /// Parses an [`Expression`]
pub fn parse_expression(&mut self, handler: &impl Handler<Error>) -> Option<Expression> { pub fn parse_expression(&mut self, handler: &impl Handler<Error>) -> Option<Expression> {
@ -141,6 +197,65 @@ impl<'a> Parser<'a> {
Some(Primary::StringLiteral(literal)) 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 => { unexpected => {
// make progress // make progress
self.forward(); self.forward();

View File

@ -10,6 +10,10 @@ pub enum TranspileError {
MissingFunctionDeclaration(String), MissingFunctionDeclaration(String),
#[error("Unexpected expression: {}", .0.span().str())] #[error("Unexpected expression: {}", .0.span().str())]
UnexpectedExpression(Expression), UnexpectedExpression(Expression),
#[error("Lua code evaluation is disabled.")]
LuaDisabled,
#[error("Lua runtime error: {}", .0)]
LuaRuntimeError(String),
} }
/// The result of a transpilation operation. /// The result of a transpilation operation.

95
src/transpile/lua.rs Normal file
View File

@ -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<TranspileError>,
) -> TranspileResult<String> {
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::<String>()
.map_err(|err| {
let err = TranspileError::from(err);
handler.receive(err.clone());
err
})?;
Ok(lua_result)
}
}
impl From<mlua::Error> 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<TranspileError>,
) -> TranspileResult<String> {
handler.receive(TranspileError::LuaDisabled);
Err(TranspileError::LuaDisabled)
}
}
}

View File

@ -4,5 +4,7 @@
#[cfg(feature = "shulkerbox")] #[cfg(feature = "shulkerbox")]
pub mod conversions; pub mod conversions;
pub mod error; pub mod error;
#[doc(hidden)]
pub mod lua;
#[cfg(feature = "shulkerbox")] #[cfg(feature = "shulkerbox")]
pub mod transpiler; pub mod transpiler;

View File

@ -233,6 +233,9 @@ impl Transpiler {
Expression::Primary(Primary::StringLiteral(string)) => { Expression::Primary(Primary::StringLiteral(string)) => {
Ok(Some(Command::Raw(string.str_content().to_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(_) => { Statement::Block(_) => {
unreachable!("Only literal commands are allowed in functions at this time.") unreachable!("Only literal commands are allowed in functions at this time.")