From cf71af7c53e0ddf6a3688c1d47465da2201dc04f Mon Sep 17 00:00:00 2001 From: Teakowa Date: Thu, 29 Oct 2026 18:23:05 +0800 Subject: [PATCH 1/5] fix(semantics): reject eventPlayer in global initializers Validate global declaration values through the existing HIR visitor, including nested and spanless receiver expressions. Preserve player initializer context and cover rejection parity with the pinned OverPy differential fixture. Refs #673 --- crates/opy-provider/src/edits.rs | 199 +----------------- crates/opy-provider/src/edits/diagnostics.rs | 198 +++++++++++++++++ .../compiler/tests/variable_initializers.rs | 28 +++ crates/opy-rs/src/lower/declarations.rs | 26 ++- .../fixture.json | 25 +++ .../oracle.json | 39 ++++ .../source.opy | 1 + 7 files changed, 319 insertions(+), 197 deletions(-) create mode 100644 crates/opy-provider/src/edits/diagnostics.rs create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-event-player-invalid/fixture.json create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-event-player-invalid/oracle.json create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-event-player-invalid/source.opy diff --git a/crates/opy-provider/src/edits.rs b/crates/opy-provider/src/edits.rs index 34b37013..cc5ddcbd 100644 --- a/crates/opy-provider/src/edits.rs +++ b/crates/opy-provider/src/edits.rs @@ -14,203 +14,10 @@ use crate::{ path_to_file_uri, resolved_path, same_path, validate_documents, }; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum DiagnosticClass { - DocumentSyntax, - MissingSource, - NameBinding, - MergedTable, - MacroTable, - Script, - ExternalFile, - Catalog, - Capability, - Semantic, - Internal, - WarningOnly, -} - -const CODE_CLASSES: &[(DiagnosticClass, &[&str])] = &[ - ( - DiagnosticClass::DocumentSyntax, - &[ - "lex-error", - "parse-error", - "workshop-source", - "directive-invalid", - "unsupported-directive", - "include-invalid", - "define-invalid", - "main-file-invalid", - "main-file-placement", - "rule-prefix-invalid", - "settings-invalid", - "settings-placement", - "macro-invalid", - "do-while-placement", - "invalid-string-entity", - "unknown-string-entity", - "script-invalid", - "enum-increment", - ], - ), - ( - DiagnosticClass::MissingSource, - &[ - "include-not-found", - "main-file-not-found", - "script-not-found", - "unknown-identifier", - "unknown-action", - "unknown-value", - "unknown-member", - ], - ), - ( - DiagnosticClass::NameBinding, - &[ - "duplicate-argument", - "duplicate-declaration", - "duplicate-definition", - "duplicate-rule-name", - "enum-type-without-member", - "invalid-arity", - "vect-arity", - "unresolved-reference", - "unknown-keyword", - ], - ), - ( - DiagnosticClass::MergedTable, - &[ - "rule-prefix-template-duplicate", - "replacement-duplicate", - "post-compile-hook-duplicate", - "macro-redeclaration", - ], - ), - ( - DiagnosticClass::MacroTable, - &["macro-arity", "macro-recursion"], - ), - ( - DiagnosticClass::Script, - &[ - "script-error", - "script-timeout", - "script-stack-limit", - "script-memory-limit", - "script-result-not-string", - "script-internal", - ], - ), - ( - DiagnosticClass::ExternalFile, - &[ - "include-cycle", - "settings-not-found", - "translations-invalid", - "settings-expression", - "rule-prefix-template-invalid", - ], - ), - ( - DiagnosticClass::Catalog, - &[ - "catalog-error", - "manifest-error", - "unknown-enum-member", - "missing-mapping", - "unknown-texture-member", - ], - ), - ( - DiagnosticClass::Capability, - &[ - "backend-directive-unsupported", - "four-dimensional-assignment", - "four-dimensional-delete", - "index-collision", - "index-exhausted", - "index-out-of-range", - "random-indexed-delete", - "split-dict-array-arguments", - "split-dict-array-arity", - "split-dict-array-key", - "symbol-collision", - "workshop-action-layout", - "unsupported-action-call", - "unsupported-construct", - "unsupported-control-flow", - "unsupported-dangling", - "unsupported-duplicate-name", - "unsupported-enum-domain", - "unsupported-enum-domain-mismatch", - "unsupported-enum-member", - "unsupported-index", - "unsupported-indexed-initializer", - "unsupported-integration-surface", - "unsupported-invalid-argument", - "unsupported-invalid-arity", - "unsupported-localized-string", - "unsupported-member", - "unsupported-member-assignment", - "unsupported-missing-argument", - "unsupported-modify-op", - "unsupported-non-finite-number", - "unsupported-rule-order", - "unsupported-settings", - "unsupported-subroutine-value", - "unsupported-value-call", - ], - ), - ( - DiagnosticClass::Semantic, - &[ - "action-in-value-position", - "break-context", - "continue-context", - "dict-access", - "invalid-argument", - "invalid-call-context", - "invalid-identifier", - "invalid-iterable", - "invalid-range-binder", - "invalid-receiver", - "invalid-span", - "invalid-string-modifier", - "invalid-structure", - "keyword-required", - "keyword-unsupported", - "lambda-context", - "missing-argument", - "positional-after-keyword", - "settings-hero-lists", - "settings-workshop-value", - "tabular-arguments", - "value-in-action-position", - ], - ), - ( - DiagnosticClass::Internal, - &[ - "incompatible-protocol", - "malformed-payload", - "unsupported-node", - "workshop-emission", - ], - ), - (DiagnosticClass::WarningOnly, &["unknown-setting"]), -]; - -fn class_of(code: &str) -> Option { - CODE_CLASSES - .iter() - .find_map(|(class, codes)| codes.contains(&code).then_some(*class)) -} - +mod diagnostics; #[cfg(test)] -const NON_DIAGNOSTIC_LITERALS: &[&str] = &["kebab-case", "non-empty"]; +use diagnostics::{CODE_CLASSES, NON_DIAGNOSTIC_LITERALS}; +use diagnostics::{DiagnosticClass, class_of}; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/crates/opy-provider/src/edits/diagnostics.rs b/crates/opy-provider/src/edits/diagnostics.rs new file mode 100644 index 00000000..6add13af --- /dev/null +++ b/crates/opy-provider/src/edits/diagnostics.rs @@ -0,0 +1,198 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum DiagnosticClass { + DocumentSyntax, + MissingSource, + NameBinding, + MergedTable, + MacroTable, + Script, + ExternalFile, + Catalog, + Capability, + Semantic, + Internal, + WarningOnly, +} + +pub(super) const CODE_CLASSES: &[(DiagnosticClass, &[&str])] = &[ + ( + DiagnosticClass::DocumentSyntax, + &[ + "lex-error", + "parse-error", + "workshop-source", + "directive-invalid", + "unsupported-directive", + "include-invalid", + "define-invalid", + "main-file-invalid", + "main-file-placement", + "rule-prefix-invalid", + "settings-invalid", + "settings-placement", + "macro-invalid", + "do-while-placement", + "invalid-string-entity", + "unknown-string-entity", + "script-invalid", + "enum-increment", + ], + ), + ( + DiagnosticClass::MissingSource, + &[ + "include-not-found", + "main-file-not-found", + "script-not-found", + "unknown-identifier", + "unknown-action", + "unknown-value", + "unknown-member", + ], + ), + ( + DiagnosticClass::NameBinding, + &[ + "duplicate-argument", + "duplicate-declaration", + "duplicate-definition", + "duplicate-rule-name", + "enum-type-without-member", + "invalid-arity", + "vect-arity", + "unresolved-reference", + "unknown-keyword", + ], + ), + ( + DiagnosticClass::MergedTable, + &[ + "rule-prefix-template-duplicate", + "replacement-duplicate", + "post-compile-hook-duplicate", + "macro-redeclaration", + ], + ), + ( + DiagnosticClass::MacroTable, + &["macro-arity", "macro-recursion"], + ), + ( + DiagnosticClass::Script, + &[ + "script-error", + "script-timeout", + "script-stack-limit", + "script-memory-limit", + "script-result-not-string", + "script-internal", + ], + ), + ( + DiagnosticClass::ExternalFile, + &[ + "include-cycle", + "settings-not-found", + "translations-invalid", + "settings-expression", + "rule-prefix-template-invalid", + ], + ), + ( + DiagnosticClass::Catalog, + &[ + "catalog-error", + "manifest-error", + "unknown-enum-member", + "missing-mapping", + "unknown-texture-member", + ], + ), + ( + DiagnosticClass::Capability, + &[ + "backend-directive-unsupported", + "four-dimensional-assignment", + "four-dimensional-delete", + "index-collision", + "index-exhausted", + "index-out-of-range", + "random-indexed-delete", + "split-dict-array-arguments", + "split-dict-array-arity", + "split-dict-array-key", + "symbol-collision", + "workshop-action-layout", + "unsupported-action-call", + "unsupported-construct", + "unsupported-control-flow", + "unsupported-dangling", + "unsupported-duplicate-name", + "unsupported-enum-domain", + "unsupported-enum-domain-mismatch", + "unsupported-enum-member", + "unsupported-index", + "unsupported-indexed-initializer", + "unsupported-integration-surface", + "unsupported-invalid-argument", + "unsupported-invalid-arity", + "unsupported-localized-string", + "unsupported-member", + "unsupported-member-assignment", + "unsupported-missing-argument", + "unsupported-modify-op", + "unsupported-non-finite-number", + "unsupported-rule-order", + "unsupported-settings", + "unsupported-subroutine-value", + "unsupported-value-call", + ], + ), + ( + DiagnosticClass::Semantic, + &[ + "action-in-value-position", + "break-context", + "continue-context", + "dict-access", + "invalid-argument", + "invalid-call-context", + "invalid-identifier", + "invalid-iterable", + "invalid-range-binder", + "invalid-receiver", + "invalid-span", + "invalid-string-modifier", + "invalid-structure", + "keyword-required", + "keyword-unsupported", + "lambda-context", + "missing-argument", + "positional-after-keyword", + "rule-event-gate", + "settings-hero-lists", + "settings-workshop-value", + "tabular-arguments", + "value-in-action-position", + ], + ), + ( + DiagnosticClass::Internal, + &[ + "incompatible-protocol", + "malformed-payload", + "unsupported-node", + "workshop-emission", + ], + ), + (DiagnosticClass::WarningOnly, &["unknown-setting"]), +]; + +pub(super) fn class_of(code: &str) -> Option { + CODE_CLASSES + .iter() + .find_map(|(class, codes)| codes.contains(&code).then_some(*class)) +} + +#[cfg(test)] +pub(super) const NON_DIAGNOSTIC_LITERALS: &[&str] = &["kebab-case", "non-empty"]; diff --git a/crates/opy-rs/src/compiler/tests/variable_initializers.rs b/crates/opy-rs/src/compiler/tests/variable_initializers.rs index 9336d733..d037cb8c 100644 --- a/crates/opy-rs/src/compiler/tests/variable_initializers.rs +++ b/crates/opy-rs/src/compiler/tests/variable_initializers.rs @@ -2,6 +2,34 @@ use std::path::Path; use crate::Compiler; +#[test] +fn global_initializers_reject_event_player_inside_expressions() { + for value in [ + "eventPlayer", + "eventPlayer()", + "[eventPlayer]", + "eventPlayer.getPosition()", + ] { + let source = format!("globalvar target = {value}\n"); + let error = crate::compile(&source, "initializer.opy", Path::new(".")).unwrap_err(); + assert_eq!(error.code, "rule-event-gate", "{value}"); + assert_eq!(error.span.unwrap().start.line, 1, "{value}"); + } +} + +#[test] +fn player_initializers_keep_event_player_context() { + let source = "playervar target = eventPlayer\n"; + let hir = crate::compile(source, "initializer.opy", Path::new(".")).unwrap(); + let artifact = Compiler::new().unwrap().compile_hir(&hir).unwrap(); + assert!(matches!( + artifact.wir.rules[0].actions.first(), + Some(workshop_rs::Action::SetPlayerVariable { player, value, .. }) + if matches!(player, workshop_rs::Value::EventPlayer) + && matches!(value, workshop_rs::Value::EventPlayer) + )); +} + #[test] fn explicit_null_initializers_are_emitted_for_global_and_player_variables() { let source = r#" diff --git a/crates/opy-rs/src/lower/declarations.rs b/crates/opy-rs/src/lower/declarations.rs index 3cfa5ee9..2543e9b4 100644 --- a/crates/opy-rs/src/lower/declarations.rs +++ b/crates/opy-rs/src/lower/declarations.rs @@ -24,7 +24,7 @@ impl Lowerer { index: *index, span: Some(span.into()), name_span: Some(name_span.into()), - initializer: self.initializer(initializer.as_ref()), + initializer: self.global_initializer(initializer.as_ref()), }), Decl::PlayerVariable { name, @@ -191,6 +191,30 @@ impl Lowerer { declaration_visible(&self.enum_declarations, name, self.current_order) } + fn global_initializer(&mut self, initializer: Option<&Expr>) -> Option> { + use crate::hir::visit::{self, Visitor}; + + struct EventPlayerCheck<'a>(&'a mut Lowerer, Span); + + impl Visitor for EventPlayerCheck<'_> { + fn visit_expr(&mut self, expression: &HirExpr) { + if matches!(expression, HirExpr::EventPlayer { .. }) { + self.0.error_at( + "rule-event-gate", + "Cannot use 'eventPlayer' with rule event 'global'".to_string(), + self.1, + ); + } + visit::walk_expr(self, expression); + } + } + + let initializer = initializer?; + let value = self.initializer(Some(initializer))?; + EventPlayerCheck(self, initializer.span()).visit_expr(&value); + Some(value) + } + /// A declaration initializer: integer-`0` literal initializers are /// dropped (matching the reference adapter, which drops `h = 0` but /// carries `j = 5` and `k = 0.0`); other initializers are kept. diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-event-player-invalid/fixture.json b/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-event-player-invalid/fixture.json new file mode 100644 index 00000000..89abe831 --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-event-player-invalid/fixture.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": 1, + "id": "synthetic/global-initializer-event-player-invalid", + "category": "diagnostics", + "features": [ + "declaration initializers", + "event context" + ], + "source": "source.opy", + "tests": { + "source": { + "nativeStatus": "failure", + "relationship": "match", + "ruleNames": false, + "diagnosticCode": "rule-event-gate" + }, + "compiler": { + "nativeStatus": "failure", + "relationship": "match", + "comparison": "diagnostic-code", + "diagnosticCode": "rule-event-gate", + "failureClass": "frontend" + } + } +} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-event-player-invalid/oracle.json b/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-event-player-invalid/oracle.json new file mode 100644 index 00000000..d01d2718 --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-event-player-invalid/oracle.json @@ -0,0 +1,39 @@ +{ + "compile": { + "diagnostics": [ + { + "severity": "error", + "text": "Error: Cannot use 'eventPlayer' with rule event 'global'\n | line 1, col 20, at source.opy" + } + ], + "exitCode": 1, + "status": "failure", + "stdout": "", + "workshop": "", + "workshopExact": "", + "workshopSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "fixture": "synthetic/global-initializer-event-player-invalid", + "input": { + "files": [ + { + "path": "source.opy", + "sha256": "6279468fe6ed59b12ac9c179a47e9572b70c4af52d7147c0c65eb13cb47f2f7b" + } + ], + "sha256": "24a17151c72b71c36a6eb3c24702f7d2f84023988fe96e6204914301b13ac471", + "source": "source.opy" + }, + "oracle": { + "contentCommit": "5a7d0e294b8cad73b9701987bb584d0551d7fa4d", + "gitHead": "ec763c1d245aa5485ef61af5b2bda4975bce4877", + "integrity": "sha512-C5F6btT5yn19BvHuE0bjMuSd/2esebBabmpL4SYLQFuJ4IdouVOKEoMLLuJFAyNc0Phwrolblj5GDkDGYR0ofg==", + "language": "en-US", + "license": "GPL-3.0-only", + "name": "overpy", + "registryTarball": "https://registry.npmjs.org/overpy/-/overpy-9.7.17.tgz", + "repository": "https://github.com/Zezombye/overpy", + "version": "9.7.17" + }, + "schemaVersion": 1 +} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-event-player-invalid/source.opy b/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-event-player-invalid/source.opy new file mode 100644 index 00000000..e17f2e00 --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-event-player-invalid/source.opy @@ -0,0 +1 @@ +globalvar target = eventPlayer From b33fb6ceff225638aa9450533a3e8ffaef4f346d Mon Sep 17 00:00:00 2001 From: Teakowa Date: Thu, 29 Oct 2026 18:41:15 +0800 Subject: [PATCH 2/5] fix(parser): accept unary plus with upstream precedence Treat unary plus as an identity, including adjacent signs combined by the lexer, while preserving postfix assignments and prefix-statement rejection. Add a separate oracle-backed fixture comparing canonical Workshop programs for initializer values, mixed signs, exponentiation and nonnumeric operands. Fixes #673 --- crates/opy-rs/src/parser/expressions.rs | 80 +--------------- .../src/parser/expressions/arithmetic.rs | 91 +++++++++++++++++++ crates/opy-rs/src/parser/statements.rs | 4 + .../corpus/synthetic/unary-plus/fixture.json | 25 +++++ .../corpus/synthetic/unary-plus/oracle.json | 34 +++++++ .../corpus/synthetic/unary-plus/source.opy | 22 +++++ 6 files changed, 178 insertions(+), 78 deletions(-) create mode 100644 crates/opy-rs/src/parser/expressions/arithmetic.rs create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus/fixture.json create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus/oracle.json create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus/source.opy diff --git a/crates/opy-rs/src/parser/expressions.rs b/crates/opy-rs/src/parser/expressions.rs index f2020f4b..7fac6d48 100644 --- a/crates/opy-rs/src/parser/expressions.rs +++ b/crates/opy-rs/src/parser/expressions.rs @@ -1,5 +1,7 @@ use super::*; +mod arithmetic; + impl Parser<'_> { pub(super) fn parse_expr(&mut self) -> Result { self.parse_expr_inner(false) @@ -111,84 +113,6 @@ impl Parser<'_> { Ok(left) } - fn parse_additive(&mut self) -> Result { - self.skip_expression_newlines(); - let mut left = self.parse_multiplicative()?; - loop { - self.skip_expression_newlines(); - if self.peek_kind() == TokenKind::Decrement - && !matches!(self.peek_at(1).kind, TokenKind::Newline | TokenKind::Eof) - { - let operator = self.advance(); - let operand = self.parse_unary()?; - let unary = unary_op("-", operator.span, operand); - let right = self.parse_multiplicative_tail(unary)?; - left = binary("-", left, right); - continue; - } - let op = match self.peek_kind() { - TokenKind::Plus => "+", - TokenKind::Minus => "-", - _ => break, - }; - self.bump(); - self.skip_expression_newlines(); - let right = self.parse_multiplicative()?; - left = binary(op, left, right); - } - Ok(left) - } - - fn parse_multiplicative(&mut self) -> Result { - self.skip_expression_newlines(); - let left = self.parse_unary()?; - self.parse_multiplicative_tail(left) - } - - fn parse_multiplicative_tail(&mut self, mut left: Expr) -> Result { - loop { - self.skip_expression_newlines(); - let op = match self.peek_kind() { - TokenKind::Star => "*", - TokenKind::Slash => "/", - TokenKind::Percent => "%", - _ => break, - }; - self.bump(); - self.skip_expression_newlines(); - let right = self.parse_unary()?; - left = binary(op, left, right); - } - Ok(left) - } - - fn parse_unary(&mut self) -> Result { - self.skip_expression_newlines(); - if matches!(self.peek_kind(), TokenKind::Minus | TokenKind::Decrement) { - let start = self.advance(); - self.skip_expression_newlines(); - let operand = self.parse_unary()?; - let unary = unary_op("-", start.span, operand); - if start.kind == TokenKind::Decrement { - return Ok(unary_op("-", start.span, unary)); - } - return Ok(unary); - } - self.parse_power() - } - - fn parse_power(&mut self) -> Result { - let base = self.parse_postfix()?; - self.skip_expression_newlines(); - if self.peek_kind() == TokenKind::DoubleStar { - self.bump(); - self.skip_expression_newlines(); - let exponent = self.parse_unary()?; - return Ok(binary("**", base, exponent)); - } - Ok(base) - } - pub(super) fn parse_postfix(&mut self) -> Result { let mut base = self.parse_primary()?; loop { diff --git a/crates/opy-rs/src/parser/expressions/arithmetic.rs b/crates/opy-rs/src/parser/expressions/arithmetic.rs new file mode 100644 index 00000000..265dc47f --- /dev/null +++ b/crates/opy-rs/src/parser/expressions/arithmetic.rs @@ -0,0 +1,91 @@ +use super::*; + +impl Parser<'_> { + pub(super) fn parse_additive(&mut self) -> Result { + self.skip_expression_newlines(); + let mut left = self.parse_multiplicative()?; + loop { + self.skip_expression_newlines(); + if matches!( + self.peek_kind(), + TokenKind::Increment | TokenKind::Decrement + ) && !matches!(self.peek_at(1).kind, TokenKind::Newline | TokenKind::Eof) + { + let operator = self.advance(); + let operand = self.parse_unary()?; + let (op, right) = if operator.kind == TokenKind::Decrement { + ("-", unary_op("-", operator.span, operand)) + } else { + ("+", operand) + }; + let right = self.parse_multiplicative_tail(right)?; + left = binary(op, left, right); + continue; + } + let op = match self.peek_kind() { + TokenKind::Plus => "+", + TokenKind::Minus => "-", + _ => break, + }; + self.bump(); + self.skip_expression_newlines(); + let right = self.parse_multiplicative()?; + left = binary(op, left, right); + } + Ok(left) + } + + fn parse_multiplicative(&mut self) -> Result { + self.skip_expression_newlines(); + let left = self.parse_unary()?; + self.parse_multiplicative_tail(left) + } + + fn parse_multiplicative_tail(&mut self, mut left: Expr) -> Result { + loop { + self.skip_expression_newlines(); + let op = match self.peek_kind() { + TokenKind::Star => "*", + TokenKind::Slash => "/", + TokenKind::Percent => "%", + _ => break, + }; + self.bump(); + self.skip_expression_newlines(); + let right = self.parse_unary()?; + left = binary(op, left, right); + } + Ok(left) + } + + fn parse_unary(&mut self) -> Result { + self.skip_expression_newlines(); + if matches!(self.peek_kind(), TokenKind::Plus | TokenKind::Increment) { + self.bump(); + return self.parse_unary(); + } + if matches!(self.peek_kind(), TokenKind::Minus | TokenKind::Decrement) { + let start = self.advance(); + self.skip_expression_newlines(); + let operand = self.parse_unary()?; + let unary = unary_op("-", start.span, operand); + if start.kind == TokenKind::Decrement { + return Ok(unary_op("-", start.span, unary)); + } + return Ok(unary); + } + self.parse_power() + } + + fn parse_power(&mut self) -> Result { + let base = self.parse_postfix()?; + self.skip_expression_newlines(); + if self.peek_kind() == TokenKind::DoubleStar { + self.bump(); + self.skip_expression_newlines(); + let exponent = self.parse_unary()?; + return Ok(binary("**", base, exponent)); + } + Ok(base) + } +} diff --git a/crates/opy-rs/src/parser/statements.rs b/crates/opy-rs/src/parser/statements.rs index e1d0c4ab..bcec79d8 100644 --- a/crates/opy-rs/src/parser/statements.rs +++ b/crates/opy-rs/src/parser/statements.rs @@ -142,6 +142,10 @@ impl Parser<'_> { } pub(super) fn parse_expr_statement(&mut self) -> Result { + if self.peek_kind() == TokenKind::Increment { + self.error_at_current("prefix increment is not a standalone assignment".to_string()); + return Err(()); + } if self.peek_kind() == TokenKind::LParen { let save_pos = self.pos; let save_errors = self.errors.len(); diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus/fixture.json b/crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus/fixture.json new file mode 100644 index 00000000..b2a14a53 --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus/fixture.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": 1, + "id": "synthetic/unary-plus", + "category": "compilation", + "features": [ + "unary plus", + "operator precedence", + "declaration initializers", + "structural convergence" + ], + "source": "source.opy", + "tests": { + "source": { + "nativeStatus": "success", + "relationship": "match", + "ruleNames": true + }, + "compiler": { + "nativeStatus": "success", + "relationship": "match", + "comparison": "semantic-wir", + "semanticEquivalent": true + } + } +} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus/oracle.json b/crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus/oracle.json new file mode 100644 index 00000000..6deba094 --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus/oracle.json @@ -0,0 +1,34 @@ +{ + "compile": { + "diagnostics": [], + "exitCode": 0, + "status": "success", + "stdout": "", + "workshop": "variables {\n global:\n 0: initial\n 1: result\n player:\n 0: target\n}\n\nrule (\"Initialize global variables\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(initial, 1);\n }\n}\n\nrule (\"Initialize player variables\") {\n event {\n Ongoing - Each Player;\n All;\n All;\n }\n actions {\n Set Player Variable(Event Player, target, Event Player);\n }\n}\n\nrule (\"unary plus\") {\n event {\n Ongoing - Each Player;\n All;\n All;\n }\n actions {\n Set Global Variable(result, 1);\n Set Global Variable(result, Event Player);\n Set Global Variable(result, Vector(1, 2, 3));\n Set Global Variable(result, Multiply(Add(Global.result, 2), 3));\n Set Global Variable(result, 8);\n Modify Global Variable(result, Raise To Power, 2);\n Set Global Variable(result, Multiply(-1, Global.result));\n Set Global Variable(result, Add(1, Global.result));\n Set Global Variable(result, 1);\n Set Global Variable(result, 1);\n Set Global Variable(result, Add(1, Global.result));\n Set Global Variable(result, Add(1, Multiply(Global.result, 2)));\n Set Global Variable(result, True);\n Set Global Variable(result, Custom String(\"x\"));\n Modify Global Variable(result, Add, 1);\n }\n}\n", + "workshopExact": "variables {\n global:\n 0: initial\n 1: result\n player:\n 0: target\n}\n\nrule (\"Initialize global variables\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(initial, 1);\n }\n}\n\nrule (\"Initialize player variables\") {\n event {\n Ongoing - Each Player;\n All;\n All;\n }\n actions {\n Set Player Variable(Event Player, target, Event Player);\n }\n}\n\nrule (\"unary plus\") {\n event {\n Ongoing - Each Player;\n All;\n All;\n }\n actions {\n Set Global Variable(result, 1);\n Set Global Variable(result, Event Player);\n Set Global Variable(result, Vector(1, 2, 3));\n Set Global Variable(result, Multiply(Add(Global.result, 2), 3));\n Set Global Variable(result, 8);\n Modify Global Variable(result, Raise To Power, 2);\n Set Global Variable(result, Multiply(-1, Global.result));\n Set Global Variable(result, Add(1, Global.result));\n Set Global Variable(result, 1);\n Set Global Variable(result, 1);\n Set Global Variable(result, Add(1, Global.result));\n Set Global Variable(result, Add(1, Multiply(Global.result, 2)));\n Set Global Variable(result, True);\n Set Global Variable(result, Custom String(\"x\"));\n Modify Global Variable(result, Add, 1);\n }\n}\n\n", + "workshopSha256": "d01dd8f63d63f2b72319efb2759ac1776123c94c8a8292ef75f6522b0946b81c" + }, + "fixture": "synthetic/unary-plus", + "input": { + "files": [ + { + "path": "source.opy", + "sha256": "b6aaaf8e96a4f047d8fbc07a4af9803353ff824c6173fd7f4b7db2702a2895d3" + } + ], + "sha256": "2201b4341216c8bae46627423e53f91db50d9d8784f205ee7d6600bec804f5ff", + "source": "source.opy" + }, + "oracle": { + "contentCommit": "5a7d0e294b8cad73b9701987bb584d0551d7fa4d", + "gitHead": "ec763c1d245aa5485ef61af5b2bda4975bce4877", + "integrity": "sha512-C5F6btT5yn19BvHuE0bjMuSd/2esebBabmpL4SYLQFuJ4IdouVOKEoMLLuJFAyNc0Phwrolblj5GDkDGYR0ofg==", + "language": "en-US", + "license": "GPL-3.0-only", + "name": "overpy", + "registryTarball": "https://registry.npmjs.org/overpy/-/overpy-9.7.17.tgz", + "repository": "https://github.com/Zezombye/overpy", + "version": "9.7.17" + }, + "schemaVersion": 1 +} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus/source.opy b/crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus/source.opy new file mode 100644 index 00000000..fc6d75cb --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus/source.opy @@ -0,0 +1,22 @@ +playervar target = +eventPlayer +globalvar initial = +1 +globalvar result +rule "unary plus": + @Event eachPlayer + result = +1 + result = +result + result = +eventPlayer + result = +vect(1, 2, 3) + result = +(result + 2) * 3 + result = 2 ** +3 + result = +result ** 2 + result = +-result + result = +(+result) + result = 1 + +result + result = ++1 + result = +++1 + result = 1++result + result = 1++result * 2 + result = +true + result = +"x" + result++ From e73e5d650074c0682d2e1247466454fbbf59150a Mon Sep 17 00:00:00 2001 From: Teakowa Date: Thu, 29 Oct 2026 19:22:35 +0800 Subject: [PATCH 3/5] fix(semantics): validate global initializer definitions Follow constant values and expression macro bodies when checking global initializer event context, with a visited set to stop recursive references. Add separate pinned differential rejection fixtures for constants and macros. Refs #673 --- crates/opy-rs/src/lower.rs | 4 +- crates/opy-rs/src/lower/declarations.rs | 64 +++++++++++++++---- .../fixture.json | 25 ++++++++ .../oracle.json | 39 +++++++++++ .../source.opy | 3 + .../fixture.json | 25 ++++++++ .../oracle.json | 39 +++++++++++ .../source.opy | 5 ++ 8 files changed, 190 insertions(+), 14 deletions(-) create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-constant-event-player-invalid/fixture.json create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-constant-event-player-invalid/oracle.json create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-constant-event-player-invalid/source.opy create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-macro-event-player-invalid/fixture.json create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-macro-event-player-invalid/oracle.json create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-macro-event-player-invalid/source.opy diff --git a/crates/opy-rs/src/lower.rs b/crates/opy-rs/src/lower.rs index 11826102..22be807a 100644 --- a/crates/opy-rs/src/lower.rs +++ b/crates/opy-rs/src/lower.rs @@ -163,9 +163,7 @@ fn lower_program( lowerer.current_order = order; match item { TopLevel::Declaration(decl) => { - if let Some(declaration) = lowerer.lower_declaration(decl) { - declarations.push(declaration); - } + declarations.extend(lowerer.lower_declaration(decl, &declarations)); } TopLevel::Rule(CstRuleEntry::Rule(rule)) => rules.push(RuleEntry::Rule( lowerer.lower_rule(rule, files.as_slice(), preprocessing)?, diff --git a/crates/opy-rs/src/lower/declarations.rs b/crates/opy-rs/src/lower/declarations.rs index 2543e9b4..4f105571 100644 --- a/crates/opy-rs/src/lower/declarations.rs +++ b/crates/opy-rs/src/lower/declarations.rs @@ -11,7 +11,11 @@ fn declaration_visible( } impl Lowerer { - pub(super) fn lower_declaration(&mut self, decl: &Decl) -> Option { + pub(super) fn lower_declaration( + &mut self, + decl: &Decl, + declarations: &[Declaration], + ) -> Option { match decl { Decl::GlobalVariable { name, @@ -24,7 +28,7 @@ impl Lowerer { index: *index, span: Some(span.into()), name_span: Some(name_span.into()), - initializer: self.global_initializer(initializer.as_ref()), + initializer: self.global_initializer(initializer.as_ref(), declarations), }), Decl::PlayerVariable { name, @@ -191,19 +195,45 @@ impl Lowerer { declaration_visible(&self.enum_declarations, name, self.current_order) } - fn global_initializer(&mut self, initializer: Option<&Expr>) -> Option> { + fn global_initializer( + &mut self, + initializer: Option<&Expr>, + declarations: &[Declaration], + ) -> Option> { use crate::hir::visit::{self, Visitor}; - struct EventPlayerCheck<'a>(&'a mut Lowerer, Span); + struct EventPlayerCheck<'a> { + declarations: &'a [Declaration], + visited: HashSet<&'a str>, + found: bool, + } impl Visitor for EventPlayerCheck<'_> { fn visit_expr(&mut self, expression: &HirExpr) { - if matches!(expression, HirExpr::EventPlayer { .. }) { - self.0.error_at( - "rule-event-gate", - "Cannot use 'eventPlayer' with rule event 'global'".to_string(), - self.1, - ); + match expression { + HirExpr::EventPlayer { .. } => self.found = true, + HirExpr::Constant { name, .. } | HirExpr::MacroCall { name, .. } => { + for declaration in self.declarations { + match declaration { + Declaration::Constant { + name: defined, + value, + .. + } if defined == name && self.visited.insert(defined) => { + self.visit_expr(value); + } + Declaration::Macro { + name: defined, + body, + .. + } if defined == name && self.visited.insert(defined) => { + visit::walk_stmts(self, body); + } + _ => {} + } + } + } + _ => {} } visit::walk_expr(self, expression); } @@ -211,7 +241,19 @@ impl Lowerer { let initializer = initializer?; let value = self.initializer(Some(initializer))?; - EventPlayerCheck(self, initializer.span()).visit_expr(&value); + let mut check = EventPlayerCheck { + declarations, + visited: HashSet::new(), + found: false, + }; + check.visit_expr(&value); + if check.found { + self.error_at( + "rule-event-gate", + "Cannot use 'eventPlayer' with rule event 'global'".to_string(), + initializer.span(), + ); + } Some(value) } diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-constant-event-player-invalid/fixture.json b/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-constant-event-player-invalid/fixture.json new file mode 100644 index 00000000..ea402bed --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-constant-event-player-invalid/fixture.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": 1, + "id": "synthetic/global-initializer-constant-event-player-invalid", + "category": "diagnostics", + "features": [ + "declaration initializers", + "event context" + ], + "source": "source.opy", + "tests": { + "source": { + "nativeStatus": "failure", + "relationship": "match", + "ruleNames": false, + "diagnosticCode": "rule-event-gate" + }, + "compiler": { + "nativeStatus": "failure", + "relationship": "match", + "comparison": "diagnostic-code", + "diagnosticCode": "rule-event-gate", + "failureClass": "frontend" + } + } +} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-constant-event-player-invalid/oracle.json b/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-constant-event-player-invalid/oracle.json new file mode 100644 index 00000000..2b633733 --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-constant-event-player-invalid/oracle.json @@ -0,0 +1,39 @@ +{ + "compile": { + "diagnostics": [ + { + "severity": "error", + "text": "Error: Cannot use 'eventPlayer' with rule event 'global'\n | line 2, col 16, at source.opy" + } + ], + "exitCode": 1, + "status": "failure", + "stdout": "", + "workshop": "", + "workshopExact": "", + "workshopSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "fixture": "synthetic/global-initializer-constant-event-player-invalid", + "input": { + "files": [ + { + "path": "source.opy", + "sha256": "d32e111879579934051eb2cfdfed86f62b724309b074b9f23b7ada9a73669e23" + } + ], + "sha256": "3a70c640a8c2ec75136fa8a045da3ac5ba37547e716bd7bb94bea277c197c765", + "source": "source.opy" + }, + "oracle": { + "contentCommit": "5a7d0e294b8cad73b9701987bb584d0551d7fa4d", + "gitHead": "ec763c1d245aa5485ef61af5b2bda4975bce4877", + "integrity": "sha512-C5F6btT5yn19BvHuE0bjMuSd/2esebBabmpL4SYLQFuJ4IdouVOKEoMLLuJFAyNc0Phwrolblj5GDkDGYR0ofg==", + "language": "en-US", + "license": "GPL-3.0-only", + "name": "overpy", + "registryTarball": "https://registry.npmjs.org/overpy/-/overpy-9.7.17.tgz", + "repository": "https://github.com/Zezombye/overpy", + "version": "9.7.17" + }, + "schemaVersion": 1 +} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-constant-event-player-invalid/source.opy b/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-constant-event-player-invalid/source.opy new file mode 100644 index 00000000..54865e34 --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-constant-event-player-invalid/source.opy @@ -0,0 +1,3 @@ +macro p = eventPlayer +macro chain = [p] +globalvar target = chain diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-macro-event-player-invalid/fixture.json b/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-macro-event-player-invalid/fixture.json new file mode 100644 index 00000000..3ecab7c4 --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-macro-event-player-invalid/fixture.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": 1, + "id": "synthetic/global-initializer-macro-event-player-invalid", + "category": "diagnostics", + "features": [ + "declaration initializers", + "event context" + ], + "source": "source.opy", + "tests": { + "source": { + "nativeStatus": "failure", + "relationship": "match", + "ruleNames": false, + "diagnosticCode": "rule-event-gate" + }, + "compiler": { + "nativeStatus": "failure", + "relationship": "match", + "comparison": "diagnostic-code", + "diagnosticCode": "rule-event-gate", + "failureClass": "frontend" + } + } +} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-macro-event-player-invalid/oracle.json b/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-macro-event-player-invalid/oracle.json new file mode 100644 index 00000000..258ae902 --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-macro-event-player-invalid/oracle.json @@ -0,0 +1,39 @@ +{ + "compile": { + "diagnostics": [ + { + "severity": "error", + "text": "Error: Cannot use 'eventPlayer' with rule event 'global'\n | line 2, col 5, at source.opy\n | line 4, col 5, at source.opy\n | line 5, col 20, at source.opy" + } + ], + "exitCode": 1, + "status": "failure", + "stdout": "", + "workshop": "", + "workshopExact": "", + "workshopSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "fixture": "synthetic/global-initializer-macro-event-player-invalid", + "input": { + "files": [ + { + "path": "source.opy", + "sha256": "afd95840b6cb63f590b64d04fcb18a0158e696a4e4faf531386fb7fb6d0482ca" + } + ], + "sha256": "6b75483b6cab3bc469acf84ca49b9086d15dd511a172dfcf741aea72e7c18b3d", + "source": "source.opy" + }, + "oracle": { + "contentCommit": "5a7d0e294b8cad73b9701987bb584d0551d7fa4d", + "gitHead": "ec763c1d245aa5485ef61af5b2bda4975bce4877", + "integrity": "sha512-C5F6btT5yn19BvHuE0bjMuSd/2esebBabmpL4SYLQFuJ4IdouVOKEoMLLuJFAyNc0Phwrolblj5GDkDGYR0ofg==", + "language": "en-US", + "license": "GPL-3.0-only", + "name": "overpy", + "registryTarball": "https://registry.npmjs.org/overpy/-/overpy-9.7.17.tgz", + "repository": "https://github.com/Zezombye/overpy", + "version": "9.7.17" + }, + "schemaVersion": 1 +} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-macro-event-player-invalid/source.opy b/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-macro-event-player-invalid/source.opy new file mode 100644 index 00000000..f4a790be --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/global-initializer-macro-event-player-invalid/source.opy @@ -0,0 +1,5 @@ +macro p(): + eventPlayer +macro nested(): + p() +globalvar target = nested() From e1e4e1d04336298539e23cb5dcba14f21a2d1fe6 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Thu, 29 Oct 2026 19:22:51 +0800 Subject: [PATCH 4/5] fix(parser): allow repeated unary plus assignment targets Classify completed statements before rejecting prefix increment syntax, allowing repeated unary plus on assignment and postfix targets. Preserve bare-value rejection and the existing embedded-postfix diagnostic span. Add a canonical differential fixture covering the four reported forms. Refs #673 --- crates/opy-rs/src/parser/statements.rs | 13 ++++++- .../unary-plus-assignment/fixture.json | 25 ++++++++++++++ .../unary-plus-assignment/oracle.json | 34 +++++++++++++++++++ .../unary-plus-assignment/source.opy | 6 ++++ 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus-assignment/fixture.json create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus-assignment/oracle.json create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus-assignment/source.opy diff --git a/crates/opy-rs/src/parser/statements.rs b/crates/opy-rs/src/parser/statements.rs index bcec79d8..782e0825 100644 --- a/crates/opy-rs/src/parser/statements.rs +++ b/crates/opy-rs/src/parser/statements.rs @@ -142,7 +142,10 @@ impl Parser<'_> { } pub(super) fn parse_expr_statement(&mut self) -> Result { - if self.peek_kind() == TokenKind::Increment { + let starts_with_increment = self.peek_kind() == TokenKind::Increment; + if starts_with_increment + && matches!(self.peek_at(1).kind, TokenKind::Newline | TokenKind::Eof) + { self.error_at_current("prefix increment is not a standalone assignment".to_string()); return Err(()); } @@ -235,6 +238,14 @@ impl Parser<'_> { span, }) } + _ if starts_with_increment => { + self.errors.push(OpyError::at( + "parse-error", + "prefix increment is not a standalone assignment".to_string(), + start, + )); + Err(()) + } _ => { let end = expr.span().end; Ok(Stmt::Expr { diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus-assignment/fixture.json b/crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus-assignment/fixture.json new file mode 100644 index 00000000..2bbbc277 --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus-assignment/fixture.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": 1, + "id": "synthetic/unary-plus-assignment", + "category": "compilation", + "features": [ + "unary plus", + "operator precedence", + "declaration initializers", + "structural convergence" + ], + "source": "source.opy", + "tests": { + "source": { + "nativeStatus": "success", + "relationship": "match", + "ruleNames": true + }, + "compiler": { + "nativeStatus": "success", + "relationship": "match", + "comparison": "semantic-wir", + "semanticEquivalent": true + } + } +} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus-assignment/oracle.json b/crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus-assignment/oracle.json new file mode 100644 index 00000000..656d8bb0 --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus-assignment/oracle.json @@ -0,0 +1,34 @@ +{ + "compile": { + "diagnostics": [], + "exitCode": 0, + "status": "success", + "stdout": "", + "workshop": "variables {\n global:\n 0: x\n}\n\nrule (\"test\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(x, 2);\n Modify Global Variable(x, Add, 2);\n Set Global Variable(x, 2);\n Modify Global Variable(x, Add, 1);\n }\n}\n", + "workshopExact": "variables {\n global:\n 0: x\n}\n\nrule (\"test\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(x, 2);\n Modify Global Variable(x, Add, 2);\n Set Global Variable(x, 2);\n Modify Global Variable(x, Add, 1);\n }\n}\n\n", + "workshopSha256": "fbbc3b8800035726d8f34c63951a15cd37333a7b46f1b92f5fb4f868ffcb5579" + }, + "fixture": "synthetic/unary-plus-assignment", + "input": { + "files": [ + { + "path": "source.opy", + "sha256": "a372b6a52f8818639a12ec768ad1ef988ad934254aab402f0ef69621102b6db0" + } + ], + "sha256": "d9d0b31b6446719136779f1c6448e90d7071a480cb24453347e00928567d9d1d", + "source": "source.opy" + }, + "oracle": { + "contentCommit": "5a7d0e294b8cad73b9701987bb584d0551d7fa4d", + "gitHead": "ec763c1d245aa5485ef61af5b2bda4975bce4877", + "integrity": "sha512-C5F6btT5yn19BvHuE0bjMuSd/2esebBabmpL4SYLQFuJ4IdouVOKEoMLLuJFAyNc0Phwrolblj5GDkDGYR0ofg==", + "language": "en-US", + "license": "GPL-3.0-only", + "name": "overpy", + "registryTarball": "https://registry.npmjs.org/overpy/-/overpy-9.7.17.tgz", + "repository": "https://github.com/Zezombye/overpy", + "version": "9.7.17" + }, + "schemaVersion": 1 +} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus-assignment/source.opy b/crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus-assignment/source.opy new file mode 100644 index 00000000..76100422 --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/unary-plus-assignment/source.opy @@ -0,0 +1,6 @@ +globalvar x +rule "test": + ++x = 2 + ++x += 2 + +++x = 2 + ++x++ From d9d0eda791e7ae71eb8e248c504cca26e6de5c8c Mon Sep 17 00:00:00 2001 From: Teakowa Date: Thu, 29 Oct 2026 20:07:31 +0800 Subject: [PATCH 5/5] fix: separate initializer constant and macro identities (#673) --- crates/opy-rs/src/lower/declarations.rs | 38 ++++++++++++------ .../fixture.json | 26 +++++++++++++ .../oracle.json | 39 +++++++++++++++++++ .../source.opy | 4 ++ .../fixture.json | 24 ++++++++++++ .../oracle.json | 34 ++++++++++++++++ .../source.opy | 5 +++ .../fixture.json | 26 +++++++++++++ .../oracle.json | 39 +++++++++++++++++++ .../source.opy | 4 ++ .../fixture.json | 24 ++++++++++++ .../oracle.json | 34 ++++++++++++++++ .../source.opy | 5 +++ 13 files changed, 290 insertions(+), 12 deletions(-) create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-invalid/fixture.json create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-invalid/oracle.json create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-invalid/source.opy create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-safe/fixture.json create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-safe/oracle.json create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-safe/source.opy create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-invalid/fixture.json create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-invalid/oracle.json create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-invalid/source.opy create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-safe/fixture.json create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-safe/oracle.json create mode 100644 crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-safe/source.opy diff --git a/crates/opy-rs/src/lower/declarations.rs b/crates/opy-rs/src/lower/declarations.rs index 4f105571..ad53ada4 100644 --- a/crates/opy-rs/src/lower/declarations.rs +++ b/crates/opy-rs/src/lower/declarations.rs @@ -204,7 +204,7 @@ impl Lowerer { struct EventPlayerCheck<'a> { declarations: &'a [Declaration], - visited: HashSet<&'a str>, + visited: HashSet<(std::mem::Discriminant, &'a str)>, found: bool, } @@ -214,19 +214,33 @@ impl Lowerer { HirExpr::EventPlayer { .. } => self.found = true, HirExpr::Constant { name, .. } | HirExpr::MacroCall { name, .. } => { for declaration in self.declarations { - match declaration { - Declaration::Constant { - name: defined, - value, - .. - } if defined == name && self.visited.insert(defined) => { + match (expression, declaration) { + ( + HirExpr::Constant { .. }, + Declaration::Constant { + name: defined, + value, + .. + }, + ) if defined == name + && self + .visited + .insert((std::mem::discriminant(declaration), defined)) => + { self.visit_expr(value); } - Declaration::Macro { - name: defined, - body, - .. - } if defined == name && self.visited.insert(defined) => { + ( + HirExpr::MacroCall { .. }, + Declaration::Macro { + name: defined, + body, + .. + }, + ) if defined == name + && self + .visited + .insert((std::mem::discriminant(declaration), defined)) => + { visit::walk_stmts(self, body); } _ => {} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-invalid/fixture.json b/crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-invalid/fixture.json new file mode 100644 index 00000000..e2557308 --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-invalid/fixture.json @@ -0,0 +1,26 @@ +{ + "schemaVersion": 1, + "id": "synthetic/constant-first-macro-initializer-invalid", + "category": "diagnostics", + "features": [ + "declaration initializers", + "event context", + "constant and callable macro namespaces" + ], + "source": "source.opy", + "tests": { + "source": { + "nativeStatus": "failure", + "relationship": "match", + "ruleNames": false, + "diagnosticCode": "rule-event-gate" + }, + "compiler": { + "nativeStatus": "failure", + "relationship": "match", + "comparison": "diagnostic-code", + "diagnosticCode": "rule-event-gate", + "failureClass": "frontend" + } + } +} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-invalid/oracle.json b/crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-invalid/oracle.json new file mode 100644 index 00000000..9393784d --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-invalid/oracle.json @@ -0,0 +1,39 @@ +{ + "compile": { + "diagnostics": [ + { + "severity": "error", + "text": "Error: Cannot use 'eventPlayer' with rule event 'global'\n | line 3, col 5, at source.opy\n | line 4, col 24, at source.opy" + } + ], + "exitCode": 1, + "status": "failure", + "stdout": "", + "workshop": "", + "workshopExact": "", + "workshopSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "fixture": "synthetic/constant-first-macro-initializer-invalid", + "input": { + "files": [ + { + "path": "source.opy", + "sha256": "4ae5da3417f76af7324148f1a2aeb2f9d7c3f26e044e61fb0efbcb870677c0a6" + } + ], + "sha256": "25bc658cf790b00b9daf2ab21ba6fd08e484bcfe5907a2e558f784c194f54185", + "source": "source.opy" + }, + "oracle": { + "contentCommit": "5a7d0e294b8cad73b9701987bb584d0551d7fa4d", + "gitHead": "ec763c1d245aa5485ef61af5b2bda4975bce4877", + "integrity": "sha512-C5F6btT5yn19BvHuE0bjMuSd/2esebBabmpL4SYLQFuJ4IdouVOKEoMLLuJFAyNc0Phwrolblj5GDkDGYR0ofg==", + "language": "en-US", + "license": "GPL-3.0-only", + "name": "overpy", + "registryTarball": "https://registry.npmjs.org/overpy/-/overpy-9.7.17.tgz", + "repository": "https://github.com/Zezombye/overpy", + "version": "9.7.17" + }, + "schemaVersion": 1 +} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-invalid/source.opy b/crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-invalid/source.opy new file mode 100644 index 00000000..81c0fd0c --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-invalid/source.opy @@ -0,0 +1,4 @@ +macro p = 1 +macro p(): + eventPlayer +globalvar target = [p, p()] diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-safe/fixture.json b/crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-safe/fixture.json new file mode 100644 index 00000000..59b33cf1 --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-safe/fixture.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 1, + "id": "synthetic/constant-first-macro-initializer-safe", + "category": "compilation", + "features": [ + "declaration initializers", + "event context", + "constant and callable macro namespaces" + ], + "source": "source.opy", + "tests": { + "source": { + "nativeStatus": "success", + "relationship": "match", + "ruleNames": true + }, + "compiler": { + "nativeStatus": "success", + "relationship": "match", + "comparison": "semantic-wir", + "semanticEquivalent": true + } + } +} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-safe/oracle.json b/crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-safe/oracle.json new file mode 100644 index 00000000..1ba84587 --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-safe/oracle.json @@ -0,0 +1,34 @@ +{ + "compile": { + "diagnostics": [], + "exitCode": 0, + "status": "success", + "stdout": "", + "workshop": "variables {\n global:\n 0: target\n player:\n 0: playerTarget\n}\n\nrule (\"Initialize global variables\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(target, 1);\n }\n}\n\nrule (\"Initialize player variables\") {\n event {\n Ongoing - Each Player;\n All;\n All;\n }\n actions {\n Set Player Variable(Event Player, playerTarget, Event Player);\n }\n}\n", + "workshopExact": "variables {\n global:\n 0: target\n player:\n 0: playerTarget\n}\n\nrule (\"Initialize global variables\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(target, 1);\n }\n}\n\nrule (\"Initialize player variables\") {\n event {\n Ongoing - Each Player;\n All;\n All;\n }\n actions {\n Set Player Variable(Event Player, playerTarget, Event Player);\n }\n}\n\n", + "workshopSha256": "31f5cdf794b0aac9089c83027ff555e374d2c6e6d0fa85140b98f3f3c51d2b77" + }, + "fixture": "synthetic/constant-first-macro-initializer-safe", + "input": { + "files": [ + { + "path": "source.opy", + "sha256": "d15687f4dfd578243bae80736a29b757ed7b8ff1f3e000790cfa785df0c89492" + } + ], + "sha256": "edb71f43ec5026813a6df26c82a301ce8073082163b4532fce50e1d53ac5dc84", + "source": "source.opy" + }, + "oracle": { + "contentCommit": "5a7d0e294b8cad73b9701987bb584d0551d7fa4d", + "gitHead": "ec763c1d245aa5485ef61af5b2bda4975bce4877", + "integrity": "sha512-C5F6btT5yn19BvHuE0bjMuSd/2esebBabmpL4SYLQFuJ4IdouVOKEoMLLuJFAyNc0Phwrolblj5GDkDGYR0ofg==", + "language": "en-US", + "license": "GPL-3.0-only", + "name": "overpy", + "registryTarball": "https://registry.npmjs.org/overpy/-/overpy-9.7.17.tgz", + "repository": "https://github.com/Zezombye/overpy", + "version": "9.7.17" + }, + "schemaVersion": 1 +} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-safe/source.opy b/crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-safe/source.opy new file mode 100644 index 00000000..f7ad82cb --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/constant-first-macro-initializer-safe/source.opy @@ -0,0 +1,5 @@ +macro p = eventPlayer +macro p(): + 1 +globalvar target = p() +playervar playerTarget = p diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-invalid/fixture.json b/crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-invalid/fixture.json new file mode 100644 index 00000000..4f5f1b26 --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-invalid/fixture.json @@ -0,0 +1,26 @@ +{ + "schemaVersion": 1, + "id": "synthetic/macro-first-constant-initializer-invalid", + "category": "diagnostics", + "features": [ + "declaration initializers", + "event context", + "constant and callable macro namespaces" + ], + "source": "source.opy", + "tests": { + "source": { + "nativeStatus": "failure", + "relationship": "match", + "ruleNames": false, + "diagnosticCode": "rule-event-gate" + }, + "compiler": { + "nativeStatus": "failure", + "relationship": "match", + "comparison": "diagnostic-code", + "diagnosticCode": "rule-event-gate", + "failureClass": "frontend" + } + } +} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-invalid/oracle.json b/crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-invalid/oracle.json new file mode 100644 index 00000000..f774c1b5 --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-invalid/oracle.json @@ -0,0 +1,39 @@ +{ + "compile": { + "diagnostics": [ + { + "severity": "error", + "text": "Error: Cannot use 'eventPlayer' with rule event 'global'\n | line 4, col 26, at source.opy" + } + ], + "exitCode": 1, + "status": "failure", + "stdout": "", + "workshop": "", + "workshopExact": "", + "workshopSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "fixture": "synthetic/macro-first-constant-initializer-invalid", + "input": { + "files": [ + { + "path": "source.opy", + "sha256": "1df59f21d4de16327de98b4968242200064900f135adc6b08a7ad9c1f19305bb" + } + ], + "sha256": "aa18365b4b262a0123e24b1072d84de4b9dd89d6d5caad98950aa193b7343b8e", + "source": "source.opy" + }, + "oracle": { + "contentCommit": "5a7d0e294b8cad73b9701987bb584d0551d7fa4d", + "gitHead": "ec763c1d245aa5485ef61af5b2bda4975bce4877", + "integrity": "sha512-C5F6btT5yn19BvHuE0bjMuSd/2esebBabmpL4SYLQFuJ4IdouVOKEoMLLuJFAyNc0Phwrolblj5GDkDGYR0ofg==", + "language": "en-US", + "license": "GPL-3.0-only", + "name": "overpy", + "registryTarball": "https://registry.npmjs.org/overpy/-/overpy-9.7.17.tgz", + "repository": "https://github.com/Zezombye/overpy", + "version": "9.7.17" + }, + "schemaVersion": 1 +} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-invalid/source.opy b/crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-invalid/source.opy new file mode 100644 index 00000000..fa509fb1 --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-invalid/source.opy @@ -0,0 +1,4 @@ +macro p(): + 1 +macro p = eventPlayer +globalvar target = [p(), p] diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-safe/fixture.json b/crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-safe/fixture.json new file mode 100644 index 00000000..552373ff --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-safe/fixture.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 1, + "id": "synthetic/macro-first-constant-initializer-safe", + "category": "compilation", + "features": [ + "declaration initializers", + "event context", + "constant and callable macro namespaces" + ], + "source": "source.opy", + "tests": { + "source": { + "nativeStatus": "success", + "relationship": "match", + "ruleNames": true + }, + "compiler": { + "nativeStatus": "success", + "relationship": "match", + "comparison": "semantic-wir", + "semanticEquivalent": true + } + } +} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-safe/oracle.json b/crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-safe/oracle.json new file mode 100644 index 00000000..e7d86e9c --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-safe/oracle.json @@ -0,0 +1,34 @@ +{ + "compile": { + "diagnostics": [], + "exitCode": 0, + "status": "success", + "stdout": "", + "workshop": "variables {\n global:\n 0: target\n player:\n 0: playerTarget\n}\n\nrule (\"Initialize global variables\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(target, 1);\n }\n}\n\nrule (\"Initialize player variables\") {\n event {\n Ongoing - Each Player;\n All;\n All;\n }\n actions {\n Set Player Variable(Event Player, playerTarget, Event Player);\n }\n}\n", + "workshopExact": "variables {\n global:\n 0: target\n player:\n 0: playerTarget\n}\n\nrule (\"Initialize global variables\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(target, 1);\n }\n}\n\nrule (\"Initialize player variables\") {\n event {\n Ongoing - Each Player;\n All;\n All;\n }\n actions {\n Set Player Variable(Event Player, playerTarget, Event Player);\n }\n}\n\n", + "workshopSha256": "31f5cdf794b0aac9089c83027ff555e374d2c6e6d0fa85140b98f3f3c51d2b77" + }, + "fixture": "synthetic/macro-first-constant-initializer-safe", + "input": { + "files": [ + { + "path": "source.opy", + "sha256": "21bb932cfd45ddb07a95d293aad1cd24770398a2b29d11b16ec7f1f0e10ec7bf" + } + ], + "sha256": "b59c3b43497e32060594625bb2b062bf3dfccfb3d25202354be20003cb5f5a2d", + "source": "source.opy" + }, + "oracle": { + "contentCommit": "5a7d0e294b8cad73b9701987bb584d0551d7fa4d", + "gitHead": "ec763c1d245aa5485ef61af5b2bda4975bce4877", + "integrity": "sha512-C5F6btT5yn19BvHuE0bjMuSd/2esebBabmpL4SYLQFuJ4IdouVOKEoMLLuJFAyNc0Phwrolblj5GDkDGYR0ofg==", + "language": "en-US", + "license": "GPL-3.0-only", + "name": "overpy", + "registryTarball": "https://registry.npmjs.org/overpy/-/overpy-9.7.17.tgz", + "repository": "https://github.com/Zezombye/overpy", + "version": "9.7.17" + }, + "schemaVersion": 1 +} diff --git a/crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-safe/source.opy b/crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-safe/source.opy new file mode 100644 index 00000000..3075dfcd --- /dev/null +++ b/crates/opy-rs/tests/fixtures/corpus/synthetic/macro-first-constant-initializer-safe/source.opy @@ -0,0 +1,5 @@ +macro p(): + eventPlayer +macro p = 1 +globalvar target = p +playervar playerTarget = p()