From 927b0f52c13dbc5a0aabdceaed9ac2e18accd77c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20H=C3=B6lting?= <87192362+moritz-hoelting@users.noreply.github.com> Date: Wed, 18 Jun 2025 11:44:26 +0200 Subject: [PATCH] implement first parts of member access --- CHANGELOG.md | 2 + Cargo.toml | 4 +- src/semantic/mod.rs | 12 +- src/syntax/syntax_tree/expression.rs | 59 ++++++++- src/transpile/expression.rs | 183 ++++++++++++++++++++++++++- src/transpile/function.rs | 1 + src/transpile/transpiler.rs | 9 +- 7 files changed, 264 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 460afc3..66d4a62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Integer and boolean arrays (scoreboard and data storage) - Integer map (scoreboard) - Boolean map (tag) + - Member access (e.g. `.objective` to get objective name where int is stored) +- Return statement - Example: barebones compiler ### Changed diff --git a/Cargo.toml b/Cargo.toml index 5f1e4fc..d17efa9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ zip = ["shulkerbox?/zip"] cfg-if = "1.0.0" chksum-md5 = { version = "0.1.0", optional = true } colored = "3.0.0" -derive_more = { version = "2.0.1", default-features = false, features = ["deref", "deref_mut", "from"] } +derive_more = { version = "2.0.1", default-features = false, features = [ "deref", "deref_mut", "from"] } enum-as-inner = "0.6.0" getset = "0.1.2" itertools = "0.14.0" @@ -35,7 +35,7 @@ pathdiff = "0.2.3" serde = { version = "1.0.217", features = ["derive"], optional = true } serde_json = { version = "1.0.138", optional = true } # shulkerbox = { version = "0.1.0", default-features = false, optional = true } -shulkerbox = { git = "https://github.com/moritz-hoelting/shulkerbox", rev = "5c4d340162fc16065add448ed387a1ce481c27d6", default-features = false, optional = true } +shulkerbox = { git = "https://github.com/moritz-hoelting/shulkerbox", rev = "6ff544131b2518b8c92bc4d2de7682efd7141ec4", default-features = false, optional = true } strsim = "0.11.1" strum = { version = "0.27.0", features = ["derive"] } thiserror = "2.0.11" diff --git a/src/semantic/mod.rs b/src/semantic/mod.rs index 95cb737..a51a06e 100644 --- a/src/semantic/mod.rs +++ b/src/semantic/mod.rs @@ -623,6 +623,10 @@ impl Primary { Err(err) } } + Self::MemberAccess(_) => { + // TODO: + Ok(()) + } Self::Parenthesized(expr) => expr.analyze_semantics(scope, handler), Self::Prefix(prefixed) => match prefixed.operator() { PrefixOperator::LogicalNot(_) => { @@ -680,7 +684,9 @@ impl Primary { match self { Self::Boolean(_) => expected == ValueType::Boolean, Self::Integer(_) => expected == ValueType::Integer, - Self::StringLiteral(_) | Self::MacroStringLiteral(_) => expected == ValueType::String, + Self::StringLiteral(_) | Self::MacroStringLiteral(_) => { + matches!(expected, ValueType::String | ValueType::Boolean) + } Self::FunctionCall(_) => matches!(expected, ValueType::Boolean | ValueType::Integer), Self::Indexed(indexed) => match indexed.object().as_ref() { Self::Identifier(ident) => { @@ -699,6 +705,10 @@ impl Primary { } _ => false, }, + Self::MemberAccess(_) => { + // TODO: + true + } Self::Identifier(ident) => match scope.get_variable(ident.span.str()) { Some(VariableType::BooleanStorage) => expected == ValueType::Boolean, Some(VariableType::ScoreboardValue) => expected == ValueType::Integer, diff --git a/src/syntax/syntax_tree/expression.rs b/src/syntax/syntax_tree/expression.rs index 2c663d1..5b358b6 100644 --- a/src/syntax/syntax_tree/expression.rs +++ b/src/syntax/syntax_tree/expression.rs @@ -175,6 +175,7 @@ impl SourceElement for Expression { /// | Boolean /// | StringLiteral /// | FunctionCall +/// | MemberAccess /// | MacroStringLiteral /// | LuaCode /// ``` @@ -190,6 +191,7 @@ pub enum Primary { Boolean(Boolean), StringLiteral(StringLiteral), FunctionCall(FunctionCall), + MemberAccess(MemberAccess), MacroStringLiteral(MacroStringLiteral), Lua(Box), } @@ -205,6 +207,7 @@ impl SourceElement for Primary { Self::Boolean(bool) => bool.span(), Self::StringLiteral(string_literal) => string_literal.span(), Self::FunctionCall(function_call) => function_call.span(), + Self::MemberAccess(member_access) => member_access.span(), Self::MacroStringLiteral(macro_string_literal) => macro_string_literal.span(), Self::Lua(lua_code) => lua_code.span(), } @@ -443,6 +446,45 @@ impl LuaCode { } } +/// Represents a member access in the syntax tree. +/// +/// Syntax Synopsis: +/// +/// ```ebnf +/// MemberAccess: +/// Primary '.' Identifier +/// ``` +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Getters)] +pub struct MemberAccess { + /// The parent expression + #[get = "pub"] + parent: Box, + /// The dot in the middle + #[get = "pub"] + dot: Punctuation, + /// The member being accessed + #[get = "pub"] + member: Identifier, +} + +impl SourceElement for MemberAccess { + fn span(&self) -> Span { + self.parent + .span() + .join(&self.member.span) + .expect("invalid span") + } +} + +impl MemberAccess { + /// Dissolves the [`MemberAccess`] into its components. + #[must_use] + pub fn dissolve(self) -> (Box, Punctuation, Identifier) { + (self.parent, self.dot, self.member) + } +} + impl Parser<'_> { /// Parses an [`Expression`] /// @@ -509,7 +551,7 @@ impl Parser<'_> { /// - If the parser is not at a valid primary expression. #[expect(clippy::too_many_lines)] pub fn parse_primary(&mut self, handler: &impl Handler) -> ParseResult { - match self.stop_at_significant() { + let prim = match self.stop_at_significant() { // prefixed expression with '!' or '-' Reading::Atomic(Token::Punctuation(punc)) if matches!(punc.punctuation, '!' | '-') => { // eat the prefix @@ -702,6 +744,21 @@ impl Parser<'_> { Err(err) } + }?; + + match self.stop_at_significant() { + Reading::Atomic(Token::Punctuation(punc)) if punc.punctuation == '.' => { + self.forward(); + + let member = self.parse_identifier(handler)?; + + Ok(Primary::MemberAccess(MemberAccess { + parent: Box::new(prim), + dot: punc, + member, + })) + } + _ => Ok(prim), } } diff --git a/src/transpile/expression.rs b/src/transpile/expression.rs index 36814d4..f4d7dd9 100644 --- a/src/transpile/expression.rs +++ b/src/transpile/expression.rs @@ -17,12 +17,13 @@ use super::{ }, Scope, TranspileResult, Transpiler, VariableData, }; +use crate::lexical::token::{Identifier, StringLiteral}; #[cfg(feature = "shulkerbox")] use crate::{ base::{self, source_file::SourceElement, Handler, VoidHandler}, lexical::token::MacroStringLiteralPart, syntax::syntax_tree::expression::{ - Binary, BinaryOperator, Expression, PrefixOperator, Primary, + Binary, BinaryOperator, Expression, MemberAccess, PrefixOperator, Primary, }, transpile::{ error::{FunctionArgumentsNotAllowed, MissingValue}, @@ -319,6 +320,9 @@ impl Primary { false } } + Self::MemberAccess(_) => { + todo!() + } #[cfg_attr(not(feature = "lua"), expect(unused_variables))] Self::Lua(lua) => { cfg_if::cfg_if! { @@ -379,6 +383,11 @@ impl Primary { Self::Parenthesized(parenthesized) => { parenthesized.expression().comptime_eval(scope, handler) } + Self::MemberAccess(member_access) => { + member_access + .parent() + .comptime_member_access(member_access, scope, handler) + } Self::Prefix(prefix) => { prefix .operand() @@ -419,6 +428,176 @@ impl Primary { } } } + + fn comptime_member_access( + &self, + member_access: &MemberAccess, + scope: &Arc, + handler: &impl Handler, + ) -> Result { + match self { + Self::StringLiteral(s) => s.comptime_member_access(member_access, scope, handler), + Self::Identifier(ident) => ident.comptime_member_access(member_access, scope, handler), + + _ => todo!(), + } + } +} + +impl StringLiteral { + fn comptime_member_access( + &self, + member_access: &MemberAccess, + _scope: &Arc, + _handler: &impl Handler, + ) -> Result { + match member_access.member().span.str() { + "length" => Ok(ComptimeValue::Integer( + i64::try_from(self.str_content().len()) + .expect("string literal length should fit in i64"), + )), + _ => Err(NotComptime { + expression: member_access.member().span(), + }), + } + } +} + +impl Identifier { + #[expect(clippy::too_many_lines)] + fn comptime_member_access( + &self, + member_access: &MemberAccess, + scope: &Arc, + _handler: &impl Handler, + ) -> Result { + scope.get_variable(self.span.str()).map_or_else( + || { + Err(NotComptime { + expression: self.span(), + }) + }, + |data| match data.as_ref() { + VariableData::ComptimeValue { value, .. } => { + let value = value.read().unwrap(); + value.as_ref().map_or_else( + || { + Err(NotComptime { + expression: self.span(), + }) + }, + |value| match value { + ComptimeValue::String(s) => match member_access.member().span.str() { + "length" => Ok(ComptimeValue::Integer( + i64::try_from(s.len()) + .expect("comptime string length should fit in i64"), + )), + _ => Err(NotComptime { + expression: member_access.member().span(), + }), + }, + _ => Err(NotComptime { + expression: self.span(), + }), + }, + ) + } + VariableData::BooleanStorage { storage_name, path } => { + match member_access.member().span.str() { + "storage" => Ok(ComptimeValue::String(storage_name.to_owned())), + "path" => Ok(ComptimeValue::String(path.to_owned())), + _ => Err(NotComptime { + expression: member_access.member().span(), + }), + } + } + VariableData::BooleanStorageArray { + storage_name, + paths: _, + } => { + match member_access.member().span.str() { + "storage" => Ok(ComptimeValue::String(storage_name.to_owned())), + "paths" => { + // TODO: implement when comptime arrays are implemented + Err(NotComptime { + expression: member_access.member().span(), + }) + } + _ => Err(NotComptime { + expression: member_access.member().span(), + }), + } + } + VariableData::Function { path, .. } => { + match member_access.member().span.str() { + "path" => { + #[expect(clippy::option_if_let_else)] + if let Some(path) = path.get() { + Ok(ComptimeValue::String(path.to_owned())) + } else { + // TODO: add support for non already compiled functions + Err(NotComptime { + expression: member_access.member().span(), + }) + } + } + _ => Err(NotComptime { + expression: member_access.member().span(), + }), + } + } + VariableData::InternalFunction { .. } => Err(NotComptime { + expression: member_access.member().span(), + }), + VariableData::MacroParameter { macro_name, .. } => { + match member_access.member().span.str() { + "name" => Ok(ComptimeValue::String(macro_name.to_owned())), + _ => Err(NotComptime { + expression: member_access.member().span(), + }), + } + } + VariableData::Scoreboard { objective } => match member_access.member().span.str() { + "objective" => Ok(ComptimeValue::String(objective.to_owned())), + _ => Err(NotComptime { + expression: member_access.member().span(), + }), + }, + VariableData::ScoreboardArray { + objective, + targets: _, + } => { + match member_access.member().span.str() { + "objective" => Ok(ComptimeValue::String(objective.to_owned())), + "targets" => { + // TODO: implement when comptime arrays are implemented + Err(NotComptime { + expression: member_access.member().span(), + }) + } + _ => Err(NotComptime { + expression: member_access.member().span(), + }), + } + } + VariableData::ScoreboardValue { objective, target } => { + match member_access.member().span.str() { + "objective" => Ok(ComptimeValue::String(objective.to_owned())), + "target" => Ok(ComptimeValue::String(target.to_owned())), + _ => Err(NotComptime { + expression: member_access.member().span(), + }), + } + } + VariableData::Tag { tag_name } => match member_access.member().span.str() { + "name" => Ok(ComptimeValue::String(tag_name.to_owned())), + _ => Err(NotComptime { + expression: member_access.member().span(), + }), + }, + }, + ) + } } #[cfg(feature = "shulkerbox")] @@ -699,6 +878,7 @@ impl Transpiler { Primary::Parenthesized(parenthesized) => { self.transpile_expression(parenthesized.expression(), target, scope, handler) } + Primary::MemberAccess(_) => todo!(), Primary::Lua(lua) => { #[expect(clippy::option_if_let_else)] @@ -1261,6 +1441,7 @@ impl Transpiler { Err(err) } } + Primary::MemberAccess(_) => todo!(), Primary::Prefix(prefix) => match prefix.operator() { PrefixOperator::LogicalNot(_) => { let (cmds, cond) = self.transpile_primary_expression_as_condition( diff --git a/src/transpile/function.rs b/src/transpile/function.rs index cfd871e..4bc7e76 100644 --- a/src/transpile/function.rs +++ b/src/transpile/function.rs @@ -357,6 +357,7 @@ impl Transpiler { } } } + Expression::Primary(Primary::MemberAccess(_)) => todo!(), Expression::Primary( Primary::Parenthesized(_) | Primary::Prefix(_) diff --git a/src/transpile/transpiler.rs b/src/transpile/transpiler.rs index 033566f..b5044a9 100644 --- a/src/transpile/transpiler.rs +++ b/src/transpile/transpiler.rs @@ -653,6 +653,8 @@ impl Transpiler { } }, + Primary::MemberAccess(_) => todo!(), + Primary::Parenthesized(parenthesized) => match parenthesized.expression().as_ref() { Expression::Primary(expression) => { self.transpile_run_expression(expression, scope, handler) @@ -660,7 +662,7 @@ impl Transpiler { 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())]), - _ => { + Ok(_) => { let err = TranspileError::MismatchedTypes(MismatchedTypes { expression: bin.span(), expected_type: ExpectedType::String, @@ -668,6 +670,11 @@ impl Transpiler { handler.receive(err.clone()); Err(err) } + Err(not_comptime) => { + let err = TranspileError::NotComptime(not_comptime); + handler.receive(err.clone()); + Err(err) + } }, }, }