reimplement semantic checking

This commit is contained in:
Moritz Hölting 2025-03-31 21:40:59 +02:00
parent f3b3d5d3b6
commit 32d453ebef
12 changed files with 931 additions and 497 deletions

View File

@ -2,10 +2,7 @@
#![allow(missing_docs)]
use std::{collections::HashSet, fmt::Display};
use getset::Getters;
use itertools::Itertools as _;
use std::fmt::Display;
use crate::{
base::{
@ -14,6 +11,10 @@ use crate::{
},
lexical::token::StringLiteral,
syntax::syntax_tree::expression::Expression,
transpile::error::{
AssignmentError, IllegalIndexing, MismatchedTypes, MissingFunctionDeclaration,
UnknownIdentifier,
},
};
#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
@ -31,77 +32,20 @@ pub enum Error {
UnresolvedMacroUsage(#[from] UnresolvedMacroUsage),
#[error(transparent)]
IncompatibleFunctionAnnotation(#[from] IncompatibleFunctionAnnotation),
#[error(transparent)]
IllegalIndexing(#[from] IllegalIndexing),
#[error(transparent)]
MismatchedTypes(#[from] MismatchedTypes),
#[error(transparent)]
UnknownIdentifier(#[from] UnknownIdentifier),
#[error(transparent)]
AssignmentError(#[from] AssignmentError),
#[error("Lua is disabled, but a Lua function was used.")]
LuaDisabled,
#[error("Other: {0}")]
Other(String),
}
// TODO: remove duplicate error (also in transpile)
/// An error that occurs when a function declaration is missing.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Getters)]
pub struct MissingFunctionDeclaration {
#[get = "pub"]
span: Span,
#[get = "pub"]
alternatives: Vec<String>,
}
impl MissingFunctionDeclaration {
#[expect(dead_code)]
pub(super) fn from_context(identifier_span: Span, functions: &HashSet<String>) -> Self {
let own_name = identifier_span.str();
let alternatives = functions
.iter()
.filter_map(|function_name| {
let normalized_distance =
strsim::normalized_damerau_levenshtein(own_name, function_name);
(normalized_distance > 0.8
|| strsim::damerau_levenshtein(own_name, function_name) < 3)
.then_some((normalized_distance, function_name))
})
.sorted_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal))
.map(|(_, data)| data)
.take(8)
.cloned()
.collect::<Vec<_>>();
Self {
alternatives,
span: identifier_span,
}
}
}
impl Display for MissingFunctionDeclaration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use std::fmt::Write;
let message = format!(
"no matching function declaration found for invocation of function `{}`",
self.span.str()
);
write!(f, "{}", Message::new(Severity::Error, message))?;
let help_message = if self.alternatives.is_empty() {
None
} else {
let mut message = String::from("did you mean ");
for (i, alternative) in self.alternatives.iter().enumerate() {
if i > 0 {
message.push_str(", ");
}
write!(message, "`{alternative}`")?;
}
Some(message + "?")
};
write!(
f,
"\n{}",
SourceCodeDisplay::new(&self.span, help_message.as_ref())
)
}
}
impl std::error::Error for MissingFunctionDeclaration {}
/// An error that occurs when a function declaration is missing.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UnexpectedExpression(pub Expression);

File diff suppressed because it is too large Load Diff

95
src/semantic/scope.rs Normal file
View File

@ -0,0 +1,95 @@
use std::{collections::HashMap, sync::RwLock};
/// Type of variable
#[derive(Clone, Debug, Copy, PartialEq, Eq)]
pub enum VariableType {
/// A function.
Function,
/// A macro function parameter.
MacroParameter,
/// A scoreboard.
Scoreboard,
/// A scoreboard value.
ScoreboardValue,
/// Multiple values stored in scoreboard.
ScoreboardArray,
/// A tag applied to entities.
Tag,
/// A boolean stored in a data storage.
BooleanStorage,
/// Multiple booleans stored in a data storage array.
BooleanStorageArray,
/// Compiler internal function.
InternalFunction,
}
/// A scope that stores variables.
#[derive(Debug, Default)]
pub struct SemanticScope<'a> {
/// Parent scope where variables are inherited from.
parent: Option<&'a Self>,
/// Variables stored in the scope.
variables: RwLock<HashMap<String, VariableType>>,
}
impl<'a> SemanticScope<'a> {
/// Creates a new scope.
#[must_use]
pub fn new() -> Self {
let scope = Self::default();
scope.set_variable("print", VariableType::InternalFunction);
scope
}
/// Creates a new scope with a parent.
#[must_use]
pub fn with_parent(parent: &'a Self) -> Self {
Self {
parent: Some(parent),
..Default::default()
}
}
/// Gets a variable from the scope.
pub fn get_variable(&self, name: &str) -> Option<VariableType> {
let var = self.variables.read().unwrap().get(name).copied();
if var.is_some() {
var
} else {
self.parent
.as_ref()
.and_then(|parent| parent.get_variable(name))
}
}
/// Sets a variable in the scope.
pub fn set_variable(&self, name: &str, var: VariableType) {
self.variables
.write()
.unwrap()
.insert(name.to_string(), var);
}
/// Gets the variables stored in the current scope.
pub fn get_local_variables(&self) -> &RwLock<HashMap<String, VariableType>> {
&self.variables
}
/// Gets all variables stored in the scope.
///
/// This function does not return a reference to the variables, but clones them.
pub fn get_all_variables(&self) -> HashMap<String, VariableType> {
let mut variables = self.variables.read().unwrap().clone();
if let Some(parent) = self.parent.as_ref() {
variables.extend(parent.get_all_variables());
}
variables
}
/// Gets the parent scope.
pub fn get_parent(&self) -> Option<&Self> {
self.parent
}
}

View File

@ -247,6 +247,14 @@ impl SourceElement for Parenthesized {
}
}
impl std::ops::Deref for Parenthesized {
type Target = Expression;
fn deref(&self) -> &Self::Target {
&self.expression
}
}
/// Represents a indexed expression in the syntax tree.
///
/// Syntax Synopsis:

View File

@ -1,7 +1,5 @@
//! Execute block statement syntax tree.
use std::collections::HashSet;
use derive_more::From;
use enum_as_inner::EnumAsInner;
use getset::Getters;
@ -998,10 +996,10 @@ pub trait ExecuteBlockHeadItem {
#[expect(clippy::missing_errors_doc)]
fn analyze_semantics(
&self,
macro_names: &HashSet<String>,
scope: &crate::semantic::SemanticScope,
handler: &impl Handler<base::Error>,
) -> Result<(), crate::semantic::error::Error> {
self.selector().analyze_semantics(macro_names, handler)
self.selector().analyze_semantics(scope, handler)
}
}

View File

@ -44,6 +44,8 @@ pub enum TranspileError {
MissingValue(#[from] MissingValue),
#[error(transparent)]
IllegalIndexing(#[from] IllegalIndexing),
#[error(transparent)]
InvalidArgument(#[from] InvalidArgument),
}
/// The result of a transpilation operation.
@ -52,8 +54,10 @@ pub type TranspileResult<T> = Result<T, TranspileError>;
/// An error that occurs when a function declaration is missing.
#[derive(Debug, Clone, PartialEq, Eq, Getters)]
pub struct MissingFunctionDeclaration {
/// The span of the identifier that is missing.
#[get = "pub"]
span: Span,
/// Possible alternatives for the missing function declaration.
#[get = "pub"]
alternatives: Vec<FunctionData>,
}
@ -127,11 +131,26 @@ impl Display for MissingFunctionDeclaration {
impl std::error::Error for MissingFunctionDeclaration {}
impl std::hash::Hash for MissingFunctionDeclaration {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.span.hash(state);
for alternative in &self.alternatives {
alternative.identifier_span.hash(state);
alternative.namespace.hash(state);
alternative.parameters.hash(state);
alternative.public.hash(state);
alternative.statements.hash(state);
}
}
}
/// An error that occurs when a function declaration is missing.
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LuaRuntimeError {
/// The span of the code block that caused the error.
pub code_block: Span,
/// The error message of the Lua runtime.
pub error_message: String,
}
@ -155,6 +174,8 @@ impl std::error::Error for LuaRuntimeError {}
#[cfg(feature = "lua")]
impl LuaRuntimeError {
/// Creates a new Lua runtime error from an mlua error.
#[must_use]
pub fn from_lua_err(err: &mlua::Error, span: Span) -> Self {
let err_string = err.to_string();
Self {
@ -168,9 +189,13 @@ impl LuaRuntimeError {
}
/// An error that occurs when an annotation has an illegal content.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Getters)]
pub struct IllegalAnnotationContent {
/// The span of the annotation.
#[get = "pub"]
pub annotation: Span,
/// The error message.
#[get = "pub"]
pub message: String,
}
@ -194,9 +219,13 @@ impl Display for IllegalAnnotationContent {
impl std::error::Error for IllegalAnnotationContent {}
/// An error that occurs when an expression can not evaluate to the wanted type.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Getters)]
pub struct MismatchedTypes {
/// The expression that can not evaluate to the wanted type.
#[get = "pub"]
pub expression: Span,
/// The expected type.
#[get = "pub"]
pub expected_type: ExpectedType,
}
@ -216,9 +245,13 @@ impl Display for MismatchedTypes {
impl std::error::Error for MismatchedTypes {}
/// An error that occurs when an expression can not evaluate to the wanted type.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Getters)]
pub struct FunctionArgumentsNotAllowed {
/// The arguments that are not allowed.
#[get = "pub"]
pub arguments: Span,
/// The error message.
#[get = "pub"]
pub message: String,
}
@ -237,9 +270,13 @@ impl Display for FunctionArgumentsNotAllowed {
impl std::error::Error for FunctionArgumentsNotAllowed {}
/// An error that occurs when an expression can not evaluate to the wanted type.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Getters)]
pub struct AssignmentError {
/// The identifier that is assigned to.
#[get = "pub"]
pub identifier: Span,
/// The error message.
#[get = "pub"]
pub message: String,
}
@ -258,8 +295,10 @@ impl Display for AssignmentError {
impl std::error::Error for AssignmentError {}
/// An error that occurs when an unknown identifier is used.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Getters)]
pub struct UnknownIdentifier {
/// The unknown identifier.
#[get = "pub"]
pub identifier: Span,
}
@ -285,8 +324,10 @@ impl Display for UnknownIdentifier {
impl std::error::Error for UnknownIdentifier {}
/// An error that occurs when there is a value expected but none provided.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Getters)]
pub struct MissingValue {
/// The expression that is missing a value.
#[get = "pub"]
pub expression: Span,
}
@ -312,9 +353,13 @@ impl Display for MissingValue {
impl std::error::Error for MissingValue {}
/// An error that occurs when an indexing operation is not permitted.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Getters)]
pub struct IllegalIndexing {
/// The reason why the indexing operation is not permitted.
#[get = "pub"]
pub reason: IllegalIndexingReason,
/// The expression that is the reason for the indexing being illegal.
#[get = "pub"]
pub expression: Span,
}
@ -333,11 +378,24 @@ impl Display for IllegalIndexing {
impl std::error::Error for IllegalIndexing {}
/// The reason why an indexing operation is not permitted.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum IllegalIndexingReason {
/// The expression is not an identifier.
NotIdentifier,
InvalidComptimeType { expected: ExpectedType },
IndexOutOfBounds { index: usize, length: usize },
/// The expression cannot be indexed.
NotIndexable,
/// The expression can only be indexed with a specific type that can be evaluated at compile time.
InvalidComptimeType {
/// The expected type.
expected: ExpectedType,
},
/// The index is out of bounds.
IndexOutOfBounds {
/// The index that is out of bounds.
index: usize,
/// The length indexed object.
length: usize,
},
}
impl Display for IllegalIndexingReason {
@ -346,6 +404,9 @@ impl Display for IllegalIndexingReason {
Self::NotIdentifier => {
write!(f, "The expression is not an identifier.")
}
Self::NotIndexable => {
write!(f, "The expression cannot be indexed.")
}
Self::InvalidComptimeType { expected } => {
write!(
f,
@ -361,3 +422,28 @@ impl Display for IllegalIndexingReason {
}
}
}
/// An error that occurs when an indexing operation is not permitted.
#[derive(Debug, Clone, PartialEq, Eq, Getters)]
pub struct InvalidArgument {
/// The span of the argument.
#[get = "pub"]
pub span: Span,
/// The reason why the argument is invalid.
#[get = "pub"]
pub reason: String,
}
impl Display for InvalidArgument {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", Message::new(Severity::Error, &self.reason))?;
write!(
f,
"\n{}",
SourceCodeDisplay::new(&self.span, Option::<u8>::None)
)
}
}
impl std::error::Error for InvalidArgument {}

View File

@ -85,7 +85,7 @@ impl Display for ValueType {
}
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ExpectedType {
Boolean,
Integer,
@ -915,7 +915,7 @@ impl Transpiler {
}
}
fn transpile_binary_expression(
pub(super) fn transpile_binary_expression(
&mut self,
binary: &Binary,
target: &DataLocation,

View File

@ -361,7 +361,18 @@ impl Transpiler {
path: std::mem::take(&mut temp_path[0]),
})
}
_ => todo!("other variable types"),
_ => {
let err = TranspileError::MismatchedTypes(MismatchedTypes {
expression: expression.span(),
expected_type: ExpectedType::AnyOf(vec![
ExpectedType::Integer,
ExpectedType::Boolean,
ExpectedType::String,
]),
});
handler.receive(err.clone());
Err(err)
}
}
}
Expression::Primary(

View File

@ -5,6 +5,7 @@ use std::{
sync::Arc,
};
use cfg_if::cfg_if;
use shulkerbox::prelude::{Command, Execute};
use serde_json::{json, Value as JsonValue};
@ -15,8 +16,8 @@ use crate::{
semantic::error::{InvalidFunctionArguments, UnexpectedExpression},
syntax::syntax_tree::expression::{Expression, FunctionCall, Primary},
transpile::{
error::{IllegalIndexing, IllegalIndexingReason, LuaRuntimeError, UnknownIdentifier},
expression::{ComptimeValue, DataLocation, StorageType},
error::{IllegalIndexing, IllegalIndexingReason, UnknownIdentifier},
expression::{ComptimeValue, DataLocation, ExpectedType, StorageType},
util::MacroString,
TranspileError,
},
@ -210,17 +211,24 @@ fn print_function(
Vec::new(),
vec![JsonValue::String(string.str_content().to_string())],
)),
#[cfg_attr(not(feature = "lua"), expect(unused_variables))]
Primary::Lua(lua) => {
cfg_if! {
if #[cfg(feature = "lua")] {
let (ret, _lua) = lua.eval(scope, &VoidHandler)?;
Ok((
Vec::new(),
vec![JsonValue::String(ret.to_string().map_err(|err| {
TranspileError::LuaRuntimeError(LuaRuntimeError::from_lua_err(
TranspileError::LuaRuntimeError(super::error::LuaRuntimeError::from_lua_err(
&err,
lua.span(),
))
})?)],
))
} else {
Err(TranspileError::LuaDisabled)
}
}
}
Primary::Identifier(ident) => {
let (cur_contains_macro, cmd, part) =
@ -244,7 +252,13 @@ fn print_function(
);
Ok((cmd.into_iter().collect(), vec![value]))
} else {
todo!("allow macro string, but throw error when index is not constant string")
// TODO: allow macro string, but throw error when index is not constant string
Err(TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::InvalidComptimeType {
expected: ExpectedType::String,
},
expression: indexed.index().span(),
}))
}
}
Some(VariableData::ScoreboardArray { objective, targets }) => {
@ -274,7 +288,12 @@ fn print_function(
}))
}
} else {
todo!("throw error when index is not constant integer")
Err(TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::InvalidComptimeType {
expected: ExpectedType::Integer,
},
expression: indexed.index().span(),
}))
}
}
Some(VariableData::BooleanStorageArray {
@ -308,10 +327,18 @@ fn print_function(
}))
}
} else {
todo!("throw error when index is not constant integer")
Err(TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::InvalidComptimeType {
expected: ExpectedType::Integer,
},
expression: indexed.index().span(),
}))
}
}
_ => todo!("catch illegal indexing"),
_ => Err(TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::NotIndexable,
expression: indexed.object().span(),
})),
}
}
_ => Err(TranspileError::IllegalIndexing(IllegalIndexing {
@ -339,9 +366,43 @@ fn print_function(
Ok((cmds, parts))
}
_ => todo!("print_function Primary"),
primary => {
let (storage_name, mut storage_paths) = transpiler.get_temp_storage_locations(1);
let location = DataLocation::Storage {
storage_name,
path: std::mem::take(&mut storage_paths[0]),
r#type: StorageType::Int,
};
let cmds = transpiler.transpile_primary_expression(
primary,
&location,
scope,
&VoidHandler,
)?;
let (cmd, part) = get_data_location(&location, transpiler);
Ok((
cmds.into_iter().chain(cmd.into_iter()).collect(),
vec![part],
))
}
},
Expression::Binary(_) => todo!("print_function Binary"),
Expression::Binary(binary) => {
let (storage_name, mut storage_paths) = transpiler.get_temp_storage_locations(1);
let location = DataLocation::Storage {
storage_name,
path: std::mem::take(&mut storage_paths[0]),
r#type: StorageType::Int,
};
let cmds =
transpiler.transpile_binary_expression(binary, &location, scope, &VoidHandler)?;
let (cmd, part) = get_data_location(&location, transpiler);
Ok((
cmds.into_iter().chain(cmd.into_iter()).collect(),
vec![part],
))
}
}?;
// TODO: prepend prefix with datapack name to parts and remove following

View File

@ -12,7 +12,7 @@ mod enabled {
syntax::syntax_tree::expression::LuaCode,
transpile::{
error::{
LuaRuntimeError, MismatchedTypes, TranspileError, TranspileResult,
InvalidArgument, LuaRuntimeError, MismatchedTypes, TranspileError, TranspileResult,
UnknownIdentifier,
},
expression::{ComptimeValue, ExpectedType},
@ -263,7 +263,11 @@ mod enabled {
Value::Table(table)
}
Some(VariableData::Function { .. } | VariableData::InternalFunction { .. }) => {
todo!("(internal) functions are not supported yet");
// TODO: add support for functions
return Err(TranspileError::InvalidArgument(InvalidArgument {
reason: "functions cannot be passed to Lua".to_string(),
span: identifier.span(),
}));
}
None => {
return Err(TranspileError::UnknownIdentifier(UnknownIdentifier {
@ -397,7 +401,7 @@ mod disabled {
&self,
scope: &Arc<Scope>,
handler: &impl Handler<base::Error>,
) -> TranspileResult<()> {
) -> TranspileResult<((), ())> {
let _ = scope;
handler.receive(TranspileError::LuaDisabled);
tracing::error!("Lua code evaluation is disabled");

View File

@ -16,7 +16,7 @@ use crate::{
#[doc(hidden)]
#[cfg(feature = "shulkerbox")]
pub mod conversions;
mod error;
pub mod error;
pub mod expression;
@ -58,7 +58,7 @@ pub struct FunctionData {
/// Possible values for an annotation.
#[expect(clippy::module_name_repetitions)]
#[derive(Debug, Clone, PartialEq, Eq, EnumIs)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, EnumIs)]
pub enum TranspileAnnotationValue {
/// No value.
None,

View File

@ -489,7 +489,15 @@ impl Transpiler {
target: s,
}),
Some(ComptimeValue::MacroString(s)) => {
todo!("indexing scoreboard with macro string: {s:?}")
// TODO: allow indexing with macro string
let err = TranspileError::IllegalIndexing(IllegalIndexing {
reason: IllegalIndexingReason::InvalidComptimeType {
expected: ExpectedType::String,
},
expression: expression.span(),
});
handler.receive(err.clone());
return Err(err);
}
Some(_) => {
let err = TranspileError::IllegalIndexing(IllegalIndexing {
@ -621,7 +629,15 @@ impl Transpiler {
entity: s,
}),
Some(ComptimeValue::MacroString(s)) => {
todo!("indexing tag with macro string: {s:?}")
// TODO: allow indexing tag with macro string
let err = TranspileError::IllegalIndexing(IllegalIndexing {
expression: expression.span(),
reason: IllegalIndexingReason::InvalidComptimeType {
expected: ExpectedType::String,
},
});
handler.receive(err.clone());
return Err(err);
}
Some(_) => {
let err = TranspileError::IllegalIndexing(IllegalIndexing {
@ -931,7 +947,15 @@ impl Transpiler {
Ok((name, targets))
}
TranspileAnnotationValue::Map(map) => {
todo!("allow map deobfuscate annotation for array variables")
// TODO: implement when map deobfuscate annotation is implemented
let error =
TranspileError::IllegalAnnotationContent(IllegalAnnotationContent {
annotation: deobfuscate_annotation.span(),
message: "Deobfuscate annotation value must be a string or none."
.to_string(),
});
handler.receive(error.clone());
Err(error)
}
TranspileAnnotationValue::Expression(_) => {
let error =