shulkerscript-lang/src/transpile/transpiler.rs

849 lines
33 KiB
Rust

//! Transpiler for `Shulkerscript`
use std::{
collections::{BTreeMap, HashSet},
ops::Deref,
sync::{Arc, OnceLock},
};
use shulkerbox::datapack::{self, Command, Datapack, Execute};
use crate::{
base::{self, source_file::SourceElement, Handler},
semantic::error::UnexpectedExpression,
syntax::syntax_tree::{
declaration::{Declaration, ImportItems},
expression::{Expression, FunctionCall, Primary},
program::{Namespace, ProgramFile},
statement::{
execute_block::{Conditional, Else, ExecuteBlock, ExecuteBlockHead, ExecuteBlockTail},
SemicolonStatement, Statement,
},
AnnotationAssignment,
},
transpile::util::{MacroString, MacroStringPart},
};
use super::{
error::{MismatchedTypes, MissingValue, TranspileError, TranspileResult, UnknownIdentifier},
expression::{ComptimeValue, ExpectedType, ExtendedCondition},
variables::{Scope, TranspileAssignmentTarget, VariableData},
FunctionData, TranspileAnnotationValue, TranspiledFunctionArguments,
};
/// A transpiler for `Shulkerscript`.
#[derive(Debug)]
pub struct Transpiler {
pub(super) main_namespace_name: String,
pub(super) datapack: shulkerbox::datapack::Datapack,
pub(super) setup_cmds: Vec<Command>,
pub(super) initialized_constant_scores: HashSet<i64>,
pub(super) temp_counter: usize,
/// Top-level [`Scope`] for each program identifier
pub(super) scopes: BTreeMap<String, Arc<Scope>>,
}
impl Transpiler {
/// Creates a new transpiler.
#[must_use]
pub fn new(main_namespace_name: impl Into<String>, pack_format: u8) -> Self {
let main_namespace_name = main_namespace_name.into();
Self {
main_namespace_name: main_namespace_name.clone(),
datapack: shulkerbox::datapack::Datapack::new(main_namespace_name, pack_format),
setup_cmds: Vec::new(),
initialized_constant_scores: HashSet::new(),
temp_counter: 0,
scopes: BTreeMap::new(),
}
}
/// Transpiles the given programs and returns the resulting datapack.
///
/// # Errors
/// - [`TranspileError::MissingFunctionDeclaration`] If a called function is missing
#[tracing::instrument(level = "trace", skip_all)]
pub fn transpile(
mut self,
programs: &[ProgramFile],
handler: &impl Handler<base::Error>,
) -> Result<Datapack, TranspileError> {
tracing::trace!("Transpiling program declarations");
let mut aliases = Vec::new();
for program in programs {
let mut program_aliases = BTreeMap::new();
let program_identifier = program
.namespace()
.span()
.source_file()
.identifier()
.clone();
let scope = self
.scopes
.entry(program_identifier.clone())
.or_insert_with(Scope::with_internal_functions)
.to_owned();
self.transpile_program_declarations(program, &mut program_aliases, &scope, handler)?;
aliases.push((scope.clone(), program_aliases));
}
for (scope, program_aliases) in aliases {
for (alias_name, (alias_program_identifier, actual_name)) in program_aliases {
if let Some(alias_scope) = self.scopes.get(&alias_program_identifier) {
if let Some(var_data) = alias_scope.get_variable(&actual_name) {
scope.set_arc_variable(&alias_name, var_data);
} else {
tracing::error!(
"Importing a non-existent variable: {} from {}",
actual_name,
alias_program_identifier
);
}
} else {
tracing::error!(
"Importing from a non-existent program: {}",
alias_program_identifier
);
}
}
}
let mut always_transpile_functions = Vec::new();
{
let functions = self.scopes.iter().flat_map(|(_, scope)| {
scope
.get_local_variables()
.read()
.unwrap()
.values()
.filter_map(|data| data.as_function().map(|(data, _, _)| data))
.cloned()
.collect::<Vec<_>>()
});
for data in functions {
let always_transpile_function = data.annotations.contains_key("tick")
|| data.annotations.contains_key("load")
|| data.annotations.contains_key("deobfuscate");
if always_transpile_function {
always_transpile_functions.push(data.identifier_span.clone());
};
}
}
tracing::trace!(
"Transpiling functions requested by user: {:?}",
always_transpile_functions
);
for identifier_span in always_transpile_functions {
let scope = self
.scopes
.entry(identifier_span.source_file().identifier().to_owned())
.or_insert_with(Scope::with_internal_functions)
.to_owned();
self.get_or_transpile_function(&identifier_span, None, &scope, handler)?;
}
if !self.setup_cmds.is_empty() {
let main_namespace = self.datapack.namespace_mut(&self.main_namespace_name);
let setup_fn = main_namespace.function_mut("shu/setup");
setup_fn.get_commands_mut().extend(self.setup_cmds.clone());
// prepend setup function to load tag
let load_values = self
.datapack
.namespace_mut("minecraft")
.tag_mut("load", datapack::tag::TagType::Function)
.values_mut();
load_values.insert(
0,
datapack::tag::TagValue::Simple(format!("{}:shu/setup", self.main_namespace_name)),
);
}
Ok(self.datapack)
}
/// Transpiles the given program.
fn transpile_program_declarations(
&mut self,
program: &ProgramFile,
program_aliases: &mut BTreeMap<String, (String, String)>,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<()> {
let namespace = program.namespace();
for declaration in program.declarations() {
self.transpile_declaration(declaration, namespace, program_aliases, scope, handler)?;
}
Ok(())
}
/// Transpiles the given declaration.
fn transpile_declaration(
&mut self,
declaration: &Declaration,
namespace: &Namespace,
program_aliases: &mut BTreeMap<String, (String, String)>,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<()> {
let program_identifier = declaration.span().source_file().identifier().clone();
match declaration {
Declaration::Function(function) => {
let identifier_span = &function.identifier().span;
let name = identifier_span.str().to_string();
let statements = function.block().statements().clone();
let annotations = function
.annotations()
.iter()
.map(|annotation| {
let AnnotationAssignment {
identifier: key,
value,
} = annotation.assignment();
(
key.span().str().to_string(),
TranspileAnnotationValue::from(value.clone()),
)
})
.collect();
let function_data = FunctionData {
namespace: namespace.namespace_name().str_content().to_string(),
identifier_span: identifier_span.clone(),
parameters: function
.parameters()
.as_ref()
.map(|l| l.elements().cloned().collect::<Vec<_>>())
.unwrap_or_default(),
statements,
public: function.is_public(),
annotations,
};
scope.set_variable(
&name,
VariableData::Function {
function_data,
path: OnceLock::new(),
function_scope: scope.clone(),
},
);
}
Declaration::Import(import) => {
let path = import.module().str_content();
let import_identifier =
super::util::calculate_import_identifier(&program_identifier, path);
// let aliases = &mut self.aliases;
match import.items() {
ImportItems::All(_) => {
handler.receive(base::Error::Other(
"Importing all items is not yet supported.".to_string(),
));
}
ImportItems::Named(list) => {
for item in list.elements() {
let name = item.span.str();
program_aliases.insert(
name.to_string(),
(import_identifier.clone(), name.to_string()),
);
}
}
}
}
Declaration::Tag(tag) => {
let namespace = self
.datapack
.namespace_mut(namespace.namespace_name().str_content());
let sb_tag = namespace.tag_mut(tag.name().str_content(), tag.tag_type());
if let Some(list) = &tag.entries().list {
for value in list.elements() {
sb_tag.add_value(value.str_content().as_ref().into());
}
}
if tag.replace().is_some() {
sb_tag.set_replace(true);
}
}
Declaration::GlobalVariable((_, declaration, _)) => {
let setup_variable_cmds = self.transpile_variable_declaration(
declaration,
true,
&program_identifier,
scope,
handler,
)?;
self.setup_cmds.extend(setup_variable_cmds);
}
};
Ok(())
}
pub(super) fn transpile_statement(
&mut self,
statement: &Statement,
program_identifier: &str,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<Vec<Command>> {
match statement {
Statement::LiteralCommand(literal_command) => {
Ok(vec![literal_command.clean_command().into()])
}
Statement::Run(run) => {
self.transpile_run_expression(run.expression(), program_identifier, scope, handler)
}
Statement::Block(_) => {
unreachable!("Only literal commands are allowed in functions at this time.")
}
Statement::ExecuteBlock(execute) => {
let child_scope = Scope::with_parent(scope.clone());
Ok(self.transpile_execute_block(
execute,
program_identifier,
&child_scope,
handler,
)?)
}
Statement::DocComment(doccomment) => {
let content = doccomment.content();
Ok(vec![Command::Comment(content.to_string())])
}
Statement::Grouping(group) => {
let child_scope = Scope::with_parent(scope.clone());
let statements = group.block().statements();
let mut errors = Vec::new();
let commands = statements
.iter()
.flat_map(|statement| {
self.transpile_statement(
statement,
program_identifier,
&child_scope,
handler,
)
.unwrap_or_else(|err| {
errors.push(err);
Vec::new()
})
})
.collect::<Vec<_>>();
if !errors.is_empty() {
return Err(errors.remove(0));
}
if commands.is_empty() {
Ok(Vec::new())
} else {
Ok(vec![Command::Group(commands)])
}
}
Statement::Semicolon(semi) => match semi.statement() {
SemicolonStatement::Expression(expr) => match expr {
Expression::Primary(Primary::FunctionCall(func)) => {
self.transpile_function_call(func, scope, handler)
}
unexpected => {
let error = TranspileError::UnexpectedExpression(UnexpectedExpression(
unexpected.clone(),
));
handler.receive(error.clone());
Err(error)
}
},
SemicolonStatement::VariableDeclaration(decl) => self
.transpile_variable_declaration(
decl,
false,
program_identifier,
scope,
handler,
),
SemicolonStatement::Assignment(assignment) => self.transpile_assignment(
TranspileAssignmentTarget::from(assignment.destination()),
assignment.expression(),
scope,
handler,
),
},
}
}
#[expect(clippy::only_used_in_recursion)]
fn transpile_run_expression(
&mut self,
expression: &Expression,
program_identifier: &str,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<Vec<Command>> {
match expression {
Expression::Primary(Primary::FunctionCall(func)) => {
self.transpile_function_call(func, scope, handler)
}
Expression::Primary(Primary::Identifier(ident)) => {
match scope.get_variable(ident.span.str()).as_deref() {
Some(VariableData::ComptimeValue {
value,
read_only: _,
}) => value.read().unwrap().as_ref().map_or_else(
|| {
let error = TranspileError::MissingValue(MissingValue {
expression: ident.span.clone(),
});
handler.receive(error.clone());
Err(error)
},
|val| {
let cmd = val.to_string_no_macro().map_or_else(
|| Command::UsesMacro(val.to_macro_string().into()),
Command::Raw,
);
Ok(vec![cmd])
},
),
Some(_) => {
let error = TranspileError::UnexpectedExpression(UnexpectedExpression(
expression.clone(),
));
handler.receive(error.clone());
Err(error)
}
None => {
let error = TranspileError::UnknownIdentifier(UnknownIdentifier {
identifier: ident.span.clone(),
});
handler.receive(error.clone());
Err(error)
}
}
}
expression @ Expression::Primary(
Primary::Integer(_)
| Primary::Boolean(_)
| Primary::Prefix(_)
| Primary::Indexed(_),
) => {
let error =
TranspileError::UnexpectedExpression(UnexpectedExpression(expression.clone()));
handler.receive(error.clone());
Err(error)
}
Expression::Primary(Primary::StringLiteral(string)) => {
Ok(vec![Command::Raw(string.str_content().to_string())])
}
Expression::Primary(Primary::MacroStringLiteral(string)) => {
Ok(vec![Command::UsesMacro(string.into())])
}
Expression::Primary(Primary::Lua(code)) => match code.eval_comptime(scope, handler)? {
Ok(ComptimeValue::String(cmd)) => Ok(vec![Command::Raw(cmd)]),
Ok(ComptimeValue::MacroString(cmd)) => Ok(vec![Command::UsesMacro(cmd.into())]),
Ok(ComptimeValue::Boolean(_) | ComptimeValue::Integer(_)) => {
let err = TranspileError::MismatchedTypes(MismatchedTypes {
expected_type: ExpectedType::String,
expression: code.span(),
});
handler.receive(err.clone());
Err(err)
}
Err(_) => {
let err = TranspileError::MissingValue(MissingValue {
expression: code.span(),
});
handler.receive(err.clone());
Err(err)
}
},
Expression::Primary(Primary::Parenthesized(parenthesized)) => self
.transpile_run_expression(
parenthesized.expression(),
program_identifier,
scope,
handler,
),
Expression::Binary(bin) => match bin.comptime_eval(scope, handler) {
Ok(ComptimeValue::String(cmd)) => Ok(vec![Command::Raw(cmd)]),
Ok(ComptimeValue::MacroString(cmd)) => Ok(vec![Command::UsesMacro(cmd.into())]),
_ => {
let err = TranspileError::MismatchedTypes(MismatchedTypes {
expression: bin.span(),
expected_type: ExpectedType::String,
});
handler.receive(err.clone());
Err(err)
}
},
}
}
pub(super) fn transpile_function_call(
&mut self,
func: &FunctionCall,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<Vec<Command>> {
let arguments = func
.arguments()
.as_ref()
.map(|l| l.elements().map(Deref::deref).collect::<Vec<_>>());
if let Some(VariableData::InternalFunction { implementation }) =
scope.get_variable(func.identifier().span.str()).as_deref()
{
implementation(self, scope, func).inspect_err(|err| {
handler.receive(err.clone());
})
} else {
let (location, arguments) = self.get_or_transpile_function(
&func.identifier().span,
arguments.as_deref(),
scope,
handler,
)?;
let mut function_call = format!("function {location}");
match arguments {
TranspiledFunctionArguments::Static(arguments) => {
use std::fmt::Write;
let arguments_iter = arguments.iter().map(|(ident, v)| match v {
MacroString::String(s) => MacroString::String(format!(
r#"{macro_name}:"{escaped}""#,
macro_name = crate::util::identifier_to_macro(ident),
escaped = crate::util::escape_str(s)
)),
MacroString::MacroString(parts) => MacroString::MacroString(
std::iter::once(MacroStringPart::String(format!(
r#"{macro_name}:""#,
macro_name = crate::util::identifier_to_macro(ident)
)))
.chain(parts.clone().into_iter().map(|part| match part {
MacroStringPart::String(s) => {
MacroStringPart::String(crate::util::escape_str(&s).to_string())
}
macro_usage @ MacroStringPart::MacroUsage(_) => macro_usage,
}))
.chain(std::iter::once(MacroStringPart::String('"'.to_string())))
.collect(),
),
});
let arguments = super::util::join_macro_strings(arguments_iter);
let cmd = match arguments {
MacroString::String(arguments) => {
write!(function_call, " {{{arguments}}}").unwrap();
Command::Raw(function_call)
}
MacroString::MacroString(mut parts) => {
function_call.push_str(" {");
parts.insert(0, MacroStringPart::String(function_call));
parts.push(MacroStringPart::String('}'.to_string()));
Command::UsesMacro(MacroString::MacroString(parts).into())
}
};
Ok(vec![cmd])
}
TranspiledFunctionArguments::Dynamic(mut cmds) => {
function_call.push_str(" with storage shulkerscript:function_arguments");
cmds.push(Command::Raw(function_call));
Ok(cmds)
}
TranspiledFunctionArguments::None => Ok(vec![Command::Raw(function_call)]),
}
}
}
fn transpile_execute_block(
&mut self,
execute: &ExecuteBlock,
program_identifier: &str,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<Vec<Command>> {
self.transpile_execute_block_internal(execute, program_identifier, scope, handler)
.map(|ex| {
ex.map(|(mut pre_cmds, exec)| {
pre_cmds.push(exec.into());
pre_cmds
})
.unwrap_or_default()
})
}
fn transpile_execute_block_internal(
&mut self,
execute: &ExecuteBlock,
program_identifier: &str,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<Option<(Vec<Command>, Execute)>> {
match execute {
ExecuteBlock::HeadTail(head, tail) => {
let tail = match tail {
ExecuteBlockTail::Block(block) => {
let mut errors = Vec::new();
let commands = block
.statements()
.iter()
.flat_map(|s| {
self.transpile_statement(s, program_identifier, scope, handler)
.unwrap_or_else(|err| {
errors.push(err);
Vec::new()
})
})
.collect::<Vec<_>>();
if !errors.is_empty() {
return Err(errors.remove(0));
}
if commands.is_empty() {
Ok(None)
} else {
Ok(Some((Vec::new(), Execute::Runs(commands))))
}
}
ExecuteBlockTail::ExecuteBlock(_, execute_block) => self
.transpile_execute_block_internal(
execute_block,
program_identifier,
scope,
handler,
),
}?;
self.combine_execute_head_tail(head, tail, program_identifier, scope, handler)
}
ExecuteBlock::IfElse(cond, block, el) => {
let statements = block.statements();
let then = if statements.is_empty() {
Some(Execute::Runs(Vec::new()))
} else if statements.len() > 1 {
let mut errors = Vec::new();
let commands = statements
.iter()
.flat_map(|statement| {
self.transpile_statement(statement, program_identifier, scope, handler)
.unwrap_or_else(|err| {
errors.push(err);
Vec::new()
})
})
.collect();
if !errors.is_empty() {
return Err(errors.remove(0));
}
Some(Execute::Runs(commands))
} else {
let cmds = self.transpile_statement(
&statements[0],
program_identifier,
scope,
handler,
)?;
if cmds.len() > 1 {
Some(Execute::Runs(cmds))
} else {
cmds.into_iter()
.next()
.map(|cmd| Execute::Run(Box::new(cmd)))
}
};
then.map_or_else(
|| Ok(None),
|then| {
self.transpile_conditional(
cond,
then,
Some(el),
program_identifier,
scope,
handler,
)
},
)
}
}
}
fn transpile_conditional(
&mut self,
cond: &Conditional,
then: Execute,
el: Option<&Else>,
program_identifier: &str,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<Option<(Vec<Command>, Execute)>> {
let cond_expression = cond.condition().expression().as_ref();
let mut errors = Vec::new();
let el = el.and_then(|el| {
let (_, block) = el.clone().dissolve();
let statements = block.statements();
let cmds = statements
.iter()
.flat_map(|statement| {
self.transpile_statement(statement, program_identifier, scope, handler)
.unwrap_or_else(|err| {
errors.push(err);
Vec::new()
})
})
.collect::<Vec<_>>();
match cmds.len() {
0 => None,
1 => Some(Execute::Run(Box::new(
cmds.into_iter().next().expect("length is 1"),
))),
_ => Some(Execute::Runs(cmds)),
}
});
if let Ok(ComptimeValue::Boolean(value)) = cond_expression.comptime_eval(scope, handler) {
if value {
Ok(Some((Vec::new(), then)))
} else {
Ok(el.map(|el| (Vec::new(), el)))
}
} else {
if !errors.is_empty() {
return Err(errors.remove(0));
}
let (pre_cond_cmds, cond) =
self.transpile_expression_as_condition(cond_expression, scope, handler)?;
match cond {
ExtendedCondition::Runtime(cond) => Ok(Some((
pre_cond_cmds,
Execute::If(cond, Box::new(then), el.map(Box::new)),
))),
ExtendedCondition::Comptime(cond) => {
if cond {
Ok(Some((Vec::new(), then)))
} else {
Ok(el.map(|el| (Vec::new(), el)))
}
}
}
}
}
fn combine_execute_head_tail(
&mut self,
head: &ExecuteBlockHead,
tail: Option<(Vec<Command>, Execute)>,
program_identifier: &str,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<Option<(Vec<Command>, Execute)>> {
Ok(match head {
ExecuteBlockHead::Conditional(cond) => {
if let Some((mut pre_cmds, tail)) = tail {
self.transpile_conditional(
cond,
tail,
None,
program_identifier,
scope,
handler,
)?
.map(|(pre_cond_cmds, cond)| {
pre_cmds.extend(pre_cond_cmds);
(pre_cmds, cond)
})
} else {
None
}
}
ExecuteBlockHead::As(r#as) => {
let selector = r#as.as_selector();
tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::As(selector.into(), Box::new(tail)))
})
}
ExecuteBlockHead::At(at) => {
let selector = at.at_selector();
tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::At(selector.into(), Box::new(tail)))
})
}
ExecuteBlockHead::Align(align) => {
let align = align.align_selector();
tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::Align(align.into(), Box::new(tail)))
})
}
ExecuteBlockHead::Anchored(anchored) => {
let anchor = anchored.anchored_selector();
tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::Anchored(anchor.into(), Box::new(tail)))
})
}
ExecuteBlockHead::In(r#in) => {
let dimension = r#in.in_selector();
tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::In(dimension.into(), Box::new(tail)))
})
}
ExecuteBlockHead::Positioned(positioned) => {
let position = positioned.positioned_selector();
tail.map(|(pre_cmds, tail)| {
(
pre_cmds,
Execute::Positioned(position.into(), Box::new(tail)),
)
})
}
ExecuteBlockHead::Rotated(rotated) => {
let rotation = rotated.rotated_selector();
tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::Rotated(rotation.into(), Box::new(tail)))
})
}
ExecuteBlockHead::Facing(facing) => {
let facing = facing.facing_selector();
tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::Facing(facing.into(), Box::new(tail)))
})
}
ExecuteBlockHead::AsAt(as_at) => {
let selector = as_at.asat_selector();
tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::AsAt(selector.into(), Box::new(tail)))
})
}
ExecuteBlockHead::On(on) => {
let dimension = on.on_selector();
tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::On(dimension.into(), Box::new(tail)))
})
}
ExecuteBlockHead::Store(store) => {
let store = store.store_selector();
tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::Store(store.into(), Box::new(tail)))
})
}
ExecuteBlockHead::Summon(summon) => {
let entity = summon.summon_selector();
tail.map(|(pre_cmds, tail)| {
(pre_cmds, Execute::Summon(entity.into(), Box::new(tail)))
})
}
})
}
}