From 43e7762e3689c5897b776293cc130d038574ddc0 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Sun, 27 Sep 2026 22:45:18 +0800 Subject: [PATCH 01/67] refactor(opy-rs): simplify repeated lowering paths --- crates/opy-cli/src/main.rs | 40 +- crates/opy-rs/src/compiler/backend.rs | 418 +++++++----------- crates/opy-rs/src/compiler/lowering.rs | 560 ++++++++----------------- crates/opy-rs/src/diag.rs | 11 + crates/opy-rs/src/lower/expressions.rs | 19 +- crates/opy-rs/src/manifest/mod.rs | 35 +- crates/opy-rs/src/parser.rs | 14 +- crates/opy-rs/src/parser/statements.rs | 49 +-- crates/opy-rs/src/preprocess.rs | 17 +- crates/opy-rs/src/preprocess/macros.rs | 17 +- crates/opy-rs/src/settings.rs | 21 +- 11 files changed, 412 insertions(+), 789 deletions(-) diff --git a/crates/opy-cli/src/main.rs b/crates/opy-cli/src/main.rs index 4fe51014..d2fa5fd1 100644 --- a/crates/opy-cli/src/main.rs +++ b/crates/opy-cli/src/main.rs @@ -14,7 +14,7 @@ use std::process::ExitCode; use clap::{CommandFactory, Parser, error::ErrorKind}; use clap_complete::{generate, shells}; -use opy_rs::tooling::{CheckOutcome, Diagnostic as OpyDiagnostic, check}; +use opy_rs::tooling::{CheckOutcome, Diagnostic as OpyDiagnostic, SourceLocation, check}; use opy_rs::{CompileDiagnostic, CompileStatus, Compiler}; use opy_rs::{FilesystemProject, LANGUAGE_NAME, LANGUAGE_VERSION}; use serde::Serialize; @@ -240,17 +240,7 @@ fn diagnostic_view(diagnostic: &OpyDiagnostic) -> DiagnosticView { }, code: diagnostic.code.clone(), message: diagnostic.message.clone(), - span: diagnostic.span.as_ref().map(|span| SpanView { - path: span.path.clone(), - start: PositionView { - line: span.start.line, - col: span.start.col, - }, - end: PositionView { - line: span.end.line, - col: span.end.col, - }, - }), + span: diagnostic.span.as_ref().map(diagnostic_span_view), } } @@ -262,17 +252,21 @@ fn compile_diagnostic_view(diagnostic: &CompileDiagnostic) -> DiagnosticView { }, code: diagnostic.code.clone(), message: diagnostic.message.clone(), - span: diagnostic.span.as_ref().map(|span| SpanView { - path: span.path.clone(), - start: PositionView { - line: span.start.line, - col: span.start.col, - }, - end: PositionView { - line: span.end.line, - col: span.end.col, - }, - }), + span: diagnostic.span.as_ref().map(diagnostic_span_view), + } +} + +fn diagnostic_span_view(span: &SourceLocation) -> SpanView { + SpanView { + path: span.path.clone(), + start: PositionView { + line: span.start.line, + col: span.start.col, + }, + end: PositionView { + line: span.end.line, + col: span.end.col, + }, } } diff --git a/crates/opy-rs/src/compiler/backend.rs b/crates/opy-rs/src/compiler/backend.rs index 35613ef8..b46d57b9 100644 --- a/crates/opy-rs/src/compiler/backend.rs +++ b/crates/opy-rs/src/compiler/backend.rs @@ -79,7 +79,7 @@ fn expand_macros_with( hir::Declaration::GlobalVariable { initializer, .. } | hir::Declaration::PlayerVariable { initializer, .. } => { if let Some(initializer) = initializer { - **initializer = expander.expand_expr(initializer, &bindings)?; + **initializer = expander.expand_expr(initializer, &bindings, None)?; } } _ => {} @@ -89,15 +89,15 @@ fn expand_macros_with( match entry { RuleEntry::Rule(rule) => { for argument in &mut rule.event.args { - *argument = expander.expand_expr(argument, &bindings)?; + *argument = expander.expand_expr(argument, &bindings, None)?; } for condition in &mut rule.conditions { - *condition = expander.expand_expr(condition, &bindings)?; + *condition = expander.expand_expr(condition, &bindings, None)?; } - rule.actions = expander.expand_stmts(&rule.actions, &bindings)?; + rule.actions = expander.expand_stmts(&rule.actions, &bindings, None)?; } RuleEntry::SubroutineDef { body, .. } => { - *body = expander.expand_stmts(body, &bindings)?; + *body = expander.expand_stmts(body, &bindings, None)?; } } } @@ -109,6 +109,7 @@ impl MacroExpander { &mut self, statements: &[Stmt], bindings: &MacroBindings, + site: Option, ) -> Result, IntegrationError> { let mut expanded = Vec::new(); for statement in statements { @@ -116,13 +117,13 @@ impl MacroExpander { if let Expr::MacroCall { name, args, span } = expr.as_ref() { let args = args .iter() - .map(|arg| self.expand_expr(arg, bindings)) + .map(|arg| self.expand_expr(arg, bindings, site)) .collect::, _>>()?; - expanded.extend(self.expand_macro_body(name, &args, *span)?); + expanded.extend(self.expand_macro_body(name, &args, *span, site)?); continue; } } - expanded.push(self.expand_stmt(statement, bindings)?); + expanded.push(self.expand_stmt(statement, bindings, site)?); } Ok(expanded) } @@ -131,10 +132,24 @@ impl MacroExpander { &mut self, statement: &Stmt, bindings: &MacroBindings, + site: Option, + ) -> Result { + let mut expanded = self.expand_stmt_inner(statement, bindings, site)?; + if let Some(site) = site { + set_stmt_span(&mut expanded, site); + } + Ok(expanded) + } + + fn expand_stmt_inner( + &mut self, + statement: &Stmt, + bindings: &MacroBindings, + site: Option, ) -> Result { Ok(match statement { Stmt::Expr { expr, span } => Stmt::Expr { - expr: Box::new(self.expand_expr(expr, bindings)?), + expr: Box::new(self.expand_expr(expr, bindings, site)?), span: *span, }, Stmt::Assign { @@ -142,8 +157,8 @@ impl MacroExpander { value, span, } => Stmt::Assign { - target: Box::new(self.expand_expr(target, bindings)?), - value: Box::new(self.expand_expr(value, bindings)?), + target: Box::new(self.expand_expr(target, bindings, site)?), + value: Box::new(self.expand_expr(value, bindings, site)?), span: *span, }, Stmt::If { @@ -155,14 +170,18 @@ impl MacroExpander { .iter() .map(|branch| { Ok(hir::types::IfBranch { - condition: Box::new(self.expand_expr(&branch.condition, bindings)?), - body: self.expand_stmts(&branch.body, bindings)?, + condition: Box::new(self.expand_expr( + &branch.condition, + bindings, + site, + )?), + body: self.expand_stmts(&branch.body, bindings, site)?, }) }) .collect::, IntegrationError>>()?, r#else: r#else .as_ref() - .map(|body| self.expand_stmts(body, bindings)) + .map(|body| self.expand_stmts(body, bindings, site)) .transpose()?, span: *span, }, @@ -172,9 +191,9 @@ impl MacroExpander { body, span, } => Stmt::For { - variable: Box::new(self.expand_expr(variable, bindings)?), - iterable: Box::new(self.expand_expr(iterable, bindings)?), - body: self.expand_stmts(body, bindings)?, + variable: Box::new(self.expand_expr(variable, bindings, site)?), + iterable: Box::new(self.expand_expr(iterable, bindings, site)?), + body: self.expand_stmts(body, bindings, site)?, span: *span, }, Stmt::While { @@ -182,8 +201,8 @@ impl MacroExpander { body, span, } => Stmt::While { - condition: Box::new(self.expand_expr(condition, bindings)?), - body: self.expand_stmts(body, bindings)?, + condition: Box::new(self.expand_expr(condition, bindings, site)?), + body: self.expand_stmts(body, bindings, site)?, span: *span, }, Stmt::DoWhile { @@ -191,30 +210,30 @@ impl MacroExpander { body, span, } => Stmt::DoWhile { - condition: Box::new(self.expand_expr(condition, bindings)?), - body: self.expand_stmts(body, bindings)?, + condition: Box::new(self.expand_expr(condition, bindings, site)?), + body: self.expand_stmts(body, bindings, site)?, span: *span, }, Stmt::Switch { value, arms, span } => Stmt::Switch { - value: Box::new(self.expand_expr(value, bindings)?), + value: Box::new(self.expand_expr(value, bindings, site)?), arms: arms .iter() .map(|arm| match arm { SwitchArm::Case { value, body, span } => Ok(SwitchArm::Case { - value: Box::new(self.expand_expr(value, bindings)?), - body: self.expand_stmts(body, bindings)?, - span: *span, + value: Box::new(self.expand_expr(value, bindings, site)?), + body: self.expand_stmts(body, bindings, site)?, + span: site.or(*span), }), SwitchArm::Default { body, span } => Ok(SwitchArm::Default { - body: self.expand_stmts(body, bindings)?, - span: *span, + body: self.expand_stmts(body, bindings, site)?, + span: site.or(*span), }), }) .collect::, IntegrationError>>()?, span: *span, }, Stmt::Delete { target, span } => Stmt::Delete { - target: Box::new(self.expand_expr(target, bindings)?), + target: Box::new(self.expand_expr(target, bindings, site)?), span: *span, }, Stmt::Goto { @@ -226,7 +245,7 @@ impl MacroExpander { label: label.clone(), offset: offset .as_ref() - .map(|offset| self.expand_expr(offset, bindings).map(Box::new)) + .map(|offset| self.expand_expr(offset, bindings, site).map(Box::new)) .transpose()?, rule_start: *rule_start, span: *span, @@ -243,21 +262,42 @@ impl MacroExpander { &mut self, expression: &Expr, bindings: &MacroBindings, + site: Option, + ) -> Result { + let mut expanded = self.expand_expr_inner(expression, bindings, site)?; + if let Some(site) = site { + set_expr_span(&mut expanded, site); + } + Ok(expanded) + } + + fn expand_expr_inner( + &mut self, + expression: &Expr, + bindings: &MacroBindings, + site: Option, ) -> Result { match expression { - Expr::MacroParam { name, span } => bindings.get(name).cloned().ok_or_else(|| { - IntegrationError::new( - "unsupported-integration-surface", - format!("macro parameter '{name}' has no expansion binding"), - *span, - ) - }), + Expr::MacroParam { name, span } => { + let value = bindings.get(name).ok_or_else(|| { + IntegrationError::new( + "unsupported-integration-surface", + format!("macro parameter '{name}' has no expansion binding"), + *span, + ) + })?; + if site.is_some() { + self.expand_expr(value, bindings, site) + } else { + Ok(value.clone()) + } + } Expr::MacroCall { name, args, span } => { let args = args .iter() - .map(|arg| self.expand_expr(arg, bindings)) + .map(|arg| self.expand_expr(arg, bindings, site)) .collect::, _>>()?; - let body = self.expand_macro_body(name, &args, *span)?; + let body = self.expand_macro_body(name, &args, *span, site)?; if body.len() != 1 { return Err(IntegrationError::new( "macro-invalid", @@ -277,7 +317,7 @@ impl MacroExpander { Expr::Array { elements, span } => Ok(Expr::Array { elements: elements .iter() - .map(|element| self.expand_expr(element, bindings)) + .map(|element| self.expand_expr(element, bindings, site)) .collect::, _>>()?, span: *span, }), @@ -286,9 +326,9 @@ impl MacroExpander { .iter() .map(|entry| { Ok(hir::DictEntry { - key: Box::new(self.expand_expr(&entry.key, bindings)?), - value: Box::new(self.expand_expr(&entry.value, bindings)?), - span: entry.span, + key: Box::new(self.expand_expr(&entry.key, bindings, site)?), + value: Box::new(self.expand_expr(&entry.value, bindings, site)?), + span: site.or(entry.span), }) }) .collect::, IntegrationError>>()?, @@ -304,15 +344,15 @@ impl MacroExpander { condition, span, } => Ok(Expr::Comprehension { - element: Box::new(self.expand_expr(element, bindings)?), + element: Box::new(self.expand_expr(element, bindings, site)?), variable: variable.clone(), - variable_span: *variable_span, + variable_span: site.or(*variable_span), index: index.clone(), - index_span: *index_span, - iterable: Box::new(self.expand_expr(iterable, bindings)?), + index_span: site.or(*index_span), + iterable: Box::new(self.expand_expr(iterable, bindings, site)?), condition: condition .as_ref() - .map(|condition| self.expand_expr(condition, bindings).map(Box::new)) + .map(|condition| self.expand_expr(condition, bindings, site).map(Box::new)) .transpose()?, span: *span, }), @@ -323,22 +363,22 @@ impl MacroExpander { span, } => Ok(Expr::Lambda { params: params.clone(), - param_spans: param_spans.clone(), - body: Box::new(self.expand_expr(body, bindings)?), + param_spans: param_spans.iter().map(|span| site.or(*span)).collect(), + body: Box::new(self.expand_expr(body, bindings, site)?), span: *span, }), Expr::Type { name, args, span } => Ok(Expr::Type { name: name.clone(), args: args .iter() - .map(|arg| self.expand_expr(arg, bindings)) + .map(|arg| self.expand_expr(arg, bindings, site)) .collect::, _>>()?, span: *span, }), Expr::Vector { x, y, z, span } => Ok(Expr::Vector { - x: Box::new(self.expand_expr(x, bindings)?), - y: Box::new(self.expand_expr(y, bindings)?), - z: Box::new(self.expand_expr(z, bindings)?), + x: Box::new(self.expand_expr(x, bindings, site)?), + y: Box::new(self.expand_expr(y, bindings, site)?), + z: Box::new(self.expand_expr(z, bindings, site)?), span: *span, }), Expr::PlayerVar { @@ -347,9 +387,9 @@ impl MacroExpander { member_span, span, } => Ok(Expr::PlayerVar { - player: Box::new(self.expand_expr(player, bindings)?), + player: Box::new(self.expand_expr(player, bindings, site)?), name: name.clone(), - member_span: *member_span, + member_span: site.or(*member_span), span: *span, }), Expr::Member { @@ -358,9 +398,9 @@ impl MacroExpander { member_span, span, } => Ok(Expr::Member { - receiver: Box::new(self.expand_expr(receiver, bindings)?), + receiver: Box::new(self.expand_expr(receiver, bindings, site)?), member: member.clone(), - member_span: *member_span, + member_span: site.or(*member_span), span: *span, }), Expr::Call { @@ -372,7 +412,7 @@ impl MacroExpander { name: name.clone(), args: args .iter() - .map(|arg| self.expand_expr(arg, bindings)) + .map(|arg| self.expand_expr(arg, bindings, site)) .collect::, _>>()?, debug_source: debug_source.clone(), span: *span, @@ -383,11 +423,11 @@ impl MacroExpander { args, span, } => Ok(Expr::ReceiverCall { - receiver: Box::new(self.expand_expr(receiver, bindings)?), + receiver: Box::new(self.expand_expr(receiver, bindings, site)?), name: name.clone(), args: args .iter() - .map(|arg| self.expand_expr(arg, bindings)) + .map(|arg| self.expand_expr(arg, bindings, site)) .collect::, _>>()?, span: *span, }), @@ -398,8 +438,8 @@ impl MacroExpander { span, } => Ok(Expr::Binary { op: op.clone(), - left: Box::new(self.expand_expr(left, bindings)?), - right: Box::new(self.expand_expr(right, bindings)?), + left: Box::new(self.expand_expr(left, bindings, site)?), + right: Box::new(self.expand_expr(right, bindings, site)?), span: *span, }), Expr::Conditional { @@ -408,26 +448,26 @@ impl MacroExpander { else_value, span, } => Ok(Expr::Conditional { - then_value: Box::new(self.expand_expr(then_value, bindings)?), - condition: Box::new(self.expand_expr(condition, bindings)?), - else_value: Box::new(self.expand_expr(else_value, bindings)?), + then_value: Box::new(self.expand_expr(then_value, bindings, site)?), + condition: Box::new(self.expand_expr(condition, bindings, site)?), + else_value: Box::new(self.expand_expr(else_value, bindings, site)?), span: *span, }), Expr::Unary { op, operand, span } => Ok(Expr::Unary { op: op.clone(), - operand: Box::new(self.expand_expr(operand, bindings)?), + operand: Box::new(self.expand_expr(operand, bindings, site)?), span: *span, }), Expr::Index { array, index, span } => Ok(Expr::Index { - array: Box::new(self.expand_expr(array, bindings)?), - index: Box::new(self.expand_expr(index, bindings)?), + array: Box::new(self.expand_expr(array, bindings, site)?), + index: Box::new(self.expand_expr(index, bindings, site)?), span: *span, }), Expr::Format { text, args, span } => Ok(Expr::Format { text: text.clone(), args: args .iter() - .map(|arg| self.expand_expr(arg, bindings)) + .map(|arg| self.expand_expr(arg, bindings, site)) .collect::, _>>()?, span: *span, }), @@ -440,6 +480,7 @@ impl MacroExpander { name: &str, args: &[Expr], span: Option, + parent_site: Option, ) -> Result, IntegrationError> { let Some((params, body)) = self.macros.get(name).cloned() else { return Err(IntegrationError::new( @@ -471,113 +512,34 @@ impl MacroExpander { bindings.insert(param, arg.clone()); } self.stack.push(name.to_string()); - let result = self.expand_stmts(&body, &bindings); + let site = parent_site.or_else(|| self.attribute_to_site.then_some(span).flatten()); + let result = self.expand_stmts(&body, &bindings, site); self.stack.pop(); - let mut result = result?; - if let Some(site) = span.filter(|_| self.attribute_to_site) { - relocate_stmts(&mut result, site); - } - Ok(result) + result } } -/// Attribute every span of macro-expanded code to the invocation `site`, so -/// source attribution never points into the macro definition. -fn relocate_stmts(statements: &mut [Stmt], site: HirSpan) { - for statement in statements { - relocate_stmt(statement, site); - } -} - -fn relocate_stmt(statement: &mut Stmt, site: HirSpan) { - let at = Some(site); +fn set_stmt_span(statement: &mut Stmt, site: HirSpan) { match statement { - Stmt::Expr { expr, span } | Stmt::Delete { target: expr, span } => { - relocate_expr(expr, site); - *span = at; - } - Stmt::Assign { - target, - value, - span, - } => { - relocate_expr(target, site); - relocate_expr(value, site); - *span = at; - } - Stmt::If { - branches, - r#else, - span, - } => { - for branch in branches { - relocate_expr(&mut branch.condition, site); - relocate_stmts(&mut branch.body, site); - } - if let Some(body) = r#else { - relocate_stmts(body, site); - } - *span = at; - } - Stmt::For { - variable, - iterable, - body, - span, - } => { - relocate_expr(variable, site); - relocate_expr(iterable, site); - relocate_stmts(body, site); - *span = at; - } - Stmt::While { - condition, - body, - span, - } - | Stmt::DoWhile { - condition, - body, - span, - } => { - relocate_expr(condition, site); - relocate_stmts(body, site); - *span = at; - } - Stmt::Switch { value, arms, span } => { - relocate_expr(value, site); - for arm in arms { - match arm { - SwitchArm::Case { value, body, span } => { - relocate_expr(value, site); - relocate_stmts(body, site); - *span = at; - } - SwitchArm::Default { body, span } => { - relocate_stmts(body, site); - *span = at; - } - } - } - *span = at; - } - Stmt::Goto { offset, span, .. } => { - if let Some(offset) = offset { - relocate_expr(offset, site); - } - *span = at; - } - Stmt::Break { span } + Stmt::Expr { span, .. } + | Stmt::Assign { span, .. } + | Stmt::If { span, .. } + | Stmt::For { span, .. } + | Stmt::While { span, .. } + | Stmt::DoWhile { span, .. } + | Stmt::Switch { span, .. } + | Stmt::Delete { span, .. } + | Stmt::Goto { span, .. } + | Stmt::Break { span } | Stmt::Return { span } | Stmt::Continue { span } | Stmt::Label { span, .. } | Stmt::CallSubroutine { span, .. } - | Stmt::Pass { span } => *span = at, + | Stmt::Pass { span } => *span = Some(site), } } -fn relocate_expr(expression: &mut Expr, site: HirSpan) { - let at = Some(site); +fn set_expr_span(expression: &mut Expr, site: HirSpan) { match expression { Expr::Number { span, .. } | Expr::String { span, .. } @@ -590,116 +552,22 @@ fn relocate_expr(expression: &mut Expr, site: HirSpan) { | Expr::HostPlayer { span } | Expr::EventPlayer { span } | Expr::Constant { span, .. } - | Expr::MacroParam { span, .. } => *span = at, - Expr::Array { - elements: args, - span, - } - | Expr::Call { args, span, .. } - | Expr::MacroCall { args, span, .. } - | Expr::Type { args, span, .. } - | Expr::Format { args, span, .. } => { - for arg in args { - relocate_expr(arg, site); - } - *span = at; - } - Expr::Dict { entries, span } => { - for entry in entries { - relocate_expr(&mut entry.key, site); - relocate_expr(&mut entry.value, site); - entry.span = at; - } - *span = at; - } - Expr::Comprehension { - element, - variable_span, - index_span, - iterable, - condition, - span, - .. - } => { - relocate_expr(element, site); - relocate_expr(iterable, site); - if let Some(condition) = condition { - relocate_expr(condition, site); - } - *variable_span = at; - *index_span = at; - *span = at; - } - Expr::Lambda { - param_spans, - body, - span, - .. - } => { - relocate_expr(body, site); - param_spans.iter_mut().for_each(|param| *param = at); - *span = at; - } - Expr::Vector { x, y, z, span } => { - for component in [x, y, z] { - relocate_expr(component, site); - } - *span = at; - } - Expr::PlayerVar { - player: receiver, - member_span, - span, - .. - } - | Expr::Member { - receiver, - member_span, - span, - .. - } => { - relocate_expr(receiver, site); - *member_span = at; - *span = at; - } - Expr::ReceiverCall { - receiver, - args, - span, - .. - } => { - relocate_expr(receiver, site); - for arg in args { - relocate_expr(arg, site); - } - *span = at; - } - Expr::Binary { - left, right, span, .. - } => { - relocate_expr(left, site); - relocate_expr(right, site); - *span = at; - } - Expr::Conditional { - then_value, - condition, - else_value, - span, - } => { - for part in [then_value, condition, else_value] { - relocate_expr(part, site); - } - *span = at; - } - Expr::Unary { operand, span, .. } => { - relocate_expr(operand, site); - *span = at; - } - Expr::Index { array, index, span } => { - relocate_expr(array, site); - relocate_expr(index, site); - *span = at; - } + | Expr::MacroParam { span, .. } + | Expr::Array { span, .. } + | Expr::Dict { span, .. } + | Expr::Comprehension { span, .. } + | Expr::Lambda { span, .. } + | Expr::Type { span, .. } + | Expr::Vector { span, .. } + | Expr::PlayerVar { span, .. } + | Expr::Member { span, .. } + | Expr::Call { span, .. } + | Expr::ReceiverCall { span, .. } + | Expr::Binary { span, .. } + | Expr::Conditional { span, .. } + | Expr::Unary { span, .. } + | Expr::Index { span, .. } + | Expr::Format { span, .. } + | Expr::MacroCall { span, .. } => *span = Some(site), } } diff --git a/crates/opy-rs/src/compiler/lowering.rs b/crates/opy-rs/src/compiler/lowering.rs index 21a6dfb8..2f9e6c6e 100644 --- a/crates/opy-rs/src/compiler/lowering.rs +++ b/crates/opy-rs/src/compiler/lowering.rs @@ -214,8 +214,6 @@ pub(crate) struct Lowering<'a> { deferred_gotos: Vec<(ActionId, String, Option, usize)>, translation_uses: Vec<(String, Option)>, optimized_nodes: HashMap, - /// Coerced argument values, mapped to the literal the author wrote. - authored_values: HashMap, used_maps: Vec<&'static str>, } @@ -392,7 +390,6 @@ impl<'a> Lowering<'a> { array_bindings: Vec::new(), current_rule_conditions: None, optimized_nodes: HashMap::new(), - authored_values: HashMap::new(), used_maps: used_bugged_maps(hir), visible_labels: Vec::new(), deferred_gotos: Vec::new(), @@ -429,76 +426,45 @@ impl<'a> Lowering<'a> { &self, reserved: &HashSet, ) -> Result, IntegrationError> { - if self.hir.preprocessing.translations.is_none() { + let Some(translations) = self.hir.preprocessing.translations.as_ref() else { return Ok(None); - } - (0..=127) - .rev() - .find(|index| !reserved.contains(index)) - .map(Some) - .ok_or_else(|| { - IntegrationError::new( - "index-exhausted", - "no available global variable index remains for translations", - self.hir - .preprocessing - .translations - .as_ref() - .and_then(|value| value.span), - ) - }) + }; + self.free_global_index( + reserved, + translations.span, + "no available global variable index remains for translations", + ) } - fn compression_alphabet_index( + fn helper_global_index( &self, reserved: &HashSet, + directive: &str, + message: &str, ) -> Result, IntegrationError> { - if !has_directive(self.hir, "useVariableForCompressionAlphabet") { + let Some(source) = self + .hir + .preprocessing + .directives + .iter() + .find(|item| item.name == directive) + else { return Ok(None); - } - (0..=127) - .rev() - .find(|index| !reserved.contains(index)) - .map(Some) - .ok_or_else(|| { - IntegrationError::new( - "index-exhausted", - "no available global variable index remains for the compression alphabet", - self.hir - .preprocessing - .directives - .iter() - .find(|directive| directive.name == "useVariableForCompressionAlphabet") - .and_then(|directive| directive.span), - ) - }) + }; + self.free_global_index(reserved, source.span, message) } - fn helper_global_index( + fn free_global_index( &self, reserved: &HashSet, - directive: &str, + span: Option, message: &str, ) -> Result, IntegrationError> { - if !has_directive(self.hir, directive) { - return Ok(None); - } (0..=127) .rev() .find(|index| !reserved.contains(index)) .map(Some) - .ok_or_else(|| { - IntegrationError::new( - "index-exhausted", - message, - self.hir - .preprocessing - .directives - .iter() - .find(|item| item.name == directive) - .and_then(|item| item.span), - ) - }) + .ok_or_else(|| IntegrationError::new("index-exhausted", message, span)) } pub(super) fn lower_declarations(&mut self) -> Result<(), IntegrationError> { @@ -594,7 +560,11 @@ impl<'a> Lowering<'a> { helper_reserved.insert(index); global_reserved.insert(index); } - let compression_alphabet_index = self.compression_alphabet_index(&helper_reserved)?; + let compression_alphabet_index = self.helper_global_index( + &helper_reserved, + "useVariableForCompressionAlphabet", + "no available global variable index remains for the compression alphabet", + )?; if let Some(index) = compression_alphabet_index { helper_reserved.insert(index); global_reserved.insert(index); @@ -1205,10 +1175,8 @@ impl<'a> Lowering<'a> { for expr in &rule.conditions { Self::split_rule_condition(expr, &mut condition_exprs); } - let conditions = condition_exprs - .iter() - .map(|expr| self.lower_condition(expr)) - .collect::, _>>()?; + let conditions = condition_exprs.iter().copied(); + let conditions = self.lower_values(conditions)?; let previous_conditions = self.current_rule_conditions.replace(conditions.clone()); let lowered_actions = self.lower_actions(&rule.actions, None); self.current_rule_conditions = previous_conditions; @@ -3221,12 +3189,7 @@ impl<'a> Lowering<'a> { value } else { let mut custom_args = vec![text]; - custom_args.extend( - format_args - .iter() - .map(|arg| self.lower_value(arg)) - .collect::, _>>()?, - ); + custom_args.extend(self.lower_values(format_args.iter().copied())?); self.push_call("customString", custom_args) }; let helper_id = *self.globals.get(TRANSLATION_HELPER_NAME).ok_or_else(|| { @@ -3495,7 +3458,7 @@ impl<'a> Lowering<'a> { visibility, ], ); - self.apply_replacements("createHudText", &mut args, span); + self.apply_replacements_to_values("createHudText", &mut args, span); Ok(self.push_call_action_with_spans( "createHudText", &args, @@ -3779,10 +3742,6 @@ impl<'a> Lowering<'a> { }) } - fn normalize_contextual_values(&mut self, call_id: &str, values: Vec) -> Vec { - self.normalize_contextual_arguments(call_id, values) - } - fn normalize_contextual_argument( &mut self, call_id: &str, @@ -3945,10 +3904,6 @@ impl<'a> Lowering<'a> { matches!(self.values.get(id), Some(Value::String(value)) if value.is_empty()) } - fn lower_condition(&mut self, expr: &Expr) -> Result { - self.lower_value(expr) - } - fn lower_delete( &mut self, target: &Expr, @@ -4119,23 +4074,15 @@ impl<'a> Lowering<'a> { if let Expr::Binary { op, left, right, .. } = value + && matches!(left.as_ref(), Expr::GlobalVar { name: left_name, .. } if left_name == name) + && let Some((modify_op, _)) = modify_operator(op) { - if let Expr::GlobalVar { - name: left_name, .. - } = left.as_ref() - { - if left_name == name { - if let Some(modify_op) = modify_op_from_str(op) { - let right = self.lower_value(right)?; - let val = right; - return Ok(self.push_action(Action::ModifyGlobalVariable { - variable: self.global_names[variable].clone(), - op: modify_op, - value: val, - })); - } - } - } + let value = self.lower_value(right)?; + return Ok(self.push_action(Action::ModifyGlobalVariable { + variable: self.global_names[variable].clone(), + op: modify_op, + value, + })); } let val = self.lower_value(value)?; Ok(self.push_action(Action::SetGlobalVariable { @@ -4156,26 +4103,16 @@ impl<'a> Lowering<'a> { if let Expr::Binary { op, left, right, .. } = value + && matches!(left.as_ref(), Expr::PlayerVar { player: left_player, name: left_name, .. } if left_name == name && left_player.as_ref() == player.as_ref()) + && let Some((modify_op, _)) = modify_operator(op) { - if let Expr::PlayerVar { - player: left_player, - name: left_name, - .. - } = left.as_ref() - { - if left_name == name && left_player.as_ref() == player.as_ref() { - if let Some(modify_op) = modify_op_from_str(op) { - let right = self.lower_value(right)?; - let val = right; - return Ok(self.push_action(Action::ModifyPlayerVariable { - player: player_val, - variable: self.player_names[variable].clone(), - op: modify_op, - value: val, - })); - } - } - } + let value = self.lower_value(right)?; + return Ok(self.push_action(Action::ModifyPlayerVariable { + player: player_val, + variable: self.player_names[variable].clone(), + op: modify_op, + value, + })); } let val = self.lower_value(value)?; Ok(self.push_action(Action::SetPlayerVariable { @@ -4200,42 +4137,13 @@ impl<'a> Lowering<'a> { self.global_names[variable].clone(), )); let index_val = self.lower_value(index)?; - if let Expr::Binary { - op, left, right, .. - } = value - { - if let Expr::Index { - array: left_arr, - index: left_idx, - .. - } = left.as_ref() - { - if left_arr.as_ref() == array.as_ref() - && left_idx.as_ref() == index.as_ref() - && modify_op_from_str(op).is_some() - { - let op_id = modify_catalog_name_from_str(op) - .expect("known modify operator has a catalog name"); - let op_node = self.push_call(op_id, Vec::new()); - let right = self.lower_value(right)?; - let right_val = right; - let args = self.normalize_contextual_arguments( - "modifyGlobalVariableAtIndex", - vec![var_node, index_val, op_node, right_val], - ); - return Ok(self.push_call_action( - "modifyGlobalVariableAtIndex", - &args, - )); - } - } - } - let val = self.lower_value(value)?; - let args = self.normalize_contextual_arguments( - "setGlobalVariableAtIndex", - vec![var_node, index_val, val], - ); - Ok(self.push_call_action("setGlobalVariableAtIndex", &args)) + self.lower_indexed_assignment( + target, + var_node, + index_val, + value, + ("setGlobalVariableAtIndex", "modifyGlobalVariableAtIndex"), + ) } Expr::PlayerVar { player, @@ -4252,49 +4160,13 @@ impl<'a> Lowering<'a> { variable: self.player_names[variable].clone(), }); let index_val = self.lower_value(index)?; - if let Expr::Binary { - op, left, right, .. - } = value - { - if let Expr::Index { - array: left_arr, - index: left_idx, - .. - } = left.as_ref() - { - if left_arr.as_ref() == array.as_ref() - && left_idx.as_ref() == index.as_ref() - && modify_op_from_str(op).is_some() - { - let op_id = modify_catalog_name_from_str(op) - .expect("known modify operator has a catalog name"); - let op_node = self.push_call(op_id, Vec::new()); - let right = self.lower_value(right)?; - let right_val = right; - let args = self.normalize_contextual_arguments( - "modifyPlayerVariableAtIndex", - vec![var_node, index_val, op_node, right_val], - ); - // The canonical signature takes the - // player-variable value node (which - // carries the player) as its first - // argument. - return Ok(self.push_call_action( - "modifyPlayerVariableAtIndex", - &args, - )); - } - } - } - let val = self.lower_value(value)?; - let args = self.normalize_contextual_arguments( - "setPlayerVariableAtIndex", - vec![var_node, index_val, val], - ); - // The canonical signature takes the player-variable - // value node (which carries the player) as its first - // argument. - Ok(self.push_call_action("setPlayerVariableAtIndex", &args)) + self.lower_indexed_assignment( + target, + var_node, + index_val, + value, + ("setPlayerVariableAtIndex", "modifyPlayerVariableAtIndex"), + ) } _ => Err(self.unsupported( "indexing assignment is only representable for global or player variables", @@ -4308,6 +4180,45 @@ impl<'a> Lowering<'a> { } } + fn lower_indexed_assignment( + &mut self, + target: &Expr, + variable: ValueId, + index_value: ValueId, + value: &Expr, + actions: (&str, &str), + ) -> Result { + let Expr::Index { array, index, .. } = target else { + unreachable!("indexed assignment target was matched before lowering") + }; + let (set_action, modify_action) = actions; + if let Expr::Binary { + op, left, right, .. + } = value + && let Expr::Index { + array: left_array, + index: left_index, + .. + } = left.as_ref() + && left_array.as_ref() == array.as_ref() + && left_index.as_ref() == index.as_ref() + && let Some((_, call_name)) = modify_operator(op) + { + let operator = self.push_call(call_name, Vec::new()); + let value = self.lower_value(right)?; + let args = self.normalize_contextual_arguments( + modify_action, + vec![variable, index_value, operator, value], + ); + return Ok(self.push_call_action(modify_action, &args)); + } + + let value = self.lower_value(value)?; + let args = + self.normalize_contextual_arguments(set_action, vec![variable, index_value, value]); + Ok(self.push_call_action(set_action, &args)) + } + fn lower_nested_indexed_assign( &mut self, root: &Expr, @@ -4381,7 +4292,7 @@ impl<'a> Lowering<'a> { op, left, right, .. } = value && left.as_ref() == target - && let Some(call_name) = modify_catalog_name_from_str(op) + && let Some((_, call_name)) = modify_operator(op) { let current = self.lower_indexed_read(array, index, index_value)?; let right = self.lower_value(right)?; @@ -4585,10 +4496,7 @@ impl<'a> Lowering<'a> { .iter() .map(|expr| expr.span().copied()) .collect::>(); - let args = args - .iter() - .map(|expr| self.lower_value(expr)) - .collect::, _>>()?; + let args = self.lower_values(args)?; return Ok(self.push_call_action_with_spans(name, &args, spans)); } let function = self @@ -4653,10 +4561,7 @@ impl<'a> Lowering<'a> { .iter() .map(|expr| expr.span().copied()) .chain(std::iter::once(None)); - let mut lowered = args - .iter() - .map(|expr| self.lower_value(expr)) - .collect::, _>>()?; + let mut lowered = self.lower_values(args)?; let mut zero_vector = Vec::with_capacity(3); for value in [0.0, 0.0, 0.0] { zero_vector.push(self.push_number(value, "0")); @@ -4669,10 +4574,7 @@ impl<'a> Lowering<'a> { .iter() .map(|expr| expr.span().copied()) .collect::>(); - let args = args - .iter() - .map(|expr| self.lower_value(expr)) - .collect::, _>>()?; + let args = self.lower_values(args)?; let catalog_id = if matches!(function.id.as_str(), "stopChasingVariable" | "stopChasing") { match args.first().map(|value| self.value(*value)) { Some(Value::GlobalVariable(_)) => "stopChasingGlobalVariable", @@ -4696,7 +4598,7 @@ impl<'a> Lowering<'a> { })? }; let mut args = self.normalize_contextual_arguments(catalog_id, args); - self.apply_replacements(catalog_id, &mut args, span); + self.apply_replacements_to_values(catalog_id, &mut args, span); self.optimize_wait_duration(catalog_id, &mut args, span); Ok(self.push_call_action_with_spans(catalog_id, &args, spans)) } @@ -4990,16 +4892,22 @@ impl<'a> Lowering<'a> { .collect::>(); let mut lowered = Vec::with_capacity(args.len() + 1); lowered.push(self.lower_value(receiver)?); - lowered.extend( - args.iter() - .map(|arg| self.lower_value(arg)) - .collect::, _>>()?, - ); + lowered.extend(self.lower_values(args.iter())?); let mut args = self.normalize_contextual_arguments(catalog_id, lowered); - self.apply_replacements(catalog_id, &mut args, span); + self.apply_replacements_to_values(catalog_id, &mut args, span); Ok(self.push_call_action_with_spans(catalog_id.clone(), &args, argument_spans)) } + fn lower_values<'expr>( + &mut self, + expressions: impl IntoIterator, + ) -> Result, IntegrationError> { + expressions + .into_iter() + .map(|expr| self.lower_value(expr)) + .collect() + } + fn lower_value(&mut self, expr: &Expr) -> Result { let value_id = self.lower_value_unoptimized(expr)?; let optimization = self.optimization_state_at(expr.span()); @@ -5125,10 +5033,7 @@ impl<'a> Lowering<'a> { } } Expr::Array { elements, .. } => { - let elements = elements - .iter() - .map(|element| self.lower_value(element)) - .collect::, _>>()?; + let elements = self.lower_values(elements)?; return self.lower_array(elements, span); } Expr::Vector { x, y, z, .. } => { @@ -5210,16 +5115,15 @@ impl<'a> Lowering<'a> { span, ) })?; - let lowered_args = dynamic_args - .iter() - .map(|arg| self.lower_value(arg)) - .collect::, _>>()?; + let lowered_args = dynamic_args.iter().copied(); + let lowered_args = self.lower_values(lowered_args)?; let mut parts = Vec::with_capacity(chunks.len()); for (chunk, indices) in chunks { let text = self.push_value(Value::String(chunk)); let mut call_args = vec![text]; call_args.extend(indices.into_iter().map(|index| lowered_args[index])); - let call_args = self.normalize_contextual_values("customString", call_args); + let call_args = + self.normalize_contextual_arguments("customString", call_args); parts.push(self.push_value(Value::Call { name: "customString".to_string(), args: call_args, @@ -5404,9 +5308,6 @@ impl<'a> Lowering<'a> { if name == "createWorkshopSetting" { return self.lower_workshop_setting(args, span); } - if matches!(name.as_str(), "_" | "__" | "___") { - return self.lower_translation(name, args, span); - } if name == "buttonToString" { let [button] = args.as_slice() else { return Err(self.unsupported("buttonToString requires one button", span)); @@ -5453,10 +5354,7 @@ impl<'a> Lowering<'a> { }; } if name == "getRealPlayersInRadius" { - let lowered = args - .iter() - .map(|arg| self.lower_value(arg)) - .collect::, _>>()?; + let lowered = self.lower_values(args)?; let players = self.push_call("getPlayersInRadius", lowered); let current = self.push_call("currentArrayElement", Vec::new()); let alive = self.push_call("isAlive", vec![current]); @@ -5771,10 +5669,7 @@ impl<'a> Lowering<'a> { | "createWorkshopSettingFloat" | "createWorkshopSettingHero" ) { - let mut lowered = args - .iter() - .map(|arg| self.lower_value(arg)) - .collect::, _>>()?; + let mut lowered = self.lower_values(args)?; // The sort order is the last parameter; OverPy writes 0 when omitted. let (canonical, arity_without_sort_order) = workshop_setting_call(name); if lowered.len() == arity_without_sort_order { @@ -5900,10 +5795,7 @@ impl<'a> Lowering<'a> { if function.id == "getAllPlayers" { return Ok(self.lower_all_players()); } - let lowered_args = args - .iter() - .map(|arg| self.lower_value(arg)) - .collect::, _>>()?; + let lowered_args = self.lower_values(args)?; Value::Call { name: catalog_id.clone(), args: self.value_args(&lowered_args), @@ -6110,10 +6002,7 @@ impl<'a> Lowering<'a> { span, ) })?; - let lowered_args = receiver_args - .iter() - .map(|arg| self.lower_value(arg)) - .collect::, _>>()?; + let lowered_args = self.lower_values(receiver_args)?; return Ok(self.push_value(Value::Call { name: catalog_id, args: self.value_args(&lowered_args), @@ -6164,11 +6053,7 @@ impl<'a> Lowering<'a> { })?; let mut lowered = Vec::with_capacity(args.len() + 1); lowered.push(self.lower_value(receiver)?); - lowered.extend( - args.iter() - .map(|arg| self.lower_value(arg)) - .collect::, _>>()?, - ); + lowered.extend(self.lower_values(args)?); Value::Call { name: catalog_id.clone(), args: self.value_args(&lowered), @@ -6287,7 +6172,7 @@ impl<'a> Lowering<'a> { }; let name = name.clone(); let args = args.clone(); - let mut args = self.normalize_contextual_values(&name, args); + let mut args = self.normalize_contextual_arguments(&name, args); self.apply_replacements_to_values(&name, &mut args, span); if let Some(Value::Call { args: target_args, .. @@ -6370,10 +6255,6 @@ impl<'a> Lowering<'a> { } } - fn apply_replacements(&mut self, call_id: &str, args: &mut [ValueId], span: Option) { - self.apply_replacements_to_values(call_id, args, span); - } - fn apply_replacement( &mut self, value_id: ValueId, @@ -7028,43 +6909,10 @@ impl<'a> Lowering<'a> { } fn materialize_value_inner(&self, id: ValueId) -> workshop_rs::Value { - let Some(strict) = self.optimized_nodes.get(&id) else { - return self.materialize_node(id); - }; - // Folds see the literal the author wrote; the parameter's canonical - // form is applied to whatever call remains. - let Value::Call { name, args } = self.value(id) else { - return OperatorOptimizer::new(self.compiler, *strict).node(self.materialize_node(id)); - }; - let authored: Vec = args - .iter() - .map(|arg| { - self.materialize_value_inner(self.authored_values.get(arg).copied().unwrap_or(*arg)) - }) - .collect(); - let folded = - OperatorOptimizer::new(self.compiler, *strict).node(workshop_rs::Value::Call { - name: name.clone(), - args: authored.clone(), - }); - match folded { - workshop_rs::Value::Call { - name: folded_name, - args: mut folded_args, - } if folded_name == *name && folded_args.len() == args.len() => { - for (index, arg) in args.iter().enumerate() { - if self.authored_values.contains_key(arg) - && same(&folded_args[index], &authored[index]) - { - folded_args[index] = self.materialize_value_inner(*arg); - } - } - workshop_rs::Value::Call { - name: folded_name, - args: folded_args, - } - } - folded => folded, + let value = self.materialize_node(id); + match self.optimized_nodes.get(&id) { + Some(strict) => OperatorOptimizer::new(self.compiler, *strict).node(value), + None => value, } } @@ -7312,23 +7160,12 @@ impl<'a> Lowering<'a> { name: name.clone(), args: args .iter() - .map(|arg| self.materialize_value(self.written_empty_array(*arg))) + .map(|arg| self.materialize_value(*arg)) .collect(), }, } } - /// An empty array the author wrote keeps its spelling in an action - /// argument, where the reference does not read it as an empty string. - fn written_empty_array(&self, id: ValueId) -> ValueId { - match self.authored_values.get(&id) { - Some(&authored) if matches!(self.value(authored), Value::Call { name, args } if name == "emptyArray" && args.is_empty()) => { - authored - } - _ => id, - } - } - fn set_rule_provenance( &mut self, rule: usize, @@ -8119,38 +7956,39 @@ fn fullwidth(value: &str) -> String { fn case_sensitive(value: &str) -> String { let mut output = value.replace('æ', "\u{04d5}").replace("nj", "\u{01cc}"); output = output.replace(" a ", " a "); - output - .chars() - .map(|character| match character { - 'a' => 'ạ', - 'b' => 'ḅ', - 'c' => 'ƈ', - 'd' => 'ḍ', - 'e' => 'ẹ', - 'f' => 'ƒ', - 'g' => 'ǥ', - 'h' => '\u{04bb}', - 'i' => 'і', - 'j' => 'ј', - 'k' => 'ḳ', - 'l' => 'I', - 'm' => 'ṃ', - 'n' => 'ṇ', - 'o' => 'ο', - 'p' => 'ṗ', - 'q' => 'ǫ', - 'r' => 'ṛ', - 's' => 'ѕ', - 't' => 'ṭ', - 'u' => 'υ', - 'v' => 'ν', - 'w' => 'ẉ', - 'x' => '\u{04b3}', - 'y' => 'ỵ', - 'z' => 'ẓ', - _ => character, - }) - .collect() + output.chars().map(case_sensitive_character).collect() +} + +fn case_sensitive_character(character: char) -> char { + match character { + 'a' => 'ạ', + 'b' => 'ḅ', + 'c' => 'ƈ', + 'd' => 'ḍ', + 'e' => 'ẹ', + 'f' => 'ƒ', + 'g' => 'ǥ', + 'h' => 'һ', + 'i' => 'і', + 'j' => 'ј', + 'k' => 'ḳ', + 'l' => 'I', + 'm' => 'ṃ', + 'n' => 'ṇ', + 'o' => 'ο', + 'p' => 'ṗ', + 'q' => 'ǫ', + 'r' => 'ṛ', + 's' => 'ѕ', + 't' => 'ṭ', + 'u' => 'υ', + 'v' => 'ν', + 'w' => 'ẉ', + 'x' => 'ҳ', + 'y' => 'ỵ', + 'z' => 'ẓ', + _ => character, + } } fn computed_number_text(value: f64) -> String { @@ -8392,37 +8230,7 @@ fn debug_expr_text(expr: &Expr) -> String { } fn canonical_debug_text(text: &str) -> String { - text.chars() - .map(|character| match character { - 'a' => 'ạ', - 'b' => 'ḅ', - 'c' => 'ƈ', - 'd' => 'ḍ', - 'e' => 'ẹ', - 'f' => 'ƒ', - 'g' => 'ǥ', - 'h' => 'һ', - 'i' => 'і', - 'j' => 'ј', - 'k' => 'ḳ', - 'l' => 'I', - 'm' => 'ṃ', - 'n' => 'ṇ', - 'o' => 'ο', - 'p' => 'ṗ', - 'q' => 'ǫ', - 'r' => 'ṛ', - 's' => 'ѕ', - 't' => 'ṭ', - 'u' => 'υ', - 'v' => 'ν', - 'w' => 'ẉ', - 'x' => 'ҳ', - 'y' => 'ỵ', - 'z' => 'ẓ', - _ => character, - }) - .collect() + text.chars().map(case_sensitive_character).collect() } fn negated_comparison(op: &str) -> Option<&'static str> { @@ -8437,28 +8245,16 @@ fn negated_comparison(op: &str) -> Option<&'static str> { }) } -fn modify_op_from_str(op: &str) -> Option { - match op { - "+" => Some(ModifyOp::Add), - "-" => Some(ModifyOp::Subtract), - "*" => Some(ModifyOp::Multiply), - "/" => Some(ModifyOp::Divide), - "%" => Some(ModifyOp::Modulo), - "**" => Some(ModifyOp::RaiseToPower), - _ => None, - } -} - -fn modify_catalog_name_from_str(op: &str) -> Option<&'static str> { - match op { - "+" => Some("add"), - "-" => Some("subtract"), - "*" => Some("multiply"), - "/" => Some("divide"), - "%" => Some("modulo"), - "**" => Some("raiseToPower"), - _ => None, - } +fn modify_operator(op: &str) -> Option<(ModifyOp, &'static str)> { + Some(match op { + "+" => (ModifyOp::Add, "add"), + "-" => (ModifyOp::Subtract, "subtract"), + "*" => (ModifyOp::Multiply, "multiply"), + "/" => (ModifyOp::Divide, "divide"), + "%" => (ModifyOp::Modulo, "modulo"), + "**" => (ModifyOp::RaiseToPower, "raiseToPower"), + _ => return None, + }) } /// Words the Workshop refuses in a rule name. Each entry is the text before the diff --git a/crates/opy-rs/src/diag.rs b/crates/opy-rs/src/diag.rs index f3d6103e..57ee971d 100644 --- a/crates/opy-rs/src/diag.rs +++ b/crates/opy-rs/src/diag.rs @@ -40,6 +40,17 @@ impl Position { } } +pub(crate) fn shift_position(position: Position, origin: Position) -> Position { + Position::new( + origin.line + position.line.saturating_sub(1), + if position.line == 1 { + origin.col + position.col.saturating_sub(1) + } else { + position.col + }, + ) +} + impl Span { pub fn new(file: u32, start: Position, end: Position) -> Span { Span { file, start, end } diff --git a/crates/opy-rs/src/lower/expressions.rs b/crates/opy-rs/src/lower/expressions.rs index 05734c00..8e775d90 100644 --- a/crates/opy-rs/src/lower/expressions.rs +++ b/crates/opy-rs/src/lower/expressions.rs @@ -1347,7 +1347,7 @@ impl Lowerer { ); } } - CallPosition::Value => { + CallPosition::Value | CallPosition::LambdaArgument => { if entry.kind.is_action() { self.error_at( "action-in-value-position", @@ -1374,23 +1374,6 @@ impl Lowerer { ); } } - CallPosition::LambdaArgument => { - if entry.kind.is_action() { - self.error_at( - "action-in-value-position", - format!("action function '{name}' cannot be used as a value"), - span, - ); - } else if policy::function_context(&entry.id) - == Some(policy::FunctionContext::ForIterable) - { - self.error_at( - "invalid-call-context", - format!("'{name}' is only valid as a for-loop iterable"), - span, - ); - } - } CallPosition::MacroBody => {} } } diff --git a/crates/opy-rs/src/manifest/mod.rs b/crates/opy-rs/src/manifest/mod.rs index 96d59e28..3584bf94 100644 --- a/crates/opy-rs/src/manifest/mod.rs +++ b/crates/opy-rs/src/manifest/mod.rs @@ -348,23 +348,34 @@ impl Manifest { probes_file.schema_version ))); } + let ManifestFile { + schema_version, + reference, + functions, + aliases, + provenance, + } = file; let mut manifest = Manifest { - schema_version: file.schema_version, - reference: file.reference.clone(), - functions: Vec::new(), - aliases: Vec::new(), - provenance: file.provenance.clone(), + schema_version, + reference, + functions: Vec::with_capacity(functions.len()), + aliases: Vec::with_capacity(aliases.len()), + provenance, probes: probes_file.probes, by_function: HashMap::new(), by_member: HashMap::new(), alias_by_source: HashMap::new(), domain_identities: HashSet::new(), }; - manifest.validate(file)?; + manifest.validate(functions, aliases)?; Ok(manifest) } - fn validate(&mut self, file: ManifestFile) -> Result<(), ManifestError> { + fn validate( + &mut self, + functions: Vec, + aliases: Vec, + ) -> Result<(), ManifestError> { // Probe ids must be unique and must record the accept probes the // entries reference. let mut probes: HashMap<&str, &Probe> = HashMap::new(); @@ -377,7 +388,7 @@ impl Manifest { // Functions: unique ids, member-only receiver/kind combinations, // declared enum domains, declared enum-default members, and probe // evidence that records acceptance. - for function in &file.functions { + for function in functions { if self.by_function.contains_key(&function.id) { return Err(ManifestError(format!( "duplicate function id '{}'", @@ -465,7 +476,7 @@ impl Manifest { ))); } } - crate::lower::policy::validate(function).map_err(ManifestError)?; + crate::lower::policy::validate(&function).map_err(ManifestError)?; if let Some(contextual) = crate::lower::policy::contextual_domain(&function.id) { for option in contextual.options { self.domain_identities.insert(option.domain.to_string()); @@ -479,12 +490,12 @@ impl Manifest { self.by_function .insert(function.id.clone(), self.functions.len()); } - self.functions.push(function.clone()); + self.functions.push(function); } // Aliases: unique sources, declared targets of the matching class, // no collision with declared function ids. - for alias in &file.aliases { + for alias in aliases { if self.alias_by_source.contains_key(&alias.source) { return Err(ManifestError(format!( "duplicate alias source '{}'", @@ -520,7 +531,7 @@ impl Manifest { self.check_evidence(&alias.source, &alias.evidence, &probes)?; self.alias_by_source .insert(alias.source.clone(), self.aliases.len()); - self.aliases.push(alias.clone()); + self.aliases.push(alias); } Ok(()) diff --git a/crates/opy-rs/src/parser.rs b/crates/opy-rs/src/parser.rs index 0f36c620..1fb9a254 100644 --- a/crates/opy-rs/src/parser.rs +++ b/crates/opy-rs/src/parser.rs @@ -363,20 +363,10 @@ pub(crate) fn parse_expression_fragment( } fn shift_span(span: Span, origin: Position) -> Span { - fn shift(position: Position, origin: Position) -> Position { - Position::new( - origin.line + position.line.saturating_sub(1), - if position.line == 1 { - origin.col + position.col.saturating_sub(1) - } else { - position.col - }, - ) - } Span::new( span.file, - shift(span.start, origin), - shift(span.end, origin), + crate::diag::shift_position(span.start, origin), + crate::diag::shift_position(span.end, origin), ) } diff --git a/crates/opy-rs/src/parser/statements.rs b/crates/opy-rs/src/parser/statements.rs index bd35c11c..21767519 100644 --- a/crates/opy-rs/src/parser/statements.rs +++ b/crates/opy-rs/src/parser/statements.rs @@ -170,19 +170,7 @@ impl Parser<'_> { } .to_string(); self.advance(); - let rhs = self.parse_expr()?; - let end = rhs.span().end; - let value = Expr::Binary { - op, - left: Box::new(expr.clone()), - right: Box::new(rhs), - span: Span::new(start.file, start.start, end), - }; - Ok(Stmt::Assign { - target: expr, - value, - span: Span::new(start.file, start.start, end), - }) + self.finish_augmented_assignment(expr, start, op) } TokenKind::Ident if matches!(self.peek().text.as_str(), "min" | "max") @@ -190,19 +178,7 @@ impl Parser<'_> { { let op = self.advance().text; self.advance(); - let rhs = self.parse_expr()?; - let end = rhs.span().end; - let value = Expr::Binary { - op, - left: Box::new(expr.clone()), - right: Box::new(rhs), - span: Span::new(start.file, start.start, end), - }; - Ok(Stmt::Assign { - target: expr, - value, - span: Span::new(start.file, start.start, end), - }) + self.finish_augmented_assignment(expr, start, op) } TokenKind::Increment | TokenKind::Decrement => { let operator = self.advance(); @@ -244,6 +220,27 @@ impl Parser<'_> { } } + fn finish_augmented_assignment( + &mut self, + target: Expr, + start: Span, + op: String, + ) -> Result { + let rhs = self.parse_expr()?; + let end = rhs.span().end; + let span = Span::new(start.file, start.start, end); + Ok(Stmt::Assign { + target: target.clone(), + value: Expr::Binary { + op, + left: Box::new(target), + right: Box::new(rhs), + span, + }, + span, + }) + } + pub(super) fn parse_if(&mut self) -> Result { let indent = self.peek().span.start.col; self.open_if_indents.push(indent); diff --git a/crates/opy-rs/src/preprocess.rs b/crates/opy-rs/src/preprocess.rs index f751b132..45f71609 100644 --- a/crates/opy-rs/src/preprocess.rs +++ b/crates/opy-rs/src/preprocess.rs @@ -395,23 +395,10 @@ fn can_merge_without_separator(previous: TokenKind, current: TokenKind) -> bool } fn shift_settings_span(span: Span, origin: crate::diag::Position) -> Span { - fn shift( - position: crate::diag::Position, - origin: crate::diag::Position, - ) -> crate::diag::Position { - crate::diag::Position::new( - origin.line + position.line.saturating_sub(1), - if position.line == 1 { - origin.col + position.col.saturating_sub(1) - } else { - position.col - }, - ) - } Span::new( span.file, - shift(span.start, origin), - shift(span.end, origin), + crate::diag::shift_position(span.start, origin), + crate::diag::shift_position(span.end, origin), ) } diff --git a/crates/opy-rs/src/preprocess/macros.rs b/crates/opy-rs/src/preprocess/macros.rs index 02b9a1ef..a063c77a 100644 --- a/crates/opy-rs/src/preprocess/macros.rs +++ b/crates/opy-rs/src/preprocess/macros.rs @@ -431,24 +431,11 @@ fn raw_arg_text(tokens: &[Token]) -> String { } pub(super) fn shift_expansion_spans(tokens: &mut [Token], origin: Span) { - fn shift( - position: crate::diag::Position, - origin: crate::diag::Position, - ) -> crate::diag::Position { - crate::diag::Position::new( - origin.line + position.line.saturating_sub(1), - if position.line == 1 { - origin.col + position.col.saturating_sub(1) - } else { - position.col - }, - ) - } for token in tokens { token.span = Span::new( origin.file, - shift(token.span.start, origin.start), - shift(token.span.end, origin.start), + crate::diag::shift_position(token.span.start, origin.start), + crate::diag::shift_position(token.span.end, origin.start), ); } } diff --git a/crates/opy-rs/src/settings.rs b/crates/opy-rs/src/settings.rs index aeaea16a..fa54b4ef 100644 --- a/crates/opy-rs/src/settings.rs +++ b/crates/opy-rs/src/settings.rs @@ -402,17 +402,16 @@ fn resolve_hir_node( Position::new(span.start.line, span.start.col), ) .map_err(|error| settings_expression_error(diag_span, error.message))?; - let expression = - expander - .expand_expr(&expression, &HashMap::new()) - .map_err(|error| { - let error_span = error - .diagnostic - .span - .map(hir_span_to_diag_span) - .unwrap_or(diag_span); - OpyError::at(error.diagnostic.code, error.diagnostic.message, error_span) - })?; + let expression = expander + .expand_expr(&expression, &HashMap::new(), None) + .map_err(|error| { + let error_span = error + .diagnostic + .span + .map(hir_span_to_diag_span) + .unwrap_or(diag_span); + OpyError::at(error.diagnostic.code, error.diagnostic.message, error_span) + })?; let mut stack = Vec::new(); let value = crate::compile_time::evaluate(&expression, constants, &HashMap::new(), &mut stack) From ab20300ef1996bd5f4935f3542769abcc78cfa30 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Sun, 27 Sep 2026 23:14:09 +0800 Subject: [PATCH 02/67] refactor(opy-rs): centralize HIR traversal --- crates/opy-rs/src/compiler/lowering.rs | 517 +++++-------------------- crates/opy-rs/src/hir/mod.rs | 1 + crates/opy-rs/src/hir/validate.rs | 433 +++++++-------------- crates/opy-rs/src/hir/visit.rs | 180 +++++++++ crates/opy-rs/src/tooling.rs | 292 ++++---------- 5 files changed, 483 insertions(+), 940 deletions(-) create mode 100644 crates/opy-rs/src/hir/visit.rs diff --git a/crates/opy-rs/src/compiler/lowering.rs b/crates/opy-rs/src/compiler/lowering.rs index 2f9e6c6e..156352a8 100644 --- a/crates/opy-rs/src/compiler/lowering.rs +++ b/crates/opy-rs/src/compiler/lowering.rs @@ -5186,61 +5186,38 @@ impl<'a> Lowering<'a> { { Value::Number(value) } else { - match op.as_str() { - "==" | "!=" | "<" | "<=" | ">" | ">=" => Value::Call { - name: op.clone(), - args: self.value_args(&[left, right]), - }, - "+" => Value::Call { - name: "add".to_string(), - args: self.value_args(&[left, right]), - }, - "-" => Value::Call { - name: "subtract".to_string(), - args: self.value_args(&[left, right]), - }, - "*" => Value::Call { - name: "multiply".to_string(), - args: self.value_args(&[left, right]), - }, - "/" => Value::Call { - name: "divide".to_string(), - args: self.value_args(&[left, right]), - }, - "%" => Value::Call { - name: "modulo".to_string(), - args: self.value_args(&[left, right]), - }, - "**" => Value::Call { - name: "raiseToPower".to_string(), - args: self.value_args(&[left, right]), - }, - "and" => Value::Call { - name: "and".to_string(), - args: self.value_args(&[left, right]), - }, - "or" => Value::Call { - name: "or".to_string(), - args: self.value_args(&[left, right]), - }, - "in" => Value::Call { + if op == "in" { + Value::Call { name: "arrayContains".to_string(), args: self.value_args(&[right, left]), - }, - "not in" => { - let contains = self.push_call("arrayContains", vec![right, left]); - Value::Call { - name: "not".to_string(), - args: self.value_args(&[contains]), - } } - _ => { - return Err(self.unsupported( - format!( - "binary operator '{op}' is not currently representable in canonical WIR" - ), - span, - )); + } else if op == "not in" { + let contains = self.push_call("arrayContains", vec![right, left]); + Value::Call { + name: "not".to_string(), + args: self.value_args(&[contains]), + } + } else { + let name = match op.as_str() { + "==" | "!=" | "<" | "<=" | ">" | ">=" | "and" | "or" => op, + "+" => "add", + "-" => "subtract", + "*" => "multiply", + "/" => "divide", + "%" => "modulo", + "**" => "raiseToPower", + _ => { + return Err(self.unsupported( + format!( + "binary operator '{op}' is not currently representable in canonical WIR" + ), + span, + )); + } + }; + Value::Call { + name: name.to_string(), + args: self.value_args(&[left, right]), } } } @@ -7222,68 +7199,48 @@ fn implicit_default_variables( BTreeMap>, BTreeMap>, ) { - let declared_globals = hir - .declarations - .iter() - .filter_map(|declaration| match declaration { - hir::Declaration::GlobalVariable { name, .. } => Some(name.as_str()), - _ => None, - }) - .collect::>(); - let declared_players = hir - .declarations - .iter() - .filter_map(|declaration| match declaration { - hir::Declaration::PlayerVariable { name, .. } => Some(name.as_str()), - _ => None, - }) - .collect::>(); - let mut globals = BTreeMap::new(); - let mut players = BTreeMap::new(); + let mut collector = ImplicitVariableCollector { + declared_globals: hir + .declarations + .iter() + .filter_map(|declaration| match declaration { + hir::Declaration::GlobalVariable { name, .. } => Some(name.as_str()), + _ => None, + }) + .collect(), + declared_players: hir + .declarations + .iter() + .filter_map(|declaration| match declaration { + hir::Declaration::PlayerVariable { name, .. } => Some(name.as_str()), + _ => None, + }) + .collect(), + globals: BTreeMap::new(), + players: BTreeMap::new(), + }; for declaration in &hir.declarations { let initializer = match declaration { hir::Declaration::GlobalVariable { initializer, .. } - | hir::Declaration::PlayerVariable { initializer, .. } => initializer.as_ref(), - hir::Declaration::Constant { value, .. } => Some(value), + | hir::Declaration::PlayerVariable { initializer, .. } => initializer.as_deref(), + hir::Declaration::Constant { value, .. } => Some(value.as_ref()), _ => None, }; - if let Some(expr) = initializer { - collect_implicit_expr( - expr, - &declared_globals, - &declared_players, - &mut globals, - &mut players, - ); + if let Some(expression) = initializer { + hir::visit::Visitor::visit_expr(&mut collector, expression); } } for entry in &hir.rules { match entry { RuleEntry::Rule(rule) => { for condition in &rule.conditions { - collect_implicit_expr( - condition, - &declared_globals, - &declared_players, - &mut globals, - &mut players, - ); + hir::visit::Visitor::visit_expr(&mut collector, condition); } - collect_implicit_stmts( - &rule.actions, - &declared_globals, - &declared_players, - &mut globals, - &mut players, - ); + hir::visit::walk_stmts(&mut collector, &rule.actions); + } + RuleEntry::SubroutineDef { body, .. } => { + hir::visit::walk_stmts(&mut collector, body); } - RuleEntry::SubroutineDef { body, .. } => collect_implicit_stmts( - body, - &declared_globals, - &declared_players, - &mut globals, - &mut players, - ), } } if hir @@ -7292,338 +7249,50 @@ fn implicit_default_variables( .iter() .any(|directive| directive.name == "translateWithPlayerVar") { - players.insert("__languageIndex__".to_string(), None); + collector + .players + .insert("__languageIndex__".to_string(), None); } - (globals, players) + (collector.globals, collector.players) } -fn collect_implicit_stmts( - statements: &[Stmt], - declared_globals: &HashSet<&str>, - declared_players: &HashSet<&str>, - globals: &mut BTreeMap>, - players: &mut BTreeMap>, -) { - for statement in statements { - match statement { - Stmt::Expr { expr, .. } => { - collect_implicit_expr(expr, declared_globals, declared_players, globals, players) - } - Stmt::Assign { target, value, .. } => { - collect_implicit_expr(target, declared_globals, declared_players, globals, players); - collect_implicit_expr(value, declared_globals, declared_players, globals, players); - } - Stmt::Delete { target, .. } => { - collect_implicit_expr(target, declared_globals, declared_players, globals, players); - } - Stmt::If { - branches, r#else, .. - } => { - for branch in branches { - collect_implicit_expr( - &branch.condition, - declared_globals, - declared_players, - globals, - players, - ); - collect_implicit_stmts( - &branch.body, - declared_globals, - declared_players, - globals, - players, - ); - } - if let Some(default_body) = r#else { - collect_implicit_stmts( - default_body, - declared_globals, - declared_players, - globals, - players, - ); - } - } - Stmt::For { - variable, - iterable, - body, - .. - } => { - collect_implicit_expr( - variable, - declared_globals, - declared_players, - globals, - players, - ); - collect_implicit_expr( - iterable, - declared_globals, - declared_players, - globals, - players, - ); - collect_implicit_stmts(body, declared_globals, declared_players, globals, players); - } - Stmt::While { - condition, body, .. - } - | Stmt::DoWhile { - condition, body, .. - } => { - collect_implicit_expr( - condition, - declared_globals, - declared_players, - globals, - players, - ); - collect_implicit_stmts(body, declared_globals, declared_players, globals, players); - } - Stmt::Switch { value, arms, .. } => { - collect_implicit_expr(value, declared_globals, declared_players, globals, players); - for arm in arms { - match arm { - SwitchArm::Case { value, body, .. } => { - collect_implicit_expr( - value, - declared_globals, - declared_players, - globals, - players, - ); - collect_implicit_stmts( - body, - declared_globals, - declared_players, - globals, - players, - ); - } - SwitchArm::Default { body, .. } => { - collect_implicit_stmts( - body, - declared_globals, - declared_players, - globals, - players, - ); - } - } - } - } - Stmt::Goto { offset, .. } => { - if let Some(offset) = offset { - collect_implicit_expr( - offset, - declared_globals, - declared_players, - globals, - players, - ); - } - } - Stmt::Break { .. } - | Stmt::Return { .. } - | Stmt::Continue { .. } - | Stmt::Label { .. } - | Stmt::CallSubroutine { .. } - | Stmt::Pass { .. } => {} - } - } +struct ImplicitVariableCollector<'a> { + declared_globals: HashSet<&'a str>, + declared_players: HashSet<&'a str>, + globals: BTreeMap>, + players: BTreeMap>, } -fn collect_implicit_expr( - expr: &Expr, - declared_globals: &HashSet<&str>, - declared_players: &HashSet<&str>, - globals: &mut BTreeMap>, - players: &mut BTreeMap>, -) { - match expr { - Expr::GlobalVar { name, span } => { - if !declared_globals.contains(name.as_str()) && default_var_index(name).is_some() { - globals.entry(name.clone()).or_insert(*span); - } - } - Expr::Array { elements, .. } => { - for element in elements { - collect_implicit_expr( - element, - declared_globals, - declared_players, - globals, - players, - ); - } - } - Expr::Dict { entries, .. } => { - for entry in entries { - collect_implicit_expr( - &entry.key, - declared_globals, - declared_players, - globals, - players, - ); - collect_implicit_expr( - &entry.value, - declared_globals, - declared_players, - globals, - players, - ); - } - } - Expr::Comprehension { - element, - iterable, - condition, - .. - } => { - collect_implicit_expr( - element, - declared_globals, - declared_players, - globals, - players, - ); - collect_implicit_expr( - iterable, - declared_globals, - declared_players, - globals, - players, - ); - if let Some(condition) = condition { - collect_implicit_expr( - condition, - declared_globals, - declared_players, - globals, - players, - ); - } - } - Expr::Lambda { body, .. } => { - collect_implicit_expr(body, declared_globals, declared_players, globals, players) - } - Expr::Type { args, .. } => { - for arg in args { - collect_implicit_expr(arg, declared_globals, declared_players, globals, players); - } - } - Expr::Vector { x, y, z, .. } => { - collect_implicit_expr(x, declared_globals, declared_players, globals, players); - collect_implicit_expr(y, declared_globals, declared_players, globals, players); - collect_implicit_expr(z, declared_globals, declared_players, globals, players); - } - Expr::PlayerVar { - player, - name, - member_span, - span, - } => { - if !declared_players.contains(name.as_str()) && default_var_index(name).is_some() { - players.entry(name.clone()).or_insert(member_span.or(*span)); - } - collect_implicit_expr(player, declared_globals, declared_players, globals, players); - } - Expr::Member { - receiver, - member, - span, - .. - } => { - if !declared_players.contains(member.as_str()) && default_var_index(member).is_some() { - players.entry(member.clone()).or_insert(*span); +impl hir::visit::Visitor for ImplicitVariableCollector<'_> { + fn visit_expr(&mut self, expression: &Expr) { + match expression { + Expr::GlobalVar { name, span } + if !self.declared_globals.contains(name.as_str()) + && default_var_index(name).is_some() => + { + self.globals.entry(name.clone()).or_insert(*span); } - collect_implicit_expr( - receiver, - declared_globals, - declared_players, - globals, - players, - ); - } - Expr::Call { args, .. } | Expr::MacroCall { args, .. } => { - for arg in args { - collect_implicit_expr(arg, declared_globals, declared_players, globals, players); + Expr::PlayerVar { + name, + member_span, + span, + .. + } if !self.declared_players.contains(name.as_str()) + && default_var_index(name).is_some() => + { + self.players + .entry(name.clone()) + .or_insert(member_span.or(*span)); } - } - Expr::ReceiverCall { receiver, args, .. } => { - collect_implicit_expr( - receiver, - declared_globals, - declared_players, - globals, - players, - ); - for arg in args { - collect_implicit_expr(arg, declared_globals, declared_players, globals, players); + Expr::Member { member, span, .. } + if !self.declared_players.contains(member.as_str()) + && default_var_index(member).is_some() => + { + self.players.entry(member.clone()).or_insert(*span); } + _ => {} } - Expr::Binary { left, right, .. } => { - collect_implicit_expr(left, declared_globals, declared_players, globals, players); - collect_implicit_expr(right, declared_globals, declared_players, globals, players); - } - Expr::Conditional { - then_value, - condition, - else_value, - .. - } => { - collect_implicit_expr( - then_value, - declared_globals, - declared_players, - globals, - players, - ); - collect_implicit_expr( - condition, - declared_globals, - declared_players, - globals, - players, - ); - collect_implicit_expr( - else_value, - declared_globals, - declared_players, - globals, - players, - ); - } - Expr::Unary { operand, .. } => collect_implicit_expr( - operand, - declared_globals, - declared_players, - globals, - players, - ), - Expr::Index { array, index, .. } => { - collect_implicit_expr(array, declared_globals, declared_players, globals, players); - collect_implicit_expr(index, declared_globals, declared_players, globals, players); - } - Expr::Format { args, .. } => { - for arg in args { - collect_implicit_expr(arg, declared_globals, declared_players, globals, players); - } - } - Expr::Number { .. } - | Expr::String { .. } - | Expr::Bool { .. } - | Expr::Null { .. } - | Expr::StringModifier { .. } - | Expr::Local { .. } - | Expr::Enum { .. } - | Expr::EventPlayer { .. } - | Expr::HostPlayer { .. } - | Expr::Constant { .. } - | Expr::MacroParam { .. } => {} + hir::visit::walk_expr(self, expression); } } diff --git a/crates/opy-rs/src/hir/mod.rs b/crates/opy-rs/src/hir/mod.rs index 44954e09..27df3b44 100644 --- a/crates/opy-rs/src/hir/mod.rs +++ b/crates/opy-rs/src/hir/mod.rs @@ -13,6 +13,7 @@ pub mod dump; pub mod error; pub mod types; mod validate; +pub(crate) mod visit; pub use error::HirError; pub use types::{ diff --git a/crates/opy-rs/src/hir/validate.rs b/crates/opy-rs/src/hir/validate.rs index 0a3fab45..10968c07 100644 --- a/crates/opy-rs/src/hir/validate.rs +++ b/crates/opy-rs/src/hir/validate.rs @@ -23,6 +23,7 @@ use super::types::{ Declaration, Expr, PROTOCOL_MAJOR, PROTOCOL_NAME, Position, Program, Rule, RuleEntry, Settings, SettingsNode, Span, Stmt, SwitchArm, default_var_index, }; +use super::visit::{self, Visitor}; /// Declaration `kind` values understood by this consumer. const DECLARATION_KINDS: &[&str] = &[ @@ -232,9 +233,7 @@ pub(crate) fn validate_program(program: &Program) -> Result<(), HirError> { )); } } - validate_stmts(body, program, &tables, |statement| { - statement.span().copied() - })?; + validate_stmts(body, program, &tables)?; } } } @@ -272,9 +271,7 @@ pub(crate) fn validate_program(program: &Program) -> Result<(), HirError> { if !source_name.is_empty() { check_name(source_name, "subroutine source", *span)?; } - validate_stmts(body, program, &tables, |statement| { - statement.span().copied() - })?; + validate_stmts(body, program, &tables)?; } } } @@ -366,39 +363,137 @@ fn validate_rule(rule: &Rule, program: &Program, tables: &NameTables<'_>) -> Res validate_expr_vec(&rule.event.args, program, tables)?; check_span(rule.event.span, program.files.len())?; validate_expr_vec(&rule.conditions, program, tables)?; - validate_stmts(&rule.actions, program, tables, |statement| { - statement.span().copied() - }) + validate_stmts(&rule.actions, program, tables) } -/// Validate a list of owned expressions (used for event args and rule -/// conditions). fn validate_expr_vec( - exprs: &[Expr], + expressions: &[Expr], program: &Program, tables: &NameTables<'_>, ) -> Result<(), HirError> { - let refs: Vec<&Expr> = exprs.iter().collect(); - validate_exprs(&refs, program, tables) + validate_exprs(expressions, program, tables) } -/// Validate a statement list: every span, every nested expression and its -/// references, subroutine-call targets, and for-loop variables. fn validate_stmts( statements: &[Stmt], program: &Program, tables: &NameTables<'_>, - span_of: impl Fn(&Stmt) -> Option, ) -> Result<(), HirError> { - let mut errors = Vec::new(); - for_each_stmt(statements, &mut |statement| { - if let Err(error) = check_span(span_of(statement), program.files.len()) { - errors.push(error); + let mut validator = StatementValidator { + expressions: ExpressionValidator { + program, + tables, + errors: Vec::new(), + }, + errors: Vec::new(), + }; + visit::walk_stmts(&mut validator, statements); + let StatementValidator { + expressions, + errors, + } = validator; + errors + .into_iter() + .chain(expressions.errors) + .next() + .map_or(Ok(()), Err) +} + +fn validate_expr(expr: &Expr, program: &Program, tables: &NameTables<'_>) -> Result<(), HirError> { + validate_exprs(std::iter::once(expr), program, tables) +} + +fn validate_exprs<'expr>( + expressions: impl IntoIterator, + program: &Program, + tables: &NameTables<'_>, +) -> Result<(), HirError> { + let mut validator = ExpressionValidator { + program, + tables, + errors: Vec::new(), + }; + for expression in expressions { + Visitor::visit_expr(&mut validator, expression); + } + validator.errors.into_iter().next().map_or(Ok(()), Err) +} + +struct ExpressionValidator<'program, 'tables, 'names> { + program: &'program Program, + tables: &'tables NameTables<'names>, + errors: Vec, +} + +impl Visitor for ExpressionValidator<'_, '_, '_> { + fn visit_expr(&mut self, expression: &Expr) { + if let Err(error) = check_span(expression.span().copied(), self.program.files.len()) { + self.errors.push(error); + } + match expression { + Expr::GlobalVar { name, span } + if !self.tables.globals.contains(&name.as_str()) + && default_var_index(name).is_none() => + { + self.errors.push(invalid( + "unresolved-reference", + format!("reference to unknown global variable '{name}'"), + *span, + )); + } + Expr::PlayerVar { + player, name, span, .. + } if !self.tables.players.contains(&name.as_str()) + && !is_implicit_player_variable(player, name) => + { + self.errors.push(invalid( + "unresolved-reference", + format!("reference to unknown player variable '{name}'"), + *span, + )); + } + Expr::Constant { name, span } if !self.tables.constants.contains(&name.as_str()) => { + self.errors.push(invalid( + "unresolved-reference", + format!("reference to unknown constant '{name}'"), + *span, + )); + } + _ => {} + } + visit::walk_expr(self, expression); + } + + fn visit_comprehension(&mut self, element: &Expr, iterable: &Expr, condition: Option<&Expr>) { + Visitor::visit_expr(self, iterable); + Visitor::visit_expr(self, element); + if let Some(condition) = condition { + Visitor::visit_expr(self, condition); + } + } +} + +struct StatementValidator<'program, 'tables, 'names> { + expressions: ExpressionValidator<'program, 'tables, 'names>, + errors: Vec, +} + +impl Visitor for StatementValidator<'_, '_, '_> { + fn visit_expr(&mut self, expression: &Expr) { + Visitor::visit_expr(&mut self.expressions, expression); + } + + fn visit_stmt(&mut self, statement: &Stmt) { + if let Err(error) = check_span( + statement.span().copied(), + self.expressions.program.files.len(), + ) { + self.errors.push(error); } match statement { Stmt::CallSubroutine { name, span } => { - let known = tables.subroutines.contains(&name.as_str()) - || program.rules.iter().any(|entry| { + let known = self.expressions.tables.subroutines.contains(&name.as_str()) + || self.expressions.program.rules.iter().any(|entry| { matches!( entry, RuleEntry::SubroutineDef { @@ -409,7 +504,7 @@ fn validate_stmts( ) }); if !known { - errors.push(invalid( + self.errors.push(invalid( "unresolved-reference", format!("call to unknown subroutine '{name}'"), *span, @@ -418,22 +513,22 @@ fn validate_stmts( } Stmt::For { variable, span, .. } => match variable.as_ref() { Expr::GlobalVar { name, .. } - if tables.globals.contains(&name.as_str()) + if self.expressions.tables.globals.contains(&name.as_str()) || default_var_index(name).is_some() => {} - Expr::GlobalVar { name, .. } => errors.push(invalid( + Expr::GlobalVar { name, .. } => self.errors.push(invalid( "unresolved-reference", format!("for-loop variable '{name}' is not a declared global variable"), *span, )), Expr::PlayerVar { name, player, .. } - if tables.players.contains(&name.as_str()) + if self.expressions.tables.players.contains(&name.as_str()) || is_implicit_player_variable(player, name) => {} - Expr::PlayerVar { name, .. } => errors.push(invalid( + Expr::PlayerVar { name, .. } => self.errors.push(invalid( "unresolved-reference", format!("for-loop variable '{name}' is not a declared player variable"), *span, )), - other => errors.push(invalid( + other => self.errors.push(invalid( "invalid-structure", format!( "for-loop variable must be a global variable reference, got '{}'", @@ -444,7 +539,7 @@ fn validate_stmts( }, Stmt::Switch { arms, .. } => { if arms.is_empty() { - errors.push(invalid( + self.errors.push(invalid( "invalid-structure", "a switch must contain at least one arm", None, @@ -453,18 +548,16 @@ fn validate_stmts( let mut defaults = 0; for arm in arms { let arm_span = match arm { - SwitchArm::Case { span, body, .. } | SwitchArm::Default { span, body } => { - let _ = body; - span - } + SwitchArm::Case { span, .. } | SwitchArm::Default { span, .. } => span, }; - if let Err(error) = check_span(*arm_span, program.files.len()) { - errors.push(error); + if let Err(error) = check_span(*arm_span, self.expressions.program.files.len()) + { + self.errors.push(error); } if matches!(arm, SwitchArm::Default { .. }) { defaults += 1; if defaults > 1 { - errors.push(invalid( + self.errors.push(invalid( "invalid-structure", "a switch may contain at most one default arm", *arm_span, @@ -475,7 +568,7 @@ fn validate_stmts( } Stmt::Delete { target, span } => { if !matches!(target.as_ref(), Expr::Index { .. }) { - errors.push(invalid( + self.errors.push(invalid( "invalid-structure", "a delete statement must target an array index", *span, @@ -487,12 +580,11 @@ fn validate_stmts( offset, rule_start, span, - .. } => { let target_count = u8::from(label.is_some()) + u8::from(offset.is_some()) + u8::from(*rule_start); if target_count != 1 { - errors.push(invalid( + self.errors.push(invalid( "invalid-structure", "a goto must contain exactly one label, offset, or RULE_START target", *span, @@ -500,154 +592,25 @@ fn validate_stmts( } if let Some(label) = label { if let Err(error) = check_name(label, "label", *span) { - errors.push(error); + self.errors.push(error); } } } Stmt::Label { name, span } => { if let Err(error) = check_name(name, "label", *span) { - errors.push(error); + self.errors.push(error); } } _ => {} } - }); - validate_exprs(&statement_exprs(statements), program, tables)?; - errors.into_iter().next().map_or(Ok(()), Err) -} - -/// Validate a single expression, including its children. -fn validate_expr(expr: &Expr, program: &Program, tables: &NameTables<'_>) -> Result<(), HirError> { - validate_exprs(&[expr], program, tables) -} - -/// Validate every expression in a list, including spans and references. -fn validate_exprs( - exprs: &[&Expr], - program: &Program, - tables: &NameTables<'_>, -) -> Result<(), HirError> { - let mut errors = Vec::new(); - for expr in exprs { - for_each_expr(expr, &mut |node| { - if let Err(error) = check_span(node.span().copied(), program.files.len()) { - errors.push(error); - } - match node { - Expr::GlobalVar { name, span } - if !tables.globals.contains(&name.as_str()) - && default_var_index(name).is_none() => - { - errors.push(invalid( - "unresolved-reference", - format!("reference to unknown global variable '{name}'"), - *span, - )); - } - Expr::PlayerVar { - player, name, span, .. - } if !tables.players.contains(&name.as_str()) - && !is_implicit_player_variable(player, name) => - { - errors.push(invalid( - "unresolved-reference", - format!("reference to unknown player variable '{name}'"), - *span, - )); - } - Expr::Constant { name, span } if !tables.constants.contains(&name.as_str()) => { - errors.push(invalid( - "unresolved-reference", - format!("reference to unknown constant '{name}'"), - *span, - )); - } - _ => {} - } - }); + visit::walk_stmt(self, statement); } - errors.into_iter().next().map_or(Ok(()), Err) } fn is_implicit_player_variable(_player: &Expr, name: &str) -> bool { default_var_index(name).is_some() } -/// The expressions directly contained in a statement list (used to feed -/// `validate_exprs`; nested bodies are covered by `for_each_stmt`). -fn statement_exprs(statements: &[Stmt]) -> Vec<&Expr> { - let mut exprs = Vec::new(); - for statement in statements { - match statement { - Stmt::Expr { expr, .. } => exprs.push(expr.as_ref()), - Stmt::Assign { target, value, .. } => { - exprs.push(target.as_ref()); - exprs.push(value.as_ref()); - } - Stmt::Delete { target, .. } => exprs.push(target.as_ref()), - Stmt::If { - branches, r#else, .. - } => { - for branch in branches { - exprs.push(branch.condition.as_ref()); - exprs.extend(statement_exprs(&branch.body)); - } - if let Some(else_body) = r#else { - exprs.extend(statement_exprs(else_body)); - } - } - Stmt::For { - variable, - iterable, - body, - .. - } => { - exprs.push(variable.as_ref()); - exprs.push(iterable.as_ref()); - exprs.extend(statement_exprs(body)); - } - Stmt::While { - condition, body, .. - } => { - exprs.push(condition.as_ref()); - exprs.extend(statement_exprs(body)); - } - Stmt::DoWhile { - condition, body, .. - } => { - exprs.push(condition.as_ref()); - exprs.extend(statement_exprs(body)); - } - Stmt::Switch { value, arms, .. } => { - exprs.push(value.as_ref()); - for arm in arms { - match arm { - SwitchArm::Case { value, body, .. } => { - exprs.push(value.as_ref()); - exprs.extend(statement_exprs(body)); - } - SwitchArm::Default { body, .. } => { - exprs.extend(statement_exprs(body)); - } - } - } - } - Stmt::Goto { offset, .. } => { - if let Some(offset) = offset { - exprs.push(offset.as_ref()); - } - } - Stmt::Break { .. } - | Stmt::Return { .. } - | Stmt::Continue { .. } - | Stmt::Label { .. } - | Stmt::CallSubroutine { .. } - | Stmt::Pass { .. } => {} - } - } - exprs -} - fn check_name(name: &str, what: &str, span: Option) -> Result<(), HirError> { if name.is_empty() { return Err(invalid( @@ -709,132 +672,6 @@ fn valid_position(position: &Position) -> bool { position.line >= 1 && position.col >= 1 } -/// Visit every statement in a tree (including nested bodies). -fn for_each_stmt<'a>(statements: &'a [Stmt], f: &mut impl FnMut(&'a Stmt)) { - for statement in statements { - f(statement); - match statement { - Stmt::If { - branches, r#else, .. - } => { - for branch in branches { - for_each_stmt(&branch.body, f); - } - if let Some(else_body) = r#else { - for_each_stmt(else_body, f); - } - } - Stmt::For { body, .. } | Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => { - for_each_stmt(body, f) - } - Stmt::Switch { arms, .. } => { - for arm in arms { - match arm { - SwitchArm::Case { body, .. } | SwitchArm::Default { body, .. } => { - for_each_stmt(body, f); - } - } - } - } - Stmt::Expr { .. } - | Stmt::Assign { .. } - | Stmt::Delete { .. } - | Stmt::Break { .. } - | Stmt::Return { .. } - | Stmt::Continue { .. } - | Stmt::Goto { .. } - | Stmt::Label { .. } - | Stmt::CallSubroutine { .. } - | Stmt::Pass { .. } => {} - } - } -} - -/// Visit every expression in a list (including nested children). -fn for_each_expr<'a>(expr: &'a Expr, f: &mut impl FnMut(&'a Expr)) { - f(expr); - match expr { - Expr::Array { elements, .. } => { - for element in elements { - for_each_expr(element, f); - } - } - Expr::Dict { entries, .. } => { - for entry in entries { - for_each_expr(&entry.key, f); - for_each_expr(&entry.value, f); - } - } - Expr::Comprehension { - element, - iterable, - condition, - .. - } => { - for_each_expr(iterable, f); - for_each_expr(element, f); - if let Some(condition) = condition { - for_each_expr(condition, f); - } - } - Expr::Lambda { body, .. } => for_each_expr(body, f), - Expr::Vector { x, y, z, .. } => { - for_each_expr(x, f); - for_each_expr(y, f); - for_each_expr(z, f); - } - Expr::PlayerVar { player, .. } => for_each_expr(player, f), - Expr::Member { receiver, .. } => for_each_expr(receiver, f), - Expr::Call { args, .. } | Expr::MacroCall { args, .. } | Expr::Format { args, .. } => { - for arg in args { - for_each_expr(arg, f); - } - } - Expr::ReceiverCall { receiver, args, .. } => { - for_each_expr(receiver, f); - for arg in args { - for_each_expr(arg, f); - } - } - Expr::Binary { left, right, .. } => { - for_each_expr(left, f); - for_each_expr(right, f); - } - Expr::Conditional { - then_value, - condition, - else_value, - .. - } => { - for_each_expr(then_value, f); - for_each_expr(condition, f); - for_each_expr(else_value, f); - } - Expr::Unary { operand, .. } => for_each_expr(operand, f), - Expr::Index { array, index, .. } => { - for_each_expr(array, f); - for_each_expr(index, f); - } - Expr::Type { args, .. } => { - for arg in args { - for_each_expr(arg, f); - } - } - Expr::Number { .. } - | Expr::String { .. } - | Expr::Bool { .. } - | Expr::Null { .. } - | Expr::Enum { .. } - | Expr::GlobalVar { .. } - | Expr::EventPlayer { .. } - | Expr::HostPlayer { .. } - | Expr::Constant { .. } - | Expr::MacroParam { .. } - | Expr::StringModifier { .. } - | Expr::Local { .. } => {} - } -} - fn check_declaration(value: &Value) -> Result<(), HirError> { let Some(object) = value.as_object() else { return Ok(()); diff --git a/crates/opy-rs/src/hir/visit.rs b/crates/opy-rs/src/hir/visit.rs new file mode 100644 index 00000000..3e25d272 --- /dev/null +++ b/crates/opy-rs/src/hir/visit.rs @@ -0,0 +1,180 @@ +use super::types::{Expr, Stmt, SwitchArm}; + +pub(crate) trait Visitor { + fn visit_expr(&mut self, expression: &Expr) { + walk_expr(self, expression); + } + + fn visit_comprehension(&mut self, element: &Expr, iterable: &Expr, condition: Option<&Expr>) { + walk_comprehension(self, element, iterable, condition); + } + + fn visit_stmt(&mut self, statement: &Stmt) { + walk_stmt(self, statement); + } +} + +pub(crate) fn walk_expr(visitor: &mut V, expression: &Expr) { + match expression { + Expr::Array { elements, .. } => { + for element in elements { + visitor.visit_expr(element); + } + } + Expr::Dict { entries, .. } => { + for entry in entries { + visitor.visit_expr(&entry.key); + visitor.visit_expr(&entry.value); + } + } + Expr::Comprehension { + element, + iterable, + condition, + .. + } => visitor.visit_comprehension(element, iterable, condition.as_deref()), + Expr::Lambda { body, .. } => visitor.visit_expr(body), + Expr::Type { args, .. } | Expr::Call { args, .. } | Expr::MacroCall { args, .. } => { + for argument in args { + visitor.visit_expr(argument); + } + } + Expr::Vector { x, y, z, .. } => { + visitor.visit_expr(x); + visitor.visit_expr(y); + visitor.visit_expr(z); + } + Expr::PlayerVar { player, .. } + | Expr::Member { + receiver: player, .. + } => { + visitor.visit_expr(player); + } + Expr::ReceiverCall { receiver, args, .. } => { + visitor.visit_expr(receiver); + for argument in args { + visitor.visit_expr(argument); + } + } + Expr::Binary { left, right, .. } + | Expr::Index { + array: left, + index: right, + .. + } => { + visitor.visit_expr(left); + visitor.visit_expr(right); + } + Expr::Conditional { + then_value, + condition, + else_value, + .. + } => { + visitor.visit_expr(then_value); + visitor.visit_expr(condition); + visitor.visit_expr(else_value); + } + Expr::Unary { operand, .. } => visitor.visit_expr(operand), + Expr::Format { args, .. } => { + for argument in args { + visitor.visit_expr(argument); + } + } + Expr::Number { .. } + | Expr::String { .. } + | Expr::Bool { .. } + | Expr::Null { .. } + | Expr::StringModifier { .. } + | Expr::Local { .. } + | Expr::Enum { .. } + | Expr::GlobalVar { .. } + | Expr::HostPlayer { .. } + | Expr::EventPlayer { .. } + | Expr::Constant { .. } + | Expr::MacroParam { .. } => {} + } +} + +pub(crate) fn walk_comprehension( + visitor: &mut V, + element: &Expr, + iterable: &Expr, + condition: Option<&Expr>, +) { + visitor.visit_expr(element); + visitor.visit_expr(iterable); + if let Some(condition) = condition { + visitor.visit_expr(condition); + } +} + +pub(crate) fn walk_stmts(visitor: &mut V, statements: &[Stmt]) { + for statement in statements { + visitor.visit_stmt(statement); + } +} + +pub(crate) fn walk_stmt(visitor: &mut V, statement: &Stmt) { + match statement { + Stmt::Expr { expr, .. } => visitor.visit_expr(expr), + Stmt::Assign { target, value, .. } => { + visitor.visit_expr(target); + visitor.visit_expr(value); + } + Stmt::If { + branches, r#else, .. + } => { + for branch in branches { + visitor.visit_expr(&branch.condition); + walk_stmts(visitor, &branch.body); + } + if let Some(default_body) = r#else { + walk_stmts(visitor, default_body); + } + } + Stmt::For { + variable, + iterable, + body, + .. + } => { + visitor.visit_expr(variable); + visitor.visit_expr(iterable); + walk_stmts(visitor, body); + } + Stmt::While { + condition, body, .. + } + | Stmt::DoWhile { + condition, body, .. + } => { + visitor.visit_expr(condition); + walk_stmts(visitor, body); + } + Stmt::Switch { value, arms, .. } => { + visitor.visit_expr(value); + for arm in arms { + match arm { + SwitchArm::Case { value, body, .. } => { + visitor.visit_expr(value); + walk_stmts(visitor, body); + } + SwitchArm::Default { body, .. } => walk_stmts(visitor, body), + } + } + } + Stmt::Delete { target, .. } => visitor.visit_expr(target), + Stmt::Goto { offset, .. } => { + if let Some(offset) = offset { + visitor.visit_expr(offset); + } + } + Stmt::Break { .. } + | Stmt::Return { .. } + | Stmt::Continue { .. } + | Stmt::Label { .. } + | Stmt::CallSubroutine { .. } + | Stmt::Pass { .. } => {} + } +} diff --git a/crates/opy-rs/src/tooling.rs b/crates/opy-rs/src/tooling.rs index 177c522b..a7c1c843 100644 --- a/crates/opy-rs/src/tooling.rs +++ b/crates/opy-rs/src/tooling.rs @@ -498,9 +498,9 @@ impl SemanticModel { }); } - let mut sites: Vec<(SymbolKind, String, Span)> = Vec::new(); - for decl in &self.hir.declarations { - match decl { + let mut collector = ReferenceSiteCollector { sites: Vec::new() }; + for declaration in &self.hir.declarations { + match declaration { Declaration::GlobalVariable { initializer: Some(initializer), .. @@ -508,44 +508,37 @@ impl SemanticModel { | Declaration::PlayerVariable { initializer: Some(initializer), .. - } => Self::collect_expr(initializer, &mut sites), - Declaration::Constant { value, .. } => Self::collect_expr(value, &mut sites), - Declaration::Macro { body, .. } => { - for stmt in body { - Self::collect_stmt(stmt, &mut sites); - } + } => hir::visit::Visitor::visit_expr(&mut collector, initializer), + Declaration::Constant { value, .. } => { + hir::visit::Visitor::visit_expr(&mut collector, value) } + Declaration::Macro { body, .. } => hir::visit::walk_stmts(&mut collector, body), _ => {} } } for entry in &self.hir.rules { match entry { RuleEntry::Rule(rule) => { - for arg in &rule.event.args { - Self::collect_expr(arg, &mut sites); + for argument in &rule.event.args { + hir::visit::Visitor::visit_expr(&mut collector, argument); } for condition in &rule.conditions { - Self::collect_expr(condition, &mut sites); - } - for stmt in &rule.actions { - Self::collect_stmt(stmt, &mut sites); + hir::visit::Visitor::visit_expr(&mut collector, condition); } + hir::visit::walk_stmts(&mut collector, &rule.actions); } RuleEntry::SubroutineDef { body, .. } => { - for stmt in body { - Self::collect_stmt(stmt, &mut sites); - } + hir::visit::walk_stmts(&mut collector, body) } } } - for (kind, name, span) in sites { + for (kind, name, span) in collector.sites { self.attach_reference(kind, &name, span); } } /// Record a reference site for the first symbol of `kind` named `name`. - /// A call site is offered to both the `subroutine` and the `def` binding - /// kinds so both bindings of a defined subroutine collect their uses. + /// A call site is offered to both `subroutine` and `def` bindings. fn attach_reference(&mut self, kind: SymbolKind, name: &str, span: Span) { let Some(location) = resolve_span(span, &self.hir.files) else { return; @@ -558,33 +551,28 @@ impl SemanticModel { self.symbols[index].references.push(location); } } +} - fn collect_expr(expr: &HirExpr, sites: &mut Vec<(SymbolKind, String, Span)>) { - match expr { - HirExpr::Number { .. } - | HirExpr::String { .. } - | HirExpr::Bool { .. } - | HirExpr::Null { .. } - | HirExpr::Enum { .. } - | HirExpr::EventPlayer { .. } - | HirExpr::HostPlayer { .. } - | HirExpr::MacroParam { .. } - | HirExpr::StringModifier { .. } - | HirExpr::Local { .. } => {} - HirExpr::Type { args, .. } => { - for arg in args { - Self::collect_expr(arg, sites); - } +struct ReferenceSiteCollector { + sites: Vec<(SymbolKind, String, Span)>, +} + +impl hir::visit::Visitor for ReferenceSiteCollector { + fn visit_expr(&mut self, expression: &HirExpr) { + match expression { + HirExpr::GlobalVar { + name, + span: Some(span), + } => { + self.sites + .push((SymbolKind::Global, name.clone(), to_frontend_span(*span))); } - HirExpr::GlobalVar { name, span } | HirExpr::Constant { name, span } => { - let kind = if matches!(expr, HirExpr::GlobalVar { .. }) { - SymbolKind::Global - } else { - SymbolKind::Constant - }; - if let Some(span) = span { - sites.push((kind, name.clone(), to_frontend_span(*span))); - } + HirExpr::Constant { + name, + span: Some(span), + } => { + self.sites + .push((SymbolKind::Constant, name.clone(), to_frontend_span(*span))); } HirExpr::PlayerVar { name, @@ -593,190 +581,58 @@ impl SemanticModel { .. } => { if let Some(span) = member_span.as_ref().or(span.as_ref()) { - sites.push((SymbolKind::Player, name.clone(), to_frontend_span(*span))); - } - } - HirExpr::Member { receiver, .. } => Self::collect_expr(receiver, sites), - HirExpr::Array { elements, .. } => { - for element in elements { - Self::collect_expr(element, sites); + self.sites + .push((SymbolKind::Player, name.clone(), to_frontend_span(*span))); } } - HirExpr::Dict { entries, .. } => { - for entry in entries { - Self::collect_expr(&entry.key, sites); - Self::collect_expr(&entry.value, sites); - } - } - HirExpr::Comprehension { - element, - iterable, - condition, - .. - } => { - Self::collect_expr(iterable, sites); - Self::collect_expr(element, sites); - if let Some(condition) = condition { - Self::collect_expr(condition, sites); - } - } - HirExpr::Lambda { body, .. } => Self::collect_expr(body, sites), - HirExpr::Vector { x, y, z, .. } => { - Self::collect_expr(x, sites); - Self::collect_expr(y, sites); - Self::collect_expr(z, sites); - } HirExpr::Call { - name, span, args, .. + name, + span: Some(span), + .. } => { - // A call may name a declared subroutine (with arguments) or - // nothing user-declared (a builtin); unresolved names never - // reach the model. Offer both subroutine binding kinds. - if let Some(span) = span { - sites.push(( - SymbolKind::Subroutine, - name.clone(), - to_frontend_span(*span), - )); - sites.push((SymbolKind::Def, name.clone(), to_frontend_span(*span))); - } - for arg in args { - Self::collect_expr(arg, sites); - } - } - HirExpr::MacroCall { name, span, args } => { - if let Some(span) = span { - sites.push((SymbolKind::Macro, name.clone(), to_frontend_span(*span))); - } - for arg in args { - Self::collect_expr(arg, sites); - } - } - HirExpr::ReceiverCall { receiver, args, .. } => { - // The receiver may be a call (e.g. getPlayersInRadius(...).x) - // whose name binds a symbol; the call span of the outer node - // is attributed to the member name, not the receiver. - Self::collect_expr(receiver, sites); - for arg in args { - Self::collect_expr(arg, sites); - } - } - HirExpr::Binary { left, right, .. } => { - Self::collect_expr(left, sites); - Self::collect_expr(right, sites); + let span = to_frontend_span(*span); + self.sites + .push((SymbolKind::Subroutine, name.clone(), span)); + self.sites.push((SymbolKind::Def, name.clone(), span)); } - HirExpr::Conditional { - then_value, - condition, - else_value, + HirExpr::MacroCall { + name, + span: Some(span), .. } => { - Self::collect_expr(then_value, sites); - Self::collect_expr(condition, sites); - Self::collect_expr(else_value, sites); - } - HirExpr::Unary { operand, .. } => Self::collect_expr(operand, sites), - HirExpr::Index { array, index, .. } => { - Self::collect_expr(array, sites); - Self::collect_expr(index, sites); - } - HirExpr::Format { args, .. } => { - for arg in args { - Self::collect_expr(arg, sites); - } + self.sites + .push((SymbolKind::Macro, name.clone(), to_frontend_span(*span))); } + _ => {} } + hir::visit::walk_expr(self, expression); } - fn collect_stmt(stmt: &HirStmt, sites: &mut Vec<(SymbolKind, String, Span)>) { - match stmt { - HirStmt::Expr { expr, .. } => Self::collect_expr(expr, sites), - HirStmt::Assign { target, value, .. } => { - Self::collect_expr(target, sites); - Self::collect_expr(value, sites); - } - HirStmt::Delete { target, .. } => Self::collect_expr(target, sites), - HirStmt::If { - branches, r#else, .. - } => { - for branch in branches { - Self::collect_expr(&branch.condition, sites); - for stmt in &branch.body { - Self::collect_stmt(stmt, sites); - } - } - if let Some(r#else) = r#else { - for stmt in r#else { - Self::collect_stmt(stmt, sites); - } - } - } - HirStmt::For { - variable, - iterable, - body, - .. - } => { - Self::collect_expr(variable, sites); - Self::collect_expr(iterable, sites); - for stmt in body { - Self::collect_stmt(stmt, sites); - } - } - HirStmt::While { - condition, body, .. - } => { - Self::collect_expr(condition, sites); - for stmt in body { - Self::collect_stmt(stmt, sites); - } - } - HirStmt::DoWhile { - condition, body, .. - } => { - Self::collect_expr(condition, sites); - for stmt in body { - Self::collect_stmt(stmt, sites); - } - } - HirStmt::Switch { value, arms, .. } => { - Self::collect_expr(value, sites); - for arm in arms { - match arm { - hir::SwitchArm::Case { value, body, .. } => { - Self::collect_expr(value, sites); - for stmt in body { - Self::collect_stmt(stmt, sites); - } - } - hir::SwitchArm::Default { body, .. } => { - for stmt in body { - Self::collect_stmt(stmt, sites); - } - } - } - } - } - HirStmt::Break { .. } => {} - HirStmt::Return { .. } => {} - HirStmt::Continue { .. } | HirStmt::Label { .. } => {} - HirStmt::Goto { offset, .. } => { - if let Some(offset) = offset { - Self::collect_expr(offset, sites); - } - } - HirStmt::CallSubroutine { name, span } => { - if let Some(span) = span { - sites.push(( - SymbolKind::Subroutine, - name.clone(), - to_frontend_span(*span), - )); - sites.push((SymbolKind::Def, name.clone(), to_frontend_span(*span))); - } - } - HirStmt::Pass { .. } => {} + fn visit_comprehension( + &mut self, + element: &HirExpr, + iterable: &HirExpr, + condition: Option<&HirExpr>, + ) { + hir::visit::Visitor::visit_expr(self, iterable); + hir::visit::Visitor::visit_expr(self, element); + if let Some(condition) = condition { + hir::visit::Visitor::visit_expr(self, condition); + } + } + + fn visit_stmt(&mut self, statement: &HirStmt) { + if let HirStmt::CallSubroutine { + name, + span: Some(span), + } = statement + { + let span = to_frontend_span(*span); + self.sites + .push((SymbolKind::Subroutine, name.clone(), span)); + self.sites.push((SymbolKind::Def, name.clone(), span)); } + hir::visit::walk_stmt(self, statement); } } From da7b21a51d49ec64ea9e3244722711c090a7d7a2 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Sun, 27 Sep 2026 23:46:43 +0800 Subject: [PATCH 03/67] refactor(opy-rs): remove redundant lowering state --- crates/opy-rs/src/compiler/lowering.rs | 485 ++++++++--------------- crates/opy-rs/src/hir/visit.rs | 43 ++ crates/opy-rs/src/lower.rs | 74 +--- crates/opy-rs/src/lower/special_forms.rs | 17 +- crates/opy-rs/src/parser/declarations.rs | 18 +- crates/opy-rs/src/parser/definitions.rs | 13 +- 6 files changed, 227 insertions(+), 423 deletions(-) diff --git a/crates/opy-rs/src/compiler/lowering.rs b/crates/opy-rs/src/compiler/lowering.rs index 156352a8..cf19f04d 100644 --- a/crates/opy-rs/src/compiler/lowering.rs +++ b/crates/opy-rs/src/compiler/lowering.rs @@ -852,7 +852,7 @@ impl<'a> Lowering<'a> { .globals .get(COMPRESSION_ALPHABET_NAME) .expect("compression alphabet variable is created"); - let value = self.lower_custom_string(compression_alphabet(), None)?; + let value = self.lower_custom_string(compression_alphabet()); let action = self.push_action(Action::SetGlobalVariable { variable: self.global_names[variable].clone(), value, @@ -926,10 +926,7 @@ impl<'a> Lowering<'a> { .get("__languageIndex__") .expect("translation player variable is created"); let player = self.push_value(Value::EventPlayer); - let value = self.push_number( - if no_tl_err { 0.1 } else { 1.1 }, - if no_tl_err { "0.1" } else { "1.1" }, - ); + let value = self.push_number(if no_tl_err { 0.1 } else { 1.1 }); actions.push(self.push_action(Action::SetPlayerVariable { player, variable: self.player_names[variable].clone(), @@ -1024,10 +1021,7 @@ impl<'a> Lowering<'a> { player, variable: self.player_names[variable].clone(), }); - let initial = self.push_number( - if no_tl_err { 0.1 } else { 1.1 }, - if no_tl_err { "0.1" } else { "1.1" }, - ); + let initial = self.push_number(if no_tl_err { 0.1 } else { 1.1 }); let has_spawned = self.push_call("hasSpawned", vec![player]); let is_dummy = self.push_call("isDummy", vec![player]); let false_value = self.push_value(Value::Bool(false)); @@ -1041,12 +1035,12 @@ impl<'a> Lowering<'a> { op: ModifyOp::AppendToArray, value: facing, }); - let ten = self.push_number(10.0, "10"); + let ten = self.push_number(10.0); let direction_index = self.translation_language_index(translations)?; let horizontal = self.push_call("multiply", vec![ten, direction_index]); - let vertical = self.push_number(5.0, "5"); + let vertical = self.push_number(5.0); let direction = self.push_call("directionFromAngles", vec![horizontal, vertical]); - let turn_rate = self.push_number(999_999_999_999.0, "999999999999"); + let turn_rate = self.push_number(999_999_999_999.0); let to_world = self.push_value(Value::Enum { value_type: "Relativity".to_string(), value: "TO_WORLD".to_string(), @@ -1061,7 +1055,7 @@ impl<'a> Lowering<'a> { ); let horizontal_angle = self.push_call("getHorizontalFacingAngle", vec![player]); - let one_hundred = self.push_number(100.0, "100"); + let one_hundred = self.push_number(100.0); let horizontal_times_hundred = self.push_call("multiply", vec![horizontal_angle, one_hundred]); let nearest = self.push_value(Value::Enum { @@ -1070,26 +1064,26 @@ impl<'a> Lowering<'a> { }); let rounded_horizontal = self.push_call("roundToInteger", vec![horizontal_times_hundred, nearest]); - let thousand = self.push_number(1000.0, "1000"); + let thousand = self.push_number(1000.0); let modulo = self.push_call("modulo", vec![rounded_horizontal, thousand]); - let zero = self.push_number(0.0, "0"); + let zero = self.push_number(0.0); let modulo_zero = self.push_call("not", vec![modulo]); let vertical_angle = self.push_call("getVerticalFacingAngle", vec![player]); let vertical_difference = self.push_call("subtract", vec![vertical_angle, vertical]); let vertical_delta = self.push_call("absoluteValue", vec![vertical_difference]); - let tolerance = self.push_number(0.01, "0.01"); + let tolerance = self.push_number(0.01); let vertical_close = self.push_call("<", vec![vertical_delta, tolerance]); let wait_condition = self.push_call("and", vec![modulo_zero, vertical_close]); - let timeout = self.push_number(15.0, "15"); + let timeout = self.push_number(15.0); let wait = self.push_call_action("waitUntil", &[wait_condition, timeout]); - let ten_for_angle = self.push_number(10.0, "10"); + let ten_for_angle = self.push_number(10.0); let horizontal_divided = self.push_call("divide", vec![horizontal_angle, ten_for_angle]); let rounded_angle = self.push_call("roundToInteger", vec![horizontal_divided, nearest]); let vertical_difference = self.push_call("subtract", vec![vertical_angle, vertical]); let vertical_delta = self.push_call("absoluteValue", vec![vertical_difference]); let vertical_match = self.push_call("<", vec![vertical_delta, tolerance]); - let one = self.push_number(1.0, "1"); + let one = self.push_number(1.0); let matched_language = self.push_call("multiply", vec![vertical_match, rounded_angle]); let language_value = self.push_call("max", vec![one, matched_language]); let set_index = self.push_call_action( @@ -1675,7 +1669,7 @@ impl<'a> Lowering<'a> { self.push_call("not", vec![condition]) }; let distance = self.canonical_action_width(&body, *span)?; - let distance = self.push_number(distance as f64, &distance.to_string()); + let distance = self.push_number(distance as f64); let skip = self.push_call_action("skipIf", &[condition, distance]); self.mark_action_origins(std::slice::from_ref(&skip), *span); actions.push(skip); @@ -1692,7 +1686,7 @@ impl<'a> Lowering<'a> { .any(|labels| labels.iter().any(|candidate| candidate == label)) { let condition = self.lower_value(condition)?; - let placeholder = self.push_number(0.0, "0"); + let placeholder = self.push_number(0.0); let skip = self.push_call_action("skipIf", &[condition, placeholder]); self.mark_action_origins(std::slice::from_ref(&skip), span); actions.push(skip); @@ -1731,7 +1725,7 @@ impl<'a> Lowering<'a> { index += 1; continue; } - let placeholder = self.push_number(0.0, "0"); + let placeholder = self.push_number(0.0); let action = self.push_call_action("skip", &[placeholder]); self.mark_action_origins(std::slice::from_ref(&action), *span); let position = actions.len(); @@ -1771,7 +1765,7 @@ impl<'a> Lowering<'a> { .unsupported("backward goto is not representable in canonical WIR", span)); } let width = self.canonical_action_width(&actions[position + 1..target], span)?; - self.push_number(width as f64, &width.to_string()) + self.push_number(width as f64) }; let Some(Action::Call { args, .. }) = self.actions.get_mut(action) else { unreachable!("goto placeholder must be a call action") @@ -1883,7 +1877,7 @@ impl<'a> Lowering<'a> { // including the structural markers that the jump must cross. // Instructions dropped from the output must not widen the jump. let width = self.useful_actions(&actions[position + 1..target]).len(); - let distance = self.push_number(width as f64, &width.to_string()); + let distance = self.push_number(width as f64); let Some(Action::Call { args, .. }) = self.actions.get_mut(action) else { unreachable!("deferred goto placeholder must be a call action") }; @@ -2057,7 +2051,7 @@ impl<'a> Lowering<'a> { } else if name == "pass" && args.is_empty() { Ok(Vec::new()) } else if name == "debug" && args.len() == 1 { - Ok(vec![self.lower_debug(&args[0], *span, debug_source.as_deref())?]) + Ok(vec![self.lower_debug(&args[0], debug_source.as_deref())?]) } else if name == "print" && args.len() == 1 { Ok(vec![self.lower_print(&args[0], *span)?]) } else if name == "createCasedProgressBarIwt" { @@ -2270,21 +2264,7 @@ impl<'a> Lowering<'a> { } fn lower_loop_body(&mut self, statements: &[Stmt]) -> Result, IntegrationError> { - self.lower_loop_sequence(statements, &[], 0) - } - - fn lower_loop_sequence( - &mut self, - statements: &[Stmt], - after: &[ActionId], - structural_after: usize, - ) -> Result, IntegrationError> { - self.lower_loop_sequence_with_break_target( - statements, - after, - structural_after, - BreakTarget::Loop, - ) + self.lower_loop_sequence_with_break_target(statements, &[], 0, BreakTarget::Loop) } fn lower_loop_sequence_with_break_target( @@ -2319,7 +2299,7 @@ impl<'a> Lowering<'a> { } args.push(condition); } - let distance = self.push_number(distance as f64, &distance.to_string()); + let distance = self.push_number(distance as f64); args.push(distance); let skip = self.push_call_action( if conditions.is_empty() { @@ -2400,7 +2380,7 @@ impl<'a> Lowering<'a> { } args.push(condition); } - args.push(self.push_number(distance as f64, &distance.to_string())); + args.push(self.push_number(distance as f64)); let skip = self.push_call_action( if conditions.is_empty() { "skip" @@ -2426,7 +2406,7 @@ impl<'a> Lowering<'a> { .any(|labels| labels.iter().any(|candidate| candidate == label)) { let condition = self.lower_value(condition)?; - let placeholder = self.push_number(0.0, "0"); + let placeholder = self.push_number(0.0); let skip = self.push_call_action("skipIf", &[condition, placeholder]); self.mark_action_origins(std::slice::from_ref(&skip), span); actions.push(skip); @@ -2583,7 +2563,7 @@ impl<'a> Lowering<'a> { } else { unreachable!("break shape was checked above") }; - let distance = self.push_number(distance as f64, &distance.to_string()); + let distance = self.push_number(distance as f64); let mut args = args; args.push(distance); let skip = self.push_call_action(name, &args); @@ -2614,21 +2594,17 @@ impl<'a> Lowering<'a> { iterable.span().copied(), )); } - let span = iterable.span().copied(); - let number = |this: &mut Self, value: f64| -> Result { - let _ = span; - Ok(this.push_number(value, &value.to_string())) - }; + let number = |this: &mut Self, value: f64| this.push_number(value); match args.as_slice() { [stop] => Ok(( - number(self, 0.0)?, + number(self, 0.0), self.lower_value(stop)?, - number(self, 1.0)?, + number(self, 1.0), )), [start, stop] => Ok(( self.lower_value(start)?, self.lower_value(stop)?, - number(self, 1.0)?, + number(self, 1.0), )), [start, stop, step] => Ok(( self.lower_value(start)?, @@ -2740,16 +2716,15 @@ impl<'a> Lowering<'a> { }); let use_shared_exit = break_arms.len() > 1 && has_later_reachable_actions; - let case_values = self.lower_array(case_values, span)?; - let _ = span; + let case_values = self.lower_array(case_values); if !use_shared_exit { let default_offset = legacy_default_offset.unwrap_or(legacy_offset); let offset_values = std::iter::once(default_offset) .chain(legacy_case_offsets) - .map(|value| self.push_number(value as f64, &value.to_string())) + .map(|value| self.push_number(value as f64)) .collect(); - let offsets = self.lower_array(offset_values, span)?; - let skip = self.lower_switch_selector(selector, case_values, offsets, span)?; + let offsets = self.lower_array(offset_values); + let skip = self.lower_switch_selector(selector, case_values, offsets); let true_value = self.push_value(Value::Bool(true)); let mut branch_body = vec![skip]; let else_body = if let Some((break_index, (break_at, _))) = first_break { @@ -2777,7 +2752,7 @@ impl<'a> Lowering<'a> { } let offsets = self.push_value(Value::Array(Vec::new())); - let skip = self.lower_switch_selector(selector, case_values, offsets, span)?; + let skip = self.lower_switch_selector(selector, case_values, offsets); let mut arm_offsets = vec![None; lowered_arms.len()]; let (switch, switch_end) = self.lower_switch_level(&lowered_arms, 0, Some(skip), 0, &mut arm_offsets, span)?; @@ -2795,9 +2770,9 @@ impl<'a> Lowering<'a> { .filter(|(_, (value, _, _))| value.is_some()) .map(|(index, _)| arm_offsets[index].unwrap()), ) - .map(|value| self.push_number(value as f64, &value.to_string())) + .map(|value| self.push_number(value as f64)) .collect(); - let offset_values = self.lower_array(offset_values, span)?; + let offset_values = self.lower_array(offset_values); let offset_value = self.value(offset_values).clone(); let Some(node) = self.values.get_mut(offsets) else { unreachable!("switch offset placeholder must exist") @@ -2823,13 +2798,12 @@ impl<'a> Lowering<'a> { selector: ValueId, case_values: ValueId, offsets: ValueId, - _span: Option, - ) -> Result { - let one = self.push_number(1.0, "1"); + ) -> ActionId { + let one = self.push_number(1.0); let index = self.push_call("indexOfArrayValue", vec![case_values, selector]); let case_offset = self.push_call("add", vec![one, index]); let skip_condition = self.push_call("valueInArray", vec![offsets, case_offset]); - Ok(self.push_call_action("skip", &[skip_condition])) + self.push_call_action("skip", &[skip_condition]) } fn lower_switch_level( @@ -2984,22 +2958,17 @@ impl<'a> Lowering<'a> { }) } - fn lower_array( - &mut self, - elements: Vec, - span: Option, - ) -> Result { + fn lower_array(&mut self, elements: Vec) -> ValueId { let name = if elements.is_empty() { "emptyArray" } else { "array" }; let elements = self.normalize_contextual_arguments(name, elements); - let _ = span; - Ok(self.push_value(Value::Call { + self.push_value(Value::Call { name: name.to_string(), args: self.value_args(&elements), - })) + }) } fn lower_translation_helper( @@ -3146,8 +3115,7 @@ impl<'a> Lowering<'a> { let raw_string = format!("{tl_err_prefix}{}", localized.join("\u{ec48}")); let replacement_mode = raw_string.chars().count() > 128 || format_args.len() > 3; if replacement_mode { - for (index, replacement) in format_args.iter().enumerate() { - let _ = replacement; + for index in 0..format_args.len() { let marker = format_number_marker(index); for value in &mut localized { *value = value.replace(&format!("{{{index}}}"), &marker); @@ -3178,10 +3146,7 @@ impl<'a> Lowering<'a> { let custom = if replacement_mode { let mut value = self.push_call("customString", vec![text]); for (index, arg) in format_args.iter().enumerate() { - let marker = self.push_number( - format_number_marker_value(index), - &format_number_marker(index), - ); + let marker = self.push_number(format_number_marker_value(index)); let marker = self.push_call("updateEveryFrame", vec![marker]); let replacement = self.lower_value(arg)?; value = self.push_call("stringReplace", vec![value, marker, replacement]); @@ -3320,7 +3285,6 @@ impl<'a> Lowering<'a> { fn lower_debug( &mut self, expr: &Expr, - _span: Option, debug_source: Option<&str>, ) -> Result { let argument_span = expr.span().copied(); @@ -3352,7 +3316,7 @@ impl<'a> Lowering<'a> { value_type: "HudPosition".to_string(), value: "LEFT".to_string(), }); - let sort_order = self.push_number(-9999.0, "-9999"); + let sort_order = self.push_number(-9999.0); let color = self.push_value(Value::Enum { value_type: "Color".to_string(), value: "WHITE".to_string(), @@ -3425,7 +3389,7 @@ impl<'a> Lowering<'a> { value_type: "HudPosition".to_string(), value: "LEFT".to_string(), }); - let sort_order = self.push_number(-9999.0, "-9999"); + let sort_order = self.push_number(-9999.0); let color = if empty_string { self.push_value(Value::Null) } else { @@ -3490,7 +3454,7 @@ impl<'a> Lowering<'a> { let is_single = call!( "==", call!("countOf", call!("currentArrayElement")), - self.push_number(1.0, "1") + self.push_number(1.0) ); let is_empty = call!("==", call!("currentArrayElement"), call!("emptyArray")); let not_null = call!( @@ -3512,7 +3476,7 @@ impl<'a> Lowering<'a> { call!( "subtract", call!("countOf", call!("currentArrayElement")), - self.push_number(1.0, "1"), + self.push_number(1.0), ), ); let element_text = call!( @@ -3549,11 +3513,11 @@ impl<'a> Lowering<'a> { call!("not", call!("countOf", call!("currentArrayElement"))), call!("!=", call!("currentArrayElement"), call!("emptyArray"),), ), - self.push_number(3.0, "3"), + self.push_number(3.0), call!( "multiply", call!("countOf", call!("currentArrayElement")), - self.push_number(3.0, "3"), + self.push_number(3.0), ), ); let x = call!( @@ -3564,12 +3528,12 @@ impl<'a> Lowering<'a> { let x_input = call!("mappedArray", mapped_input, x); let x_length = |this: &mut Self| { let current = this.push_call("currentArrayElement", Vec::new()); - let index = this.push_number(1.0, "1"); + let index = this.push_number(1.0); this.push_call("valueInArray", vec![current, index]) }; let x_value = |this: &mut Self, index: f64| { let current = this.push_call("currentArrayElement", Vec::new()); - let index_value = this.push_number(index, &index.to_string()); + let index_value = this.push_number(index); this.push_call("valueInArray", vec![current, index_value]) }; let first = call!("firstOf", call!("currentArrayElement")); @@ -3629,9 +3593,8 @@ impl<'a> Lowering<'a> { let end_length_for_slice = x_length(self); let start = self.push_number( (placeholder_text.chars().count() as isize - 4 - 3 * max_length as isize) as f64, - "", ); - let end = self.push_number((max_length * 3 + 4) as f64, ""); + let end = self.push_number((max_length * 3 + 4) as f64); let slice = call!( "stringSlice", placeholder, @@ -3646,15 +3609,15 @@ impl<'a> Lowering<'a> { call!( ">", length_for_compare, - self.push_number((max_length * 3) as f64, ""), + self.push_number((max_length * 3) as f64), ), call!( "customString", self.push_value(Value::String("+{0}".to_string())), call!( "subtract", - call!("divide", length_for_divide, self.push_number(3.0, "3")), - self.push_number(max_length as f64, ""), + call!("divide", length_for_divide, self.push_number(3.0)), + self.push_number(max_length as f64), ), ), call!("emptyArray"), @@ -3672,11 +3635,7 @@ impl<'a> Lowering<'a> { formatted_array, call!( "stringSplit", - call!( - "valueInArray", - current_for_split, - self.push_number(2.0, "2") - ), + call!("valueInArray", current_for_split, self.push_number(2.0)), call!("emptyArray"), ), ); @@ -3783,14 +3742,9 @@ impl<'a> Lowering<'a> { args } - fn lower_custom_string( - &mut self, - value: String, - span: Option, - ) -> Result { - let _ = span; + fn lower_custom_string(&mut self, value: String) -> ValueId { let text = self.push_value(Value::String(value)); - Ok(self.push_call("customString", vec![text])) + self.push_call("customString", vec![text]) } fn fold_format_constants<'b>( @@ -3856,8 +3810,7 @@ impl<'a> Lowering<'a> { (output, dynamic_args) } - fn push_number(&mut self, value: f64, text: &str) -> ValueId { - let _ = text; + fn push_number(&mut self, value: f64) -> ValueId { self.push_value(Value::Number(value)) } @@ -3920,12 +3873,7 @@ impl<'a> Lowering<'a> { return Err(self.unsupported("Cannot delete index of 4d array", span)); } indices.reverse(); - if indices.len() >= 3 - && (expr_contains_random(root) - || indices[..indices.len() - 1] - .iter() - .any(|index| expr_contains_random(index))) - { + if hir::visit::has_random_nested_delete(root, &indices) { return Err(self.unsupported( "Cannot delete from nested array with a random outer or middle index", span, @@ -4005,7 +3953,7 @@ impl<'a> Lowering<'a> { let outer_array = self.lower_indexed_read(root_value, indices[0], index)?; if indices.len() == 4 { - let replacement = self.rebuild_deleted_array(outer_array, &indices[1..], span)?; + let replacement = self.rebuild_deleted_array(outer_array, &indices[1..])?; let action_name = if action_name == "modifyGlobalVariableAtIndex" { "setGlobalVariableAtIndex" } else { @@ -4021,21 +3969,21 @@ impl<'a> Lowering<'a> { let current_index = self.push_call("currentArrayIndex", Vec::new()); let condition = self.push_call("!=", vec![current_index, leaf_index]); let filtered = self.push_call("filteredArray", vec![row, condition]); - let replacement = if let Some(number) = literal_number(indices[1]) { - let middle = self.lower_array(vec![filtered], span)?; - let maximum = self.push_number(999_999_999_999.0, "999999999999"); - let suffix_start = self.push_number(number + 1.0, &(number + 1.0).to_string()); + let replacement = if let Some(number) = hir::visit::literal_number(indices[1]) { + let middle = self.lower_array(vec![filtered]); + let maximum = self.push_number(999_999_999_999.0); + let suffix_start = self.push_number(number + 1.0); let suffix = self.push_call("slice", vec![outer_array, suffix_start, maximum]); if number == 0.0 { self.push_call("appendToArray", vec![middle, suffix]) } else { - let zero = self.push_number(0.0, "0"); + let zero = self.push_number(0.0); let prefix = self.push_call("slice", vec![outer_array, zero, inner_index]); let with_replacement = self.push_call("appendToArray", vec![prefix, middle]); self.push_call("appendToArray", vec![with_replacement, suffix]) } } else { - self.replace_array_element(outer_array, inner_index, filtered, span)? + self.replace_array_element(outer_array, inner_index, filtered) }; let action_name = if action_name == "modifyGlobalVariableAtIndex" { "setGlobalVariableAtIndex" @@ -4060,7 +4008,7 @@ impl<'a> Lowering<'a> { } if indices.len() > 1 { indices.reverse(); - return self.lower_nested_indexed_assign(root, &indices, target, value, span); + return self.lower_nested_indexed_assign(root, &indices, target, value); } } match target { @@ -4225,7 +4173,6 @@ impl<'a> Lowering<'a> { indices: &[&Expr], target: &Expr, value: &Expr, - span: Option, ) -> Result { let (action_name, root_value) = match root { Expr::GlobalVar { @@ -4265,8 +4212,7 @@ impl<'a> Lowering<'a> { let outer_index = self.lower_value(indices[0])?; let outer_array = self.lower_indexed_read(root_value, indices[0], outer_index)?; - let replacement = - self.rebuild_indexed_value(outer_array, &indices[1..], target, value, span)?; + let replacement = self.rebuild_indexed_value(outer_array, &indices[1..], target, value)?; let args = self.normalize_contextual_arguments( action_name, vec![root_value, outer_index, replacement], @@ -4280,7 +4226,6 @@ impl<'a> Lowering<'a> { indices: &[&Expr], target: &Expr, value: &Expr, - span: Option, ) -> Result { let index = indices .first() @@ -4302,9 +4247,9 @@ impl<'a> Lowering<'a> { } } else { let child = self.lower_indexed_read(array, index, index_value)?; - self.rebuild_indexed_value(child, &indices[1..], target, value, span)? + self.rebuild_indexed_value(child, &indices[1..], target, value)? }; - self.replace_array_element(array, index_value, replacement, span) + Ok(self.replace_array_element(array, index_value, replacement)) } fn lower_indexed_read( @@ -4325,24 +4270,22 @@ impl<'a> Lowering<'a> { array: ValueId, index: ValueId, replacement: ValueId, - span: Option, - ) -> Result { - let zero = self.push_number(0.0, "0"); - let one = self.push_number(1.0, "1"); + ) -> ValueId { + let zero = self.push_number(0.0); + let one = self.push_number(1.0); let end = self.push_call("add", vec![index, one]); - let maximum = self.push_number(999_999_999_999.0, "999999999999"); + let maximum = self.push_number(999_999_999_999.0); let prefix = self.push_call("slice", vec![array, zero, index]); - let middle = self.lower_array(vec![replacement], span)?; + let middle = self.lower_array(vec![replacement]); let suffix = self.push_call("slice", vec![array, end, maximum]); let with_replacement = self.push_call("appendToArray", vec![prefix, middle]); - Ok(self.push_call("appendToArray", vec![with_replacement, suffix])) + self.push_call("appendToArray", vec![with_replacement, suffix]) } fn rebuild_deleted_array( &mut self, array: ValueId, indices: &[&Expr], - span: Option, ) -> Result { let index = self.lower_value(indices[0])?; if indices.len() == 1 { @@ -4351,8 +4294,8 @@ impl<'a> Lowering<'a> { return Ok(self.push_call("filteredArray", vec![array, condition])); } let child = self.lower_indexed_read(array, indices[0], index)?; - let replacement = self.rebuild_deleted_array(child, &indices[1..], span)?; - self.replace_array_element_for_delete(array, indices[0], index, replacement, span) + let replacement = self.rebuild_deleted_array(child, &indices[1..])?; + Ok(self.replace_array_element_for_delete(array, indices[0], index, replacement)) } fn replace_array_element_for_delete( @@ -4361,22 +4304,21 @@ impl<'a> Lowering<'a> { index_expr: &Expr, index: ValueId, replacement: ValueId, - span: Option, - ) -> Result { - if let Some(number) = literal_number(index_expr) { - let middle = self.lower_array(vec![replacement], span)?; - let maximum = self.push_number(999_999_999_999.0, "999999999999"); - let suffix_start = self.push_number(number + 1.0, &(number + 1.0).to_string()); + ) -> ValueId { + if let Some(number) = hir::visit::literal_number(index_expr) { + let middle = self.lower_array(vec![replacement]); + let maximum = self.push_number(999_999_999_999.0); + let suffix_start = self.push_number(number + 1.0); let suffix = self.push_call("slice", vec![array, suffix_start, maximum]); if number == 0.0 { - return Ok(self.push_call("appendToArray", vec![middle, suffix])); + return self.push_call("appendToArray", vec![middle, suffix]); } - let zero = self.push_number(0.0, "0"); + let zero = self.push_number(0.0); let prefix = self.push_call("slice", vec![array, zero, index]); let with_replacement = self.push_call("appendToArray", vec![prefix, middle]); - return Ok(self.push_call("appendToArray", vec![with_replacement, suffix])); + return self.push_call("appendToArray", vec![with_replacement, suffix]); } - self.replace_array_element(array, index, replacement, span) + self.replace_array_element(array, index, replacement) } fn lower_cased_progress_bar( @@ -4414,7 +4356,7 @@ impl<'a> Lowering<'a> { )); } let text_count = text_count_value as usize; - if args.iter().any(expr_contains_random) { + if args.iter().any(hir::visit::contains_random) { return Err(self.unsupported( "Cannot use random functions in createCasedProgressBarIwt", span, @@ -4452,8 +4394,8 @@ impl<'a> Lowering<'a> { .unwrap_or_else(|| vec![String::new(); text_count]); let mut actions = Vec::with_capacity(text_count); for (index, text) in texts.into_iter().enumerate() { - let value = self.push_number(index as f64, &index.to_string()); - let text = self.lower_custom_string(text, span)?; + let value = self.push_number(index as f64); + let text = self.lower_custom_string(text); let values = self.normalize_contextual_arguments( "createProgressBarInWorldText", vec![ @@ -4564,7 +4506,7 @@ impl<'a> Lowering<'a> { let mut lowered = self.lower_values(args)?; let mut zero_vector = Vec::with_capacity(3); for value in [0.0, 0.0, 0.0] { - zero_vector.push(self.push_number(value, "0")); + zero_vector.push(self.push_number(value)); } lowered.push(self.push_call("vector", zero_vector)); let args = self.normalize_contextual_arguments("createDummyBot", lowered); @@ -4934,10 +4876,10 @@ impl<'a> Lowering<'a> { { match value { crate::compile_time::Value::Number(value) if value.is_finite() => { - return Ok(self.push_number(value, &computed_number_text(value))); + return Ok(self.push_number(value)); } crate::compile_time::Value::String(value) => { - return self.lower_custom_string(value, span); + return Ok(self.lower_custom_string(value)); } crate::compile_time::Value::Bool(value) => { return Ok(self.push_value(Value::Bool(value))); @@ -4951,7 +4893,7 @@ impl<'a> Lowering<'a> { let value = match expr { Expr::Number { value, .. } => Value::Number(*value), Expr::String { value, .. } => { - return self.lower_custom_string(value.clone(), span); + return Ok(self.lower_custom_string(value.clone())); } Expr::Bool { value, .. } => Value::Bool(*value), Expr::Null { .. } => Value::Null, @@ -5034,7 +4976,7 @@ impl<'a> Lowering<'a> { } Expr::Array { elements, .. } => { let elements = self.lower_values(elements)?; - return self.lower_array(elements, span); + return Ok(self.lower_array(elements)); } Expr::Vector { x, y, z, .. } => { let x = self.lower_value(x)?; @@ -5095,7 +5037,7 @@ impl<'a> Lowering<'a> { let (format_text, dynamic_args) = self.fold_format_constants(text, args); if dynamic_args.is_empty() { let value = format_text; - return self.lower_custom_string(value, span); + return Ok(self.lower_custom_string(value)); } if dynamic_args.len() <= 3 { let text_node = self.push_value(Value::String(format_text)); @@ -5315,7 +5257,7 @@ impl<'a> Lowering<'a> { name.as_str(), "getRealFarthestPlayer" | "getRealFarthestPlayers" ) { - let negative_one = self.push_number(-1.0, "-1"); + let negative_one = self.push_number(-1.0); self.push_call("multiply", vec![negative_one, distance]) } else { distance @@ -5383,7 +5325,7 @@ impl<'a> Lowering<'a> { let [text] = args.as_slice() else { return Err(self.unsupported(format!("{name} requires one string"), span)); }; - return self.lower_decompression(text, name == "decompressVectors", span); + return self.lower_decompression(text, name == "decompressVectors"); } if name == "strVisualLength" { let [Expr::String { value, .. }] = args.as_slice() else { @@ -5392,7 +5334,7 @@ impl<'a> Lowering<'a> { ); }; let width = value.chars().map(blizzard_global::width).sum::(); - return Ok(self.push_number(width as f64, "")); + return Ok(self.push_number(width as f64)); } if name == "spacesForLength" { let [Expr::Number { value, .. }] = args.as_slice() else { @@ -5406,7 +5348,7 @@ impl<'a> Lowering<'a> { span, )); } - return self.lower_custom_string(blizzard_global::spaces(*value as i32), span); + return Ok(self.lower_custom_string(blizzard_global::spaces(*value as i32))); } if name == "spacesForString" { let [Expr::String { value, .. }] = args.as_slice() else { @@ -5444,10 +5386,9 @@ impl<'a> Lowering<'a> { self.unsupported("spacesForString requires one literal string", span) ); }; - return self.lower_custom_string( - blizzard_global::spaces(value.chars().map(blizzard_global::width).sum()), - span, - ); + return Ok(self.lower_custom_string(blizzard_global::spaces( + value.chars().map(blizzard_global::width).sum(), + ))); } if name == "hsl" { let (hue, saturation, lightness, alpha) = match args.as_slice() { @@ -5466,32 +5407,32 @@ impl<'a> Lowering<'a> { let lightness = self.lower_value(lightness)?; let alpha = match alpha { Some(alpha) => self.lower_value(alpha)?, - None => self.push_number(255.0, "255"), + None => self.push_number(255.0), }; - let one = self.push_number(1.0, "1"); - let thirty = self.push_number(30.0, "30"); + let one = self.push_number(1.0); + let thirty = self.push_number(30.0); let hue_thirtieths = self.push_call("divide", vec![hue, thirty]); let lightness_complement = self.push_call("subtract", vec![one, lightness]); let lightness_limit = self.push_call("min", vec![lightness, lightness_complement]); let channel = |this: &mut Self, offset: f64| { - let offset = this.push_number(offset, ""); + let offset = this.push_number(offset); let phase = this.push_call("add", vec![offset, hue_thirtieths]); - let twelve = this.push_number(12.0, "12"); + let twelve = this.push_number(12.0); let phase = this.push_call("modulo", vec![phase, twelve]); - let three = this.push_number(3.0, "3"); + let three = this.push_number(3.0); let lower = this.push_call("subtract", vec![phase, three]); - let nine = this.push_number(9.0, "9"); + let nine = this.push_number(9.0); let upper = this.push_call("subtract", vec![nine, phase]); let clamped = this.push_call("min", vec![lower, upper]); - let negative_one = this.push_number(-1.0, "-1"); + let negative_one = this.push_number(-1.0); let clamped = this.push_call("max", vec![clamped, negative_one]); let saturation_limit = this.push_call("multiply", vec![saturation, lightness_limit]); let adjustment = this.push_call("multiply", vec![saturation_limit, clamped]); let value = this.push_call("subtract", vec![lightness, adjustment]); - let scale = this.push_number(255.0, "255"); + let scale = this.push_number(255.0); this.push_call("multiply", vec![scale, value]) }; let red = channel(self, 0.0); @@ -5504,8 +5445,8 @@ impl<'a> Lowering<'a> { return Err(self.unsupported("timeToString requires one argument", span)); }; let time = self.lower_value(time)?; - let three_thousand_six_hundred = self.push_number(3600.0, "3600"); - let sixty = self.push_number(60.0, "60"); + let three_thousand_six_hundred = self.push_number(3600.0); + let sixty = self.push_number(60.0); let hour_value = self.push_call("divide", vec![time, three_thousand_six_hundred]); let down = self.push_value(Value::Enum { @@ -5518,9 +5459,9 @@ impl<'a> Lowering<'a> { let minute_value = self.push_call("divide", vec![minute_remainder, sixty]); let minute = self.push_call("roundToInteger", vec![minute_value, down]); let second = self.push_call("modulo", vec![time, sixty]); - let hundred = self.push_number(100.0, "100"); - let first_digit = self.push_number(1.0, "1"); - let two = self.push_number(2.0, "2"); + let hundred = self.push_number(100.0); + let first_digit = self.push_number(1.0); + let two = self.push_number(2.0); let minute_with_padding = self.push_call("add", vec![minute, hundred]); let padding_template = self.push_value(Value::String("{0}".to_string())); let minute_with_padding = @@ -5530,7 +5471,7 @@ impl<'a> Lowering<'a> { let second_with_padding = self.push_call("add", vec![second, hundred]); let second_with_padding = self.push_call("customString", vec![padding_template, second_with_padding]); - let all_digits = self.push_number(9999.0, "9999"); + let all_digits = self.push_number(9999.0); let second_text = self.push_call( "stringSlice", vec![second_with_padding, first_digit, all_digits], @@ -5552,10 +5493,10 @@ impl<'a> Lowering<'a> { return Err(self.unsupported("getSign requires one argument", span)); }; let number = self.lower_value(number)?; - let zero = self.push_number(0.0, "0"); + let zero = self.push_number(0.0); let positive = self.push_call(">", vec![number, zero]); - let one = self.push_number(1.0, "1"); - let negative_one = self.push_number(-1.0, "-1"); + let one = self.push_number(1.0); + let negative_one = self.push_number(-1.0); let sign = self.push_call("ifThenElse", vec![positive, one, negative_one]); let is_zero = self.push_call("==", vec![number, zero]); return Ok(self.push_call("ifThenElse", vec![is_zero, zero, sign])); @@ -5567,7 +5508,7 @@ impl<'a> Lowering<'a> { let start = self.lower_value(start)?; let end = self.lower_value(end)?; let t = self.lower_value(t)?; - let one = self.push_number(1.0, "1"); + let one = self.push_number(1.0); let weight = self.push_call("subtract", vec![one, t]); let start_part = self.push_call("multiply", vec![start, weight]); let end_part = self.push_call("multiply", vec![end, t]); @@ -5582,18 +5523,18 @@ impl<'a> Lowering<'a> { } }; let number = self.lower_value(number)?; - let exponent = self.push_number(0.0001, "0.0001"); + let exponent = self.push_number(0.0001); let powered = self.push_call("raiseToPower", vec![number, exponent]); - let one = self.push_number(1.0, "1"); + let one = self.push_number(1.0); let delta = self.push_call("subtract", vec![powered, one]); - let scale = self.push_number(10000.0, "10000"); + let scale = self.push_number(10000.0); let approximation = self.push_call("multiply", vec![scale, delta]); if let Some(base) = base { let base = self.lower_value(base)?; let base_powered = self.push_call("raiseToPower", vec![base, exponent]); - let base_one = self.push_number(1.0, "1"); + let base_one = self.push_number(1.0); let base_delta = self.push_call("subtract", vec![base_powered, base_one]); - let base_scale = self.push_number(10000.0, "10000"); + let base_scale = self.push_number(10000.0); let base_log = self.push_call("multiply", vec![base_scale, base_delta]); return Ok(self.push_call("divide", vec![approximation, base_log])); } @@ -5650,7 +5591,7 @@ impl<'a> Lowering<'a> { // The sort order is the last parameter; OverPy writes 0 when omitted. let (canonical, arity_without_sort_order) = workshop_setting_call(name); if lowered.len() == arity_without_sort_order { - lowered.push(self.push_number(0.0, "0")); + lowered.push(self.push_number(0.0)); } Value::Call { name: canonical.to_string(), @@ -6131,7 +6072,7 @@ impl<'a> Lowering<'a> { )); } }; - return self.lower_custom_string(value, *span); + return Ok(self.lower_custom_string(value)); } _ => { return Err(self.unsupported( @@ -6227,8 +6168,8 @@ impl<'a> Lowering<'a> { args: &mut [ValueId], span: Option, ) { - for (index, value) in args.iter_mut().enumerate() { - *value = self.apply_replacement(*value, call_id, index, span); + for value in args { + *value = self.apply_replacement(*value, call_id, span); } } @@ -6236,7 +6177,6 @@ impl<'a> Lowering<'a> { &mut self, value_id: ValueId, call_id: &str, - _arg_index: usize, span: Option, ) -> ValueId { let optimization = self.optimization_state_at(span.as_ref()); @@ -6322,7 +6262,6 @@ impl<'a> Lowering<'a> { &mut self, text: &Expr, is_vector: bool, - span: Option, ) -> Result { let text = self.lower_value(text)?; let null = self.push_value(Value::Null); @@ -6335,7 +6274,7 @@ impl<'a> Lowering<'a> { .expect("compression alphabet variable is created"); self.push_value(Value::GlobalVariable(self.global_names[variable].clone())) } else { - self.lower_custom_string(compression_alphabet(), span)? + self.lower_custom_string(compression_alphabet()) }; let decoded = if has_directive(self.hir, "useVariableForCompressionAlphabet") { split @@ -6351,7 +6290,7 @@ impl<'a> Lowering<'a> { let current = this.push_call("currentArrayElement", Vec::new()); let mut terms = Vec::with_capacity(width); for index in 0..width { - let position = this.push_number((index + component_offset) as f64, ""); + let position = this.push_number((index + component_offset) as f64); let character = this.push_call("charAt", vec![current, position]); let formula_alphabet = if has_directive(this.hir, "useVariableForCompressionAlphabet") { @@ -6361,17 +6300,17 @@ impl<'a> Lowering<'a> { }; let digit = this.push_call("strIndex", vec![formula_alphabet, character]); let power = 100_f64.powf(index as f64 + min_decimal_place / 2.0); - let power = this.push_number(power, ""); + let power = this.push_number(power); terms.push(this.push_call("multiply", vec![power, digit])); } let mut value = terms .first() .copied() - .unwrap_or_else(|| this.push_number(0.0, "")); + .unwrap_or_else(|| this.push_number(0.0)); for term in terms.into_iter().skip(1) { value = this.push_call("add", vec![value, term]); } - let offset = this.push_number(offset, ""); + let offset = this.push_number(offset); this.push_call("subtract", vec![value, offset]) }; if is_vector { @@ -6408,13 +6347,13 @@ impl<'a> Lowering<'a> { Expr::Null { .. } => Some(vec![0.0]), Expr::Number { value, .. } => Some(vec![*value]), Expr::Unary { op, operand, .. } if matches!(op.as_str(), "+" | "-") => { - literal_number(operand) + hir::visit::literal_number(operand) .map(|value| vec![if op == "-" { -value } else { value }]) } Expr::Vector { x, y, z, .. } => Some(vec![ - literal_number(x)?, - literal_number(y)?, - literal_number(z)?, + hir::visit::literal_number(x)?, + hir::visit::literal_number(y)?, + hir::visit::literal_number(z)?, ]), _ => None, }) @@ -6527,9 +6466,9 @@ impl<'a> Lowering<'a> { .join("0") }; if !decode { - return self.lower_custom_string(compressed, span); + return Ok(self.lower_custom_string(compressed)); } - let compressed_string = self.lower_custom_string(compressed, span)?; + let compressed_string = self.lower_custom_string(compressed); let null = self.push_value(Value::Null); let separator = self.push_call("firstOf", vec![null]); let split = self.push_call("stringSplit", vec![compressed_string, separator]); @@ -6540,7 +6479,7 @@ impl<'a> Lowering<'a> { .expect("compression alphabet variable is created"); self.push_value(Value::GlobalVariable(self.global_names[variable].clone())) } else { - self.lower_custom_string(compression_alphabet(), span)? + self.lower_custom_string(compression_alphabet()) }; let decoded = if has_directive(self.hir, "useVariableForCompressionAlphabet") { split @@ -6555,7 +6494,7 @@ impl<'a> Lowering<'a> { let current = this.push_call("currentArrayElement", Vec::new()); let mut terms = Vec::with_capacity(width); for index in 0..width { - let position = this.push_number((index + component_offset) as f64, ""); + let position = this.push_number((index + component_offset) as f64); let character = this.push_call("charAt", vec![current, position]); let formula_alphabet = if has_directive(this.hir, "useVariableForCompressionAlphabet") { @@ -6565,7 +6504,7 @@ impl<'a> Lowering<'a> { }; let digit = this.push_call("strIndex", vec![formula_alphabet, character]); let power = 100_f64.powf(index as f64 + f64::from(min_decimal_place) / 2.0); - let power = this.push_number(power, ""); + let power = this.push_number(power); let weighted = this.push_call("multiply", vec![power, digit]); if optimization.enabled { this.optimized_nodes.insert(weighted, optimization.strict); @@ -6575,14 +6514,14 @@ impl<'a> Lowering<'a> { let mut value = terms .first() .copied() - .unwrap_or_else(|| this.push_number(0.0, "")); + .unwrap_or_else(|| this.push_number(0.0)); for term in terms.into_iter().skip(1) { value = this.push_call("add", vec![value, term]); } if is_vector || compression_offset == 0.0 { value } else { - let offset = this.push_number(compression_offset, ""); + let offset = this.push_number(compression_offset); this.push_call("add", vec![value, offset]) } }; @@ -6594,7 +6533,7 @@ impl<'a> Lowering<'a> { let value = if compression_offset == 0.0 { vector } else { - let offset = self.push_number(-compression_offset, ""); + let offset = self.push_number(-compression_offset); let offset = self.push_call("vector", vec![offset, offset, offset]); self.push_call("subtract", vec![vector, offset]) }; @@ -6828,10 +6767,7 @@ impl<'a> Lowering<'a> { } fn push_while_actions(&mut self, condition: ValueId, body: Vec) -> Vec { - let mut result = vec![self.push_action(Action::While { condition })]; - result.extend(body); - result.push(self.push_action(Action::End)); - result + self.push_loop_actions(Action::While { condition }, body) } fn push_for_global_actions( @@ -6842,15 +6778,15 @@ impl<'a> Lowering<'a> { step: ValueId, body: Vec, ) -> Vec { - let mut result = vec![self.push_action(Action::ForGlobalVariable { - variable: self.global_names[variable].clone(), - start, - stop, - step, - })]; - result.extend(body); - result.push(self.push_action(Action::End)); - result + self.push_loop_actions( + Action::ForGlobalVariable { + variable: self.global_names[variable].clone(), + start, + stop, + step, + }, + body, + ) } fn push_for_player_actions( @@ -6862,13 +6798,20 @@ impl<'a> Lowering<'a> { step: ValueId, body: Vec, ) -> Vec { - let mut result = vec![self.push_action(Action::ForPlayerVariable { - player, - variable: self.player_names[variable].clone(), - start, - stop, - step, - })]; + self.push_loop_actions( + Action::ForPlayerVariable { + player, + variable: self.player_names[variable].clone(), + start, + stop, + step, + }, + body, + ) + } + + fn push_loop_actions(&mut self, start: Action, body: Vec) -> Vec { + let mut result = vec![self.push_action(start)]; result.extend(body); result.push(self.push_action(Action::End)); result @@ -7408,84 +7351,6 @@ fn directive_value<'a>(hir: &'a hir::Program, name: &str) -> Option<&'a str> { .and_then(|directive| directive.value.as_deref()) } -fn literal_number(expr: &hir::Expr) -> Option { - match expr { - hir::Expr::Null { .. } => Some(0.0), - hir::Expr::Number { value, .. } => Some(*value), - hir::Expr::Unary { op, operand, .. } if op == "+" => literal_number(operand), - hir::Expr::Unary { op, operand, .. } if op == "-" => { - literal_number(operand).map(|value| -value) - } - _ => None, - } -} - -fn expr_contains_random(expr: &hir::Expr) -> bool { - match expr { - hir::Expr::Call { name, args, .. } | hir::Expr::MacroCall { name, args, .. } => { - name.starts_with("random.") || args.iter().any(expr_contains_random) - } - hir::Expr::Array { elements, .. } => elements.iter().any(expr_contains_random), - hir::Expr::Dict { entries, .. } => entries - .iter() - .any(|entry| expr_contains_random(&entry.key) || expr_contains_random(&entry.value)), - hir::Expr::Comprehension { - element, - iterable, - condition, - .. - } => { - expr_contains_random(element) - || expr_contains_random(iterable) - || condition.as_deref().is_some_and(expr_contains_random) - } - hir::Expr::Lambda { body, .. } | hir::Expr::Unary { operand: body, .. } => { - expr_contains_random(body) - } - hir::Expr::Vector { x, y, z, .. } => { - expr_contains_random(x) || expr_contains_random(y) || expr_contains_random(z) - } - hir::Expr::PlayerVar { player, .. } - | hir::Expr::Member { - receiver: player, .. - } => expr_contains_random(player), - hir::Expr::ReceiverCall { receiver, args, .. } => { - expr_contains_random(receiver) || args.iter().any(expr_contains_random) - } - hir::Expr::Type { args, .. } | hir::Expr::Format { args, .. } => { - args.iter().any(expr_contains_random) - } - hir::Expr::Binary { left, right, .. } => { - expr_contains_random(left) || expr_contains_random(right) - } - hir::Expr::Conditional { - then_value, - condition, - else_value, - .. - } => { - expr_contains_random(then_value) - || expr_contains_random(condition) - || expr_contains_random(else_value) - } - hir::Expr::Index { array, index, .. } => { - expr_contains_random(array) || expr_contains_random(index) - } - hir::Expr::Number { .. } - | hir::Expr::String { .. } - | hir::Expr::Bool { .. } - | hir::Expr::Null { .. } - | hir::Expr::StringModifier { .. } - | hir::Expr::Local { .. } - | hir::Expr::Enum { .. } - | hir::Expr::GlobalVar { .. } - | hir::Expr::HostPlayer { .. } - | hir::Expr::EventPlayer { .. } - | hir::Expr::Constant { .. } - | hir::Expr::MacroParam { .. } => false, - } -} - fn compression_alphabet_chars() -> Vec { (1..=47) .chain(std::iter::once(50)) @@ -7660,10 +7525,6 @@ fn case_sensitive_character(character: char) -> char { } } -fn computed_number_text(value: f64) -> String { - workshop_rs::format::format_number(value) -} - fn canonical_format_text(text: &str) -> String { let mut output = String::with_capacity(text.len()); let mut chars = text.chars().peekable(); diff --git a/crates/opy-rs/src/hir/visit.rs b/crates/opy-rs/src/hir/visit.rs index 3e25d272..c55e5697 100644 --- a/crates/opy-rs/src/hir/visit.rs +++ b/crates/opy-rs/src/hir/visit.rs @@ -1,5 +1,48 @@ use super::types::{Expr, Stmt, SwitchArm}; +pub(crate) fn contains_random(expression: &Expr) -> bool { + struct RandomFinder(bool); + + impl Visitor for RandomFinder { + fn visit_expr(&mut self, expression: &Expr) { + if self.0 { + return; + } + if matches!( + expression, + Expr::Call { name, .. } | Expr::MacroCall { name, .. } + if name.starts_with("random.") + ) { + self.0 = true; + } else { + walk_expr(self, expression); + } + } + } + + let mut finder = RandomFinder(false); + Visitor::visit_expr(&mut finder, expression); + finder.0 +} + +pub(crate) fn literal_number(expression: &Expr) -> Option { + match expression { + Expr::Null { .. } => Some(0.0), + Expr::Number { value, .. } => Some(*value), + Expr::Unary { op, operand, .. } if op == "+" => literal_number(operand), + Expr::Unary { op, operand, .. } if op == "-" => literal_number(operand).map(|value| -value), + _ => None, + } +} + +pub(crate) fn has_random_nested_delete(root: &Expr, indices: &[&Expr]) -> bool { + indices.len() >= 3 + && (contains_random(root) + || indices[..indices.len() - 1] + .iter() + .any(|index| contains_random(index))) +} + pub(crate) trait Visitor { fn visit_expr(&mut self, expression: &Expr) { walk_expr(self, expression); diff --git a/crates/opy-rs/src/lower.rs b/crates/opy-rs/src/lower.rs index 9eb82207..bb997f50 100644 --- a/crates/opy-rs/src/lower.rs +++ b/crates/opy-rs/src/lower.rs @@ -945,79 +945,7 @@ fn has_random_nested_delete(target: &HirExpr) -> bool { root = array.as_ref(); } indices.reverse(); - indices.len() >= 3 - && (hir_expr_contains_random(root) - || indices[..indices.len() - 1] - .iter() - .any(|index| hir_expr_contains_random(index))) -} - -fn hir_expr_contains_random(expr: &HirExpr) -> bool { - match expr { - HirExpr::Call { name, args, .. } | HirExpr::MacroCall { name, args, .. } => { - name.starts_with("random.") || args.iter().any(hir_expr_contains_random) - } - HirExpr::Array { elements, .. } => elements.iter().any(hir_expr_contains_random), - HirExpr::Dict { entries, .. } => entries.iter().any(|entry| { - hir_expr_contains_random(&entry.key) || hir_expr_contains_random(&entry.value) - }), - HirExpr::Comprehension { - element, - iterable, - condition, - .. - } => { - hir_expr_contains_random(element) - || hir_expr_contains_random(iterable) - || condition.as_deref().is_some_and(hir_expr_contains_random) - } - HirExpr::Lambda { body, .. } | HirExpr::Unary { operand: body, .. } => { - hir_expr_contains_random(body) - } - HirExpr::Vector { x, y, z, .. } => { - hir_expr_contains_random(x) - || hir_expr_contains_random(y) - || hir_expr_contains_random(z) - } - HirExpr::PlayerVar { player, .. } - | HirExpr::Member { - receiver: player, .. - } => hir_expr_contains_random(player), - HirExpr::ReceiverCall { receiver, args, .. } => { - hir_expr_contains_random(receiver) || args.iter().any(hir_expr_contains_random) - } - HirExpr::Type { args, .. } | HirExpr::Format { args, .. } => { - args.iter().any(hir_expr_contains_random) - } - HirExpr::Binary { left, right, .. } => { - hir_expr_contains_random(left) || hir_expr_contains_random(right) - } - HirExpr::Conditional { - then_value, - condition, - else_value, - .. - } => { - hir_expr_contains_random(then_value) - || hir_expr_contains_random(condition) - || hir_expr_contains_random(else_value) - } - HirExpr::Index { array, index, .. } => { - hir_expr_contains_random(array) || hir_expr_contains_random(index) - } - HirExpr::Number { .. } - | HirExpr::String { .. } - | HirExpr::Bool { .. } - | HirExpr::Null { .. } - | HirExpr::StringModifier { .. } - | HirExpr::Local { .. } - | HirExpr::Enum { .. } - | HirExpr::GlobalVar { .. } - | HirExpr::HostPlayer { .. } - | HirExpr::EventPlayer { .. } - | HirExpr::Constant { .. } - | HirExpr::MacroParam { .. } => false, - } + crate::hir::visit::has_random_nested_delete(root, &indices) } impl From for HirSpan { diff --git a/crates/opy-rs/src/lower/special_forms.rs b/crates/opy-rs/src/lower/special_forms.rs index 3724fe63..0a3382d2 100644 --- a/crates/opy-rs/src/lower/special_forms.rs +++ b/crates/opy-rs/src/lower/special_forms.rs @@ -336,15 +336,6 @@ impl Lowerer { /// Whether `compressed()` accepts these elements: only numbers, or only /// vectors of numbers. fn is_compressible(values: &[HirExpr]) -> bool { - fn number(expr: &HirExpr) -> Option { - match expr { - HirExpr::Number { value, .. } => Some(*value), - HirExpr::Null { .. } => Some(0.0), - HirExpr::Unary { op, operand, .. } if op == "-" => number(operand).map(|v| -v), - HirExpr::Unary { op, operand, .. } if op == "+" => number(operand), - _ => None, - } - } if values.is_empty() { return false; } @@ -356,12 +347,12 @@ fn is_compressible(values: &[HirExpr]) -> bool { let HirExpr::Vector { x, y, z, .. } = value else { return false; }; - [x, y, z] - .into_iter() - .all(|component| number(component).is_some_and(|v| v.abs() < 4999.0)) + [x, y, z].into_iter().all(|component| { + crate::hir::visit::literal_number(component).is_some_and(|v| v.abs() < 4999.0) + }) }); } values .iter() - .all(|value| number(value).is_some_and(|v| v.abs() < 49999.0)) + .all(|value| crate::hir::visit::literal_number(value).is_some_and(|v| v.abs() < 49999.0)) } diff --git a/crates/opy-rs/src/parser/declarations.rs b/crates/opy-rs/src/parser/declarations.rs index 3846dba0..9cd8cd09 100644 --- a/crates/opy-rs/src/parser/declarations.rs +++ b/crates/opy-rs/src/parser/declarations.rs @@ -5,16 +5,11 @@ impl Parser<'_> { let start = self.advance(); // The name token follows the keyword; its span is the exact declared // identifier occurrence (rename targets, not the keyword/statement). - let name_token = self.peek().clone(); + let name_span = self.peek().span; let name = match self.expect_ident("a variable name after the keyword") { Ok(name) => name, Err(()) => return false, }; - let name_span = if name_token.kind == TokenKind::Ident { - name_token.span - } else { - start.span - }; let mut index = None; let mut initializer = None; if self.peek_kind() == TokenKind::Assign { @@ -74,16 +69,11 @@ impl Parser<'_> { let start = self.advance(); // The name token follows the `subroutine` keyword; its span is the // exact declared identifier occurrence. - let name_token = self.peek().clone(); + let name_span = self.peek().span; let name = match self.expect_ident("a subroutine name") { Ok(name) => name, Err(()) => return false, }; - let name_span = if name_token.kind == TokenKind::Ident { - name_token.span - } else { - start.span - }; let end = self .tokens .get(self.pos.saturating_sub(1)) @@ -165,7 +155,7 @@ impl Parser<'_> { pub(super) fn parse_macro(&mut self, declarations: &mut Vec) -> bool { let start = self.advance(); - let name_token = self.peek().clone(); + let name_span = self.peek().span; let mut name = match self.expect_ident("a macro name") { Ok(name) => name, Err(()) => return false, @@ -228,7 +218,7 @@ impl Parser<'_> { self.errors.push(OpyError::at( "macro-redeclaration", format!("macro '{name}' is already defined"), - name_token.span, + name_span, )); } declarations.push(Decl::Macro { diff --git a/crates/opy-rs/src/parser/definitions.rs b/crates/opy-rs/src/parser/definitions.rs index c0fe3f8c..a518fb8d 100644 --- a/crates/opy-rs/src/parser/definitions.rs +++ b/crates/opy-rs/src/parser/definitions.rs @@ -349,16 +349,11 @@ impl Parser<'_> { // The name token follows the `def` keyword. `span` covers the // definition (`def name`), and `name_span` is the exact identifier // occurrence (rename targets, not the keyword). - let name_token = self.peek().clone(); + let name_span = self.peek().span; let name = match self.expect_ident("a subroutine name after `def`") { Ok(name) => name, Err(()) => return false, }; - let name_span = if name_token.kind == TokenKind::Ident { - name_token.span - } else { - start.span - }; let params = match self.parse_param_list() { Some(params) => params, None => return false, @@ -416,11 +411,7 @@ impl Parser<'_> { .and_then(|annotation| annotation.args.first()) .map(|arg| unquote_annotation_arg(&arg.text)); let body = self.parse_block(body_indent); - let span = if name_token.kind == TokenKind::Ident { - Span::new(start.span.file, start.span.start, name_token.span.end) - } else { - start.span - }; + let span = Span::new(start.span.file, start.span.start, name_span.end); rules.push(RuleEntry::SubroutineDef { name, presentation_name, From 95489f27671c1776ca5387fd70a3ba25ad243446 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Sun, 27 Sep 2026 23:58:13 +0800 Subject: [PATCH 04/67] refactor(opy-rs): collapse redundant match arms --- crates/opy-rs/src/compiler/backend.rs | 5 +- crates/opy-rs/src/compiler/lowering.rs | 59 ++++++++----------- crates/opy-rs/src/compiler/mod.rs | 9 +-- .../src/compiler/operator_optimization.rs | 6 +- crates/opy-rs/src/compiler/reconstruct.rs | 4 +- .../size_optimization/literal_slots.rs | 43 ++++++++------ crates/opy-rs/src/hir/dump.rs | 6 +- crates/opy-rs/src/hir/error.rs | 13 ++-- crates/opy-rs/src/hir/visit.rs | 10 ++-- crates/opy-rs/src/lexer.rs | 5 +- crates/opy-rs/src/lower.rs | 8 +-- crates/opy-rs/src/lower/expressions.rs | 5 +- crates/opy-rs/src/lower/textures.rs | 3 +- crates/opy-rs/src/macro_js/engine/mod.rs | 5 +- crates/opy-rs/src/parser/expressions.rs | 8 +-- crates/opy-rs/src/preprocess/project.rs | 1 - crates/opy-rs/src/settings.rs | 3 +- 17 files changed, 88 insertions(+), 105 deletions(-) diff --git a/crates/opy-rs/src/compiler/backend.rs b/crates/opy-rs/src/compiler/backend.rs index b46d57b9..11793d4d 100644 --- a/crates/opy-rs/src/compiler/backend.rs +++ b/crates/opy-rs/src/compiler/backend.rs @@ -253,8 +253,9 @@ impl MacroExpander { Stmt::Break { .. } | Stmt::Return { .. } | Stmt::CallSubroutine { .. } - | Stmt::Pass { .. } => statement.clone(), - Stmt::Continue { .. } | Stmt::Label { .. } => statement.clone(), + | Stmt::Pass { .. } + | Stmt::Continue { .. } + | Stmt::Label { .. } => statement.clone(), }) } diff --git a/crates/opy-rs/src/compiler/lowering.rs b/crates/opy-rs/src/compiler/lowering.rs index cf19f04d..f5da6251 100644 --- a/crates/opy-rs/src/compiler/lowering.rs +++ b/crates/opy-rs/src/compiler/lowering.rs @@ -336,7 +336,6 @@ fn contains_loop_continue(statement: &Stmt) -> bool { .as_ref() .is_some_and(|body| body.iter().any(contains_loop_continue)) } - Stmt::For { .. } | Stmt::While { .. } | Stmt::DoWhile { .. } => false, Stmt::Switch { arms, .. } => arms.iter().any(|arm| match arm { SwitchArm::Case { body, .. } | SwitchArm::Default { body, .. } => { body.iter().any(contains_loop_continue) @@ -367,11 +366,8 @@ fn switch_body_is_noop(statements: &[Stmt]) -> bool { } impl<'a> Lowering<'a> { - pub(super) fn new( - compiler: &'a Compiler, - hir: &'a hir::Program, - ) -> Result { - Ok(Self { + pub(super) fn new(compiler: &'a Compiler, hir: &'a hir::Program) -> Self { + Self { compiler, hir, program: Program::default(), @@ -394,7 +390,7 @@ impl<'a> Lowering<'a> { visible_labels: Vec::new(), deferred_gotos: Vec::new(), translation_uses: Vec::new(), - }) + } } pub(super) fn copy_files(&mut self) -> Result<(), IntegrationError> { @@ -1252,8 +1248,8 @@ impl<'a> Lowering<'a> { | Action::Else | Action::While { .. } | Action::End, - ) => false, - Some(Action::CallSubroutine { .. }) => true, + ) + | None => false, Some(Action::Call { name, .. }) => match name.as_str() { "abort" | "abortIf" | "break" | "continue" | "loop" | "loopIf" | "return" | "skip" | "skipIf" => false, @@ -1261,7 +1257,6 @@ impl<'a> Lowering<'a> { _ => true, }, Some(_) => true, - None => false, }) } @@ -2982,9 +2977,8 @@ impl<'a> Lowering<'a> { let locale = match language.as_str() { "de" => "de-DE", "en" => "en-US", - "es" => "es-MX", + "es" | "es_mx" => "es-MX", "es_es" => "es-ES", - "es_mx" => "es-MX", "fr" => "fr-FR", "it" => "it-IT", "ja" => "ja-JP", @@ -3951,7 +3945,7 @@ impl<'a> Lowering<'a> { return Ok(self.push_call_action(action_name, &args)); } - let outer_array = self.lower_indexed_read(root_value, indices[0], index)?; + let outer_array = self.lower_indexed_read(root_value, indices[0], index); if indices.len() == 4 { let replacement = self.rebuild_deleted_array(outer_array, &indices[1..])?; let action_name = if action_name == "modifyGlobalVariableAtIndex" { @@ -3964,7 +3958,7 @@ impl<'a> Lowering<'a> { return Ok(self.push_call_action(action_name, &args)); } let inner_index = self.lower_value(indices[1])?; - let row = self.lower_indexed_read(outer_array, indices[1], inner_index)?; + let row = self.lower_indexed_read(outer_array, indices[1], inner_index); let leaf_index = self.lower_value(indices[2])?; let current_index = self.push_call("currentArrayIndex", Vec::new()); let condition = self.push_call("!=", vec![current_index, leaf_index]); @@ -4211,7 +4205,7 @@ impl<'a> Lowering<'a> { }; let outer_index = self.lower_value(indices[0])?; - let outer_array = self.lower_indexed_read(root_value, indices[0], outer_index)?; + let outer_array = self.lower_indexed_read(root_value, indices[0], outer_index); let replacement = self.rebuild_indexed_value(outer_array, &indices[1..], target, value)?; let args = self.normalize_contextual_arguments( action_name, @@ -4239,14 +4233,14 @@ impl<'a> Lowering<'a> { && left.as_ref() == target && let Some((_, call_name)) = modify_operator(op) { - let current = self.lower_indexed_read(array, index, index_value)?; + let current = self.lower_indexed_read(array, index, index_value); let right = self.lower_value(right)?; self.push_call(call_name, vec![current, right]) } else { self.lower_value(value)? } } else { - let child = self.lower_indexed_read(array, index, index_value)?; + let child = self.lower_indexed_read(array, index, index_value); self.rebuild_indexed_value(child, &indices[1..], target, value)? }; Ok(self.replace_array_element(array, index_value, replacement)) @@ -4257,11 +4251,11 @@ impl<'a> Lowering<'a> { array: ValueId, index: &Expr, index_value: ValueId, - ) -> Result { + ) -> ValueId { if matches!(index, Expr::Number { value, .. } if *value == 0.0) { - Ok(self.push_call("firstOf", vec![array])) + self.push_call("firstOf", vec![array]) } else { - Ok(self.push_call("valueInArray", vec![array, index_value])) + self.push_call("valueInArray", vec![array, index_value]) } } @@ -4293,7 +4287,7 @@ impl<'a> Lowering<'a> { let condition = self.push_call("!=", vec![current_index, index]); return Ok(self.push_call("filteredArray", vec![array, condition])); } - let child = self.lower_indexed_read(array, indices[0], index)?; + let child = self.lower_indexed_read(array, indices[0], index); let replacement = self.rebuild_deleted_array(child, &indices[1..])?; Ok(self.replace_array_element_for_delete(array, indices[0], index, replacement)) } @@ -4885,8 +4879,8 @@ impl<'a> Lowering<'a> { return Ok(self.push_value(Value::Bool(value))); } crate::compile_time::Value::Array(_) - | crate::compile_time::Value::Object(_) => {} - crate::compile_time::Value::Number(_) => {} + | crate::compile_time::Value::Object(_) + | crate::compile_time::Value::Number(_) => {} } } } @@ -5117,7 +5111,7 @@ impl<'a> Lowering<'a> { } } if op == "==" && self.optimization_state_at(span.as_ref()).enabled { - if let Some(value) = self.lower_current_map_equality(left, right)? { + if let Some(value) = self.lower_current_map_equality(left, right) { return Ok(value); } } @@ -6103,11 +6097,7 @@ impl<'a> Lowering<'a> { /// `getCurrentMap() == Map.X`: the maps whose value comparison the /// Workshop gets wrong are compared as text instead. - fn lower_current_map_equality( - &mut self, - left: &Expr, - right: &Expr, - ) -> Result, IntegrationError> { + fn lower_current_map_equality(&mut self, left: &Expr, right: &Expr) -> Option { let is_current_map = |expr: &Expr| matches!(expr, Expr::Call { name, args, .. } if name == "getCurrentMap" && args.is_empty()); let map_of = |expr: &Expr| match expr { Expr::Enum { @@ -6118,7 +6108,7 @@ impl<'a> Lowering<'a> { let map = match (map_of(left), map_of(right)) { (Some(map), None) if is_current_map(right) => map, (None, Some(map)) if is_current_map(left) => map, - _ => return Ok(None), + _ => return None, }; let current = self.push_call("currentMap", Vec::new()); let map_value = self.push_value(Value::Enum { @@ -6126,13 +6116,13 @@ impl<'a> Lowering<'a> { value: map.clone(), }); if !TEXT_COMPARED_MAPS.contains(&map.as_str()) { - return Ok(Some(self.push_call("==", vec![current, map_value]))); + return Some(self.push_call("==", vec![current, map_value])); } let format = self.push_value(Value::String("{0}".to_string())); let current_text = self.push_call("customString", vec![format, current]); let format = self.push_value(Value::String("{0}".to_string())); let map_text = self.push_call("customString", vec![format, map_value]); - Ok(Some(self.push_call("==", vec![current_text, map_text]))) + Some(self.push_call("==", vec![current_text, map_text])) } /// A bare `getCurrentMap()` selects the used map from the bugged ones by @@ -7601,10 +7591,11 @@ fn compile_time_value_text(value: crate::compile_time::Value) -> Option crate::compile_time::Value::Number(value) if value.is_finite() => { Some(crate::compile_time::workshop_number_text(value)) } - crate::compile_time::Value::Number(_) => None, + crate::compile_time::Value::Number(_) + | crate::compile_time::Value::Array(_) + | crate::compile_time::Value::Object(_) => None, crate::compile_time::Value::String(value) => Some(value), crate::compile_time::Value::Bool(value) => Some(value.to_string()), - crate::compile_time::Value::Array(_) | crate::compile_time::Value::Object(_) => None, } } diff --git a/crates/opy-rs/src/compiler/mod.rs b/crates/opy-rs/src/compiler/mod.rs index 648bf704..7736bea5 100644 --- a/crates/opy-rs/src/compiler/mod.rs +++ b/crates/opy-rs/src/compiler/mod.rs @@ -238,8 +238,7 @@ fn emit_debug_element_counts( fn debug_value_count(node: &workshop_rs::actions::ElementCountNode) -> usize { let children = node.children.iter().map(debug_value_count).sum::(); match node.name.as_str() { - "number" | "global variable" => 2, - "localized string" => 2, + "number" | "global variable" | "localized string" => 2, "customString" => 1 + 4usize.saturating_sub(node.children.len()) + children, "Team" | "Color" => 1 + children.max(1), "array" | "evalOnce" => 2 + children, @@ -496,7 +495,7 @@ impl Compiler { } reject_unlowered_directives(hir)?; let expanded_hir = expand_macros(hir)?; - let mut lowering = Lowering::new(self, &expanded_hir)?; + let mut lowering = Lowering::new(self, &expanded_hir); lowering.copy_files()?; lowering.lower_declarations()?; lowering.lower_rules()?; @@ -564,7 +563,7 @@ impl Compiler { ) -> Result { let locale = Locale::new(language); let expanded = expand_macros_attributed(hir)?; - let mut lowering = Lowering::new(self, &expanded)?; + let mut lowering = Lowering::new(self, &expanded); lowering.copy_files()?; lowering.lower_declarations()?; lowering.lower_rules()?; @@ -836,8 +835,6 @@ fn workshop_error_span(error: &workshop_rs::WorkshopError) -> Option *span, - workshop_rs::WorkshopError::Catalog(_) - | workshop_rs::WorkshopError::MissingMapping { .. } => None, _ => None, } } diff --git a/crates/opy-rs/src/compiler/operator_optimization.rs b/crates/opy-rs/src/compiler/operator_optimization.rs index 0edcf19e..ab12b514 100644 --- a/crates/opy-rs/src/compiler/operator_optimization.rs +++ b/crates/opy-rs/src/compiler/operator_optimization.rs @@ -1206,7 +1206,9 @@ pub(super) fn same(left: &Value, right: &Value) -> bool { match (left, right) { (Value::Number(a), Value::Number(b)) => a == b, (Value::String(a), Value::String(b)) - | (Value::LocalizedString(a), Value::LocalizedString(b)) => a == b, + | (Value::LocalizedString(a), Value::LocalizedString(b)) + | (Value::GlobalVariable(a), Value::GlobalVariable(b)) + | (Value::Subroutine(a), Value::Subroutine(b)) => a == b, (Value::Bool(a), Value::Bool(b)) => a == b, (Value::Null, Value::Null) | (Value::EventPlayer, Value::EventPlayer) => true, (Value::Array(a), Value::Array(b)) => { @@ -1234,8 +1236,6 @@ pub(super) fn same(left: &Value, right: &Value) -> bool { value: bv, }, ) => at == bt && av == bv, - (Value::GlobalVariable(a), Value::GlobalVariable(b)) - | (Value::Subroutine(a), Value::Subroutine(b)) => a == b, ( Value::PlayerVariable { player: ap, diff --git a/crates/opy-rs/src/compiler/reconstruct.rs b/crates/opy-rs/src/compiler/reconstruct.rs index d90d087f..a5b0e8ba 100644 --- a/crates/opy-rs/src/compiler/reconstruct.rs +++ b/crates/opy-rs/src/compiler/reconstruct.rs @@ -614,8 +614,8 @@ impl<'a> Emitter<'a> { self.out.push_str(":\n"); match &rule.event { Event::Global => self.out.push_str(" @Event global\n"), - Event::EachPlayer => self.out.push_str(" @Event eachPlayer\n"), - Event::EachPlayerWithFilters { + Event::EachPlayer + | Event::EachPlayerWithFilters { team: EventTeam::All, target: EventTarget::All, } => self.out.push_str(" @Event eachPlayer\n"), diff --git a/crates/opy-rs/src/compiler/size_optimization/literal_slots.rs b/crates/opy-rs/src/compiler/size_optimization/literal_slots.rs index 8a6595a6..17a673c9 100644 --- a/crates/opy-rs/src/compiler/size_optimization/literal_slots.rs +++ b/crates/opy-rs/src/compiler/size_optimization/literal_slots.rs @@ -39,25 +39,25 @@ pub(super) fn slot(name: &str, index: usize) -> Option { fn is_boolean_slot(name: &str, index: usize) -> bool { match name { - // Waits, skips and arithmetic. + // Waits, skips, arithmetic, player statistics and abilities. "wait" | "skip" | "setMatchTime" | "getObjectivePosition" | "getPlayersInSlot" - | "isObjectiveComplete" => index == 0, - "skipIf" | "charAt" | "valueInArray" | "addToScore" | "addToTeamScore" | "setScore" - | "setTeamScore" | "destroyDummy" | "getAmmo" | "getMaxAmmo" => index == 1, - "max" | "min" | "randomInteger" => index <= 1, - "subtract" => index == 0, - "slice" => matches!(index, 1 | 2), - "vector" => index <= 2, - // Chase destinations and rates. - "chaseAtRate" | "chaseOverTime" => matches!(index, 1 | 2), - // Player stats and abilities. - "setAbilityCharge" | "setAbilityCooldown" | "setAbilityResource" => index == 2, - "setAmmo" | "setMaxAmmo" => matches!(index, 1 | 2), - "setGravity" + | "isObjectiveComplete" + | "subtract" => index == 0, + "skipIf" + | "charAt" + | "valueInArray" + | "addToScore" + | "addToTeamScore" + | "setScore" + | "setTeamScore" + | "destroyDummy" + | "getAmmo" + | "getMaxAmmo" + | "setGravity" | "setMoveSpeed" | "setProjectileGravity" | "setProjectileSpeed" @@ -67,13 +67,20 @@ fn is_boolean_slot(name: &str, index: usize) -> bool { | "startModifyingVoicelinePitch" | "startScalingBarriers" | "startScalingSize" => index == 1, - "setStatusEffect" => index == 3, + "max" | "min" | "randomInteger" => index <= 1, + "slice" => matches!(index, 1 | 2), + "vector" => index <= 2, + // Chase destinations and rates. + "chaseAtRate" | "chaseOverTime" => matches!(index, 1 | 2), + "setAbilityCharge" | "setAbilityCooldown" | "setAbilityResource" | "createDummyBot" => { + index == 2 + } + "setAmmo" | "setMaxAmmo" => matches!(index, 1 | 2), + "setStatusEffect" | "createInWorldText" => index == 3, "startForcingThrottle" => (1..=6).contains(&index), - // Effects, HUD and dummies. - "createDummyBot" => index == 2, + // Effects and HUD. "createEffect" => index == 4, "createHudText" => index == 5, - "createInWorldText" => index == 3, _ => false, } } diff --git a/crates/opy-rs/src/hir/dump.rs b/crates/opy-rs/src/hir/dump.rs index 5324ae76..6927bc36 100644 --- a/crates/opy-rs/src/hir/dump.rs +++ b/crates/opy-rs/src/hir/dump.rs @@ -527,7 +527,9 @@ fn render_expr(expr: &Expr, out: &mut String) { out.push(modifier.chars().next().unwrap_or_default()); out.push_str(&format!("{:?}", value)); } - Expr::Local { name, .. } => out.push_str(name), + Expr::Local { name, .. } | Expr::GlobalVar { name, .. } | Expr::Constant { name, .. } => { + out.push_str(name) + } Expr::Vector { x, y, z, .. } => { out.push_str("vect("); render_expr(x, out); @@ -544,7 +546,6 @@ fn render_expr(expr: &Expr, out: &mut String) { out.push('.'); out.push_str(value); } - Expr::GlobalVar { name, .. } => out.push_str(name), Expr::PlayerVar { player, name, .. } => { render_expr(player, out); out.push('.'); @@ -559,7 +560,6 @@ fn render_expr(expr: &Expr, out: &mut String) { } Expr::EventPlayer { .. } => out.push_str("eventPlayer"), Expr::HostPlayer { .. } => out.push_str("hostPlayer"), - Expr::Constant { name, .. } => out.push_str(name), Expr::Call { name, args, .. } => { out.push_str(name); out.push('('); diff --git a/crates/opy-rs/src/hir/error.rs b/crates/opy-rs/src/hir/error.rs index 837e27bc..e667025d 100644 --- a/crates/opy-rs/src/hir/error.rs +++ b/crates/opy-rs/src/hir/error.rs @@ -28,32 +28,33 @@ impl HirError { /// Stable machine-readable code for this error. pub fn code(&self) -> &'static str { match self { - HirError::Malformed { code, .. } => code, + HirError::Malformed { code, .. } | HirError::Invalid { code, .. } => code, HirError::IncompatibleProtocol { .. } => "incompatible-protocol", HirError::UnsupportedNode { .. } => "unsupported-node", - HirError::Invalid { code, .. } => code, } } /// Human-readable message. pub fn message(&self) -> String { match self { - HirError::Malformed { message, .. } => message.clone(), + HirError::Malformed { message, .. } | HirError::Invalid { message, .. } => { + message.clone() + } HirError::IncompatibleProtocol { expected, received } => { format!("incompatible protocol: expected {expected}, received {received}") } HirError::UnsupportedNode { kind, .. } => { format!("unsupported node kind '{kind}'") } - HirError::Invalid { message, .. } => message.clone(), } } /// The offending source span, when known. pub fn span(&self) -> Option<&Span> { match self { - HirError::UnsupportedNode { span, .. } => span.as_ref(), - HirError::Invalid { span, .. } => span.as_ref(), + HirError::UnsupportedNode { span, .. } | HirError::Invalid { span, .. } => { + span.as_ref() + } _ => None, } } diff --git a/crates/opy-rs/src/hir/visit.rs b/crates/opy-rs/src/hir/visit.rs index c55e5697..8bb799e1 100644 --- a/crates/opy-rs/src/hir/visit.rs +++ b/crates/opy-rs/src/hir/visit.rs @@ -77,7 +77,10 @@ pub(crate) fn walk_expr(visitor: &mut V, expression: &Expr) .. } => visitor.visit_comprehension(element, iterable, condition.as_deref()), Expr::Lambda { body, .. } => visitor.visit_expr(body), - Expr::Type { args, .. } | Expr::Call { args, .. } | Expr::MacroCall { args, .. } => { + Expr::Type { args, .. } + | Expr::Call { args, .. } + | Expr::MacroCall { args, .. } + | Expr::Format { args, .. } => { for argument in args { visitor.visit_expr(argument); } @@ -119,11 +122,6 @@ pub(crate) fn walk_expr(visitor: &mut V, expression: &Expr) visitor.visit_expr(else_value); } Expr::Unary { operand, .. } => visitor.visit_expr(operand), - Expr::Format { args, .. } => { - for argument in args { - visitor.visit_expr(argument); - } - } Expr::Number { .. } | Expr::String { .. } | Expr::Bool { .. } diff --git a/crates/opy-rs/src/lexer.rs b/crates/opy-rs/src/lexer.rs index 15fbeeec..81ac2bce 100644 --- a/crates/opy-rs/src/lexer.rs +++ b/crates/opy-rs/src/lexer.rs @@ -149,7 +149,7 @@ impl Lexer { )); } } - '#' => self.lex_hash()?, + '#' => self.lex_hash(), '/' if self.peek(1) == Some('*') => self.skip_block_comment()?, '"' | '\'' => self.lex_string(ch)?, c if c.is_ascii_digit() => self.lex_number()?, @@ -221,7 +221,7 @@ impl Lexer { } /// `#` starts a `#!` directive (captured as one token) or a comment. - fn lex_hash(&mut self) -> OpyResult<()> { + fn lex_hash(&mut self) { if self.peek(1) == Some('!') { let start = self.here(2); self.advance(); @@ -249,7 +249,6 @@ impl Lexer { self.advance(); } } - Ok(()) } fn skip_block_comment(&mut self) -> OpyResult<()> { diff --git a/crates/opy-rs/src/lower.rs b/crates/opy-rs/src/lower.rs index bb997f50..f248ea3a 100644 --- a/crates/opy-rs/src/lower.rs +++ b/crates/opy-rs/src/lower.rs @@ -889,7 +889,7 @@ fn context_player_expr(name: &str, span: Option) -> Option { "eventPlayer" => Some(HirExpr::EventPlayer { span: span.map(Into::into), }), - "localPlayer" => Some(HirExpr::Call { + "localPlayer" | "attacker" | "victim" | "healer" | "healee" => Some(HirExpr::Call { name: name.to_string(), args: Vec::new(), debug_source: None, @@ -898,12 +898,6 @@ fn context_player_expr(name: &str, span: Option) -> Option { "hostPlayer" => Some(HirExpr::HostPlayer { span: span.map(Into::into), }), - "attacker" | "victim" | "healer" | "healee" => Some(HirExpr::Call { - name: name.to_string(), - args: Vec::new(), - debug_source: None, - span: span.map(Into::into), - }), _ => None, } } diff --git a/crates/opy-rs/src/lower/expressions.rs b/crates/opy-rs/src/lower/expressions.rs index 8e775d90..41591915 100644 --- a/crates/opy-rs/src/lower/expressions.rs +++ b/crates/opy-rs/src/lower/expressions.rs @@ -672,10 +672,7 @@ impl Lowerer { "invalid-iterable", format!("for-loop iterable '{name}' must be a range(...) call"), ), - CallPosition::LambdaArgument => { - ("unknown-value", format!("unknown value '{name}'")) - } - CallPosition::MacroBody => { + CallPosition::LambdaArgument | CallPosition::MacroBody => { ("unknown-value", format!("unknown value '{name}'")) } }; diff --git a/crates/opy-rs/src/lower/textures.rs b/crates/opy-rs/src/lower/textures.rs index f81f15b7..28913c4a 100644 --- a/crates/opy-rs/src/lower/textures.rs +++ b/crates/opy-rs/src/lower/textures.rs @@ -71,9 +71,8 @@ pub(super) fn tag(member: &str) -> Option<&'static str> { "MOUSE_SIDE_BUTTON" => Some(""), "DPAD_UP_ARROW" => Some(""), "DPAD_DOWN_ARROW" => Some(""), - "DPAD_LEFT_ARROW" => Some(""), + "DPAD_LEFT_ARROW" | "Y_BUTTON" => Some(""), "DPAD_RIGHT_ARROW" => Some(""), - "Y_BUTTON" => Some(""), "B_BUTTON" => Some(""), "A_BUTTON" => Some(""), "MOUSE_CURSOR" => Some(""), diff --git a/crates/opy-rs/src/macro_js/engine/mod.rs b/crates/opy-rs/src/macro_js/engine/mod.rs index 684d7e98..35faebcc 100644 --- a/crates/opy-rs/src/macro_js/engine/mod.rs +++ b/crates/opy-rs/src/macro_js/engine/mod.rs @@ -26,8 +26,9 @@ pub(crate) enum EngineError { impl fmt::Display for EngineError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - EngineError::Exception { message, .. } => f.write_str(message), - EngineError::Internal(message) => f.write_str(message), + EngineError::Exception { message, .. } | EngineError::Internal(message) => { + f.write_str(message) + } } } } diff --git a/crates/opy-rs/src/parser/expressions.rs b/crates/opy-rs/src/parser/expressions.rs index dc721959..ce5e6956 100644 --- a/crates/opy-rs/src/parser/expressions.rs +++ b/crates/opy-rs/src/parser/expressions.rs @@ -470,7 +470,7 @@ impl Parser<'_> { span: token.span, }) } - TokenKind::String => self.parse_string_literal(), + TokenKind::String => Ok(self.parse_string_literal()), TokenKind::Ident => { let token = self.advance(); if token.text == "lambda" { @@ -608,7 +608,7 @@ impl Parser<'_> { /// delimiter group. Outside a group, a newline remains a statement /// boundary, matching the bounded implicit-concatenation surface used by /// the OverPy examples. - pub(super) fn parse_string_literal(&mut self) -> Result { + pub(super) fn parse_string_literal(&mut self) -> Expr { let first = self.advance(); let mut value = first.text.clone(); let mut end = first.span.end; @@ -625,10 +625,10 @@ impl Parser<'_> { value.push_str(&next.text); end = next.span.end; } - Ok(Expr::String { + Expr::String { value, span: Span::new(first.span.file, first.span.start, end), - }) + } } /// Return whether the current parser position is inside `()`, `[]`, or diff --git a/crates/opy-rs/src/preprocess/project.rs b/crates/opy-rs/src/preprocess/project.rs index d97707c0..2557e4ab 100644 --- a/crates/opy-rs/src/preprocess/project.rs +++ b/crates/opy-rs/src/preprocess/project.rs @@ -30,7 +30,6 @@ pub(super) fn display_path( let mut components = Vec::new(); for component in relative.components() { match component { - std::path::Component::CurDir => {} std::path::Component::ParentDir => { components.push("..".to_string()); } diff --git a/crates/opy-rs/src/settings.rs b/crates/opy-rs/src/settings.rs index fa54b4ef..b36d648e 100644 --- a/crates/opy-rs/src/settings.rs +++ b/crates/opy-rs/src/settings.rs @@ -909,12 +909,11 @@ impl Jsonc<'_> { depth += 1; value.push(self.advance().expect("peeked character exists")); } - ']' if depth == 0 => break, + ']' | ',' | '}' if depth == 0 => break, ')' | ']' => { depth = depth.saturating_sub(1); value.push(self.advance().expect("peeked character exists")); } - ',' | '}' if depth == 0 => break, _ => value.push(self.advance().expect("peeked character exists")), } } From 6335a7b96a24bab9dcbab34bab1df4cdcaf21ef7 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 00:13:42 +0800 Subject: [PATCH 05/67] refactor(opy-rs): split lowering responsibilities --- crates/opy-rs/src/compiler/lowering.rs | 5647 +---------------- .../src/compiler/lowering/action_calls.rs | 522 ++ .../src/compiler/lowering/assignments.rs | 467 ++ .../src/compiler/lowering/presentation.rs | 716 +++ crates/opy-rs/src/compiler/lowering/rules.rs | 1821 ++++++ crates/opy-rs/src/compiler/lowering/values.rs | 1836 ++++++ 6 files changed, 5515 insertions(+), 5494 deletions(-) create mode 100644 crates/opy-rs/src/compiler/lowering/action_calls.rs create mode 100644 crates/opy-rs/src/compiler/lowering/assignments.rs create mode 100644 crates/opy-rs/src/compiler/lowering/presentation.rs create mode 100644 crates/opy-rs/src/compiler/lowering/rules.rs create mode 100644 crates/opy-rs/src/compiler/lowering/values.rs diff --git a/crates/opy-rs/src/compiler/lowering.rs b/crates/opy-rs/src/compiler/lowering.rs index f5da6251..4de9ad0e 100644 --- a/crates/opy-rs/src/compiler/lowering.rs +++ b/crates/opy-rs/src/compiler/lowering.rs @@ -1,3 +1,9 @@ +mod action_calls; +mod assignments; +mod presentation; +mod rules; +mod values; + use super::action_optimization::ActionOptimizer; use super::number_format::trim_numbers; use super::operator_optimization::{OperatorOptimizer, same, self_modification}; @@ -1135,5536 +1141,189 @@ impl<'a> Lowering<'a> { Ok(()) } - pub(super) fn lower_rules(&mut self) -> Result<(), IntegrationError> { - for entry in &self.hir.rules { - match entry { - RuleEntry::Rule(rule) => self.lower_rule(rule)?, - RuleEntry::SubroutineDef { - name, - source_name, - span, - name_span, - body, - annotations, - .. - } => { - self.lower_subroutine(name, source_name, *span, *name_span, body, annotations)? - } - } - } - for rule in &mut self.program.rules { - rule.name = escape_bad_words(&rule.name); - } - Ok(()) - } - - fn lower_rule(&mut self, rule: &hir::Rule) -> Result<(), IntegrationError> { - self.reject_rule_metadata(rule)?; - let event = self.lower_event(&rule.event, &rule.annotations)?; - let mut condition_exprs = Vec::new(); - for expr in &rule.conditions { - Self::split_rule_condition(expr, &mut condition_exprs); - } - let conditions = condition_exprs.iter().copied(); - let conditions = self.lower_values(conditions)?; - let previous_conditions = self.current_rule_conditions.replace(conditions.clone()); - let lowered_actions = self.lower_actions(&rule.actions, None); - self.current_rule_conditions = previous_conditions; - let mut actions = Vec::new(); - actions.extend(lowered_actions?); - let optimization = self.optimization_state_at(rule.span.as_ref()); - if optimization.enabled - && !rule.delimiter - && !self.has_meaningful_rule_action(&self.useful_actions(&actions), &event) - { - return Ok(()); - } - let elide_noop_switch = actions.is_empty() - && rule.actions.len() == 1 - && matches!(rule.actions.first(), Some(Stmt::Switch { .. })); - if elide_noop_switch && !rule.disabled { - return Ok(()); - } - let rule_index = self.program.rules.len(); - self.program.rules.push(rule_from_parts( - rule.name.clone(), - rule.disabled, - event, - conditions - .iter() - .zip(&condition_exprs) - .map(|(value, expr)| { - let mut condition = self.materialize_value(*value); - split_all(&mut condition); - let optimization = self.optimization_state_at(expr.span()); - if optimization.enabled && optimization.for_size { - SizeOptimizer::new(self.compiler).condition(&mut condition); - } - let mut condition = OperatorOptimizer::new(self.compiler, optimization.strict) - .wrap_condition(condition); - trim_numbers(&mut condition); - workshop_rs::Condition::new(condition) - }) - .collect(), - self.public_actions(&actions), - )); - let action_provenance = self.action_provenance(&actions); - self.set_rule_provenance( - rule_index, - rule.span, - condition_exprs.iter().map(|expr| expr.span().copied()), - action_provenance, - )?; - Ok(()) + fn lower_array(&mut self, elements: Vec) -> ValueId { + let name = if elements.is_empty() { + "emptyArray" + } else { + "array" + }; + let elements = self.normalize_contextual_arguments(name, elements); + self.push_value(Value::Call { + name: name.to_string(), + args: self.value_args(&elements), + }) } - fn split_rule_condition<'expr>(expr: &'expr Expr, conditions: &mut Vec<&'expr Expr>) { - match expr { - Expr::Binary { - op, left, right, .. - } if op == "and" => { - Self::split_rule_condition(left, conditions); - Self::split_rule_condition(right, conditions); - } - Expr::Binary { - op, left, right, .. - } if op == "==" - && matches!(right.as_ref(), Expr::Bool { value: true, .. }) - && matches!(left.as_ref(), Expr::Binary { op, .. } if op == "and") => - { - Self::split_rule_condition(left, conditions); - } - _ => conditions.push(expr), - } + fn push_value(&mut self, value: Value) -> ValueId { + let id = self.values.len(); + self.values.push(value); + #[cfg(test)] + crate::resource_metrics::record_lowering_values(self.values.len()); + id } - fn has_meaningful_rule_action(&self, actions: &[ActionId], event: &Event) -> bool { - actions - .iter() - .any(|action| match self.actions.get(*action) { - Some( - Action::If { .. } - | Action::ElseIf { .. } - | Action::Else - | Action::While { .. } - | Action::End, - ) - | None => false, - Some(Action::Call { name, .. }) => match name.as_str() { - "abort" | "abortIf" | "break" | "continue" | "loop" | "loopIf" | "return" - | "skip" | "skipIf" => false, - "wait" => matches!(event, Event::Subroutine(_)), - _ => true, - }, - Some(_) => true, - }) + fn push_call(&mut self, name: &str, args: Vec) -> ValueId { + let args = self.normalize_contextual_arguments(name, args); + self.push_value(Value::Call { + name: name.to_string(), + args, + }) } - fn lower_subroutine( + fn normalize_contextual_argument( &mut self, - name: &str, - source_name: &str, - span: Option, - name_span: Option, - body: &[Stmt], - annotations: &[hir::Annotation], - ) -> Result<(), IntegrationError> { - self.reject_subroutine_metadata(annotations)?; - let source_name = if source_name.is_empty() { - name - } else { - source_name - }; - let subroutine = *self.subroutines.get(source_name).ok_or_else(|| { - self.unsupported( - format!("subroutine definition '{source_name}' has no declaration"), - name_span.or(span), - ) - })?; - if !self.defined_subroutines.insert(subroutine) { - return Err(self.unsupported( - format!("subroutine '{source_name}' has multiple definitions"), - name_span.or(span), - )); - } - let mut actions = Vec::new(); - actions.extend(self.lower_actions(body, None)?); - let event = Event::Subroutine(self.subroutine_names[subroutine].clone()); - if self.optimization_state_at(span.as_ref()).enabled - && !self.has_meaningful_rule_action(&actions, &event) - { - return Ok(()); - } - let rule_index = self.program.rules.len(); - self.program.rules.push(rule_from_parts( - self.subroutine_rule_name(name), - false, - event, - Vec::new(), - self.public_actions(&actions), - )); - let action_provenance = self.action_provenance(&actions); - self.set_rule_provenance(rule_index, span, std::iter::empty(), action_provenance)?; - Ok(()) - } - - fn reject_rule_metadata(&self, rule: &hir::Rule) -> Result<(), IntegrationError> { - if rule.new_page.is_some() { - let span = rule - .annotations - .iter() - .find(|annotation| annotation.name == "NewPage") - .and_then(|annotation| annotation.span) - .or(rule.span); - return Err(self.unsupported( - "rule new-page metadata is not representable in canonical WIR", - span, - )); - } - for annotation in &rule.annotations { - match annotation.name.as_str() { - "Event" | "Condition" | "Team" | "Slot" | "Hero" | "Disabled" | "Delimiter" - | "SuppressWarnings" => {} - _ => { - return Err(self.unsupported( - format!( - "rule annotation '{}' is not representable in canonical WIR", - annotation.name - ), - annotation.span.or(rule.span), - )); - } + call_id: &str, + arg_index: usize, + value_id: ValueId, + ) -> ValueId { + let domain = [Kind::Action, Kind::Value].into_iter().find_map(|kind| { + self.compiler + .catalog + .entry(kind, call_id) + .and_then(|entry| entry.param_domain(arg_index)) + }); + if domain == Some("BarrierLos") { + let value = match self.value(value_id) { + Value::Bool(true) => Some("PASS_THROUGH_BARRIERS"), + Value::Bool(false) => Some("BLOCKED_BY_ALL_BARRIERS"), + _ => None, + }; + if let Some(value) = value { + return self.push_value(Value::Enum { + value_type: "BarrierLos".to_string(), + value: value.to_string(), + }); } } - Ok(()) + value_id } - fn reject_subroutine_metadata( - &self, - annotations: &[hir::Annotation], - ) -> Result<(), IntegrationError> { - for annotation in annotations { - match annotation.name.as_str() { - "Name" | "SuppressWarnings" => {} - _ => { - return Err(self.unsupported( - format!( - "subroutine annotation '{}' is not representable in canonical WIR", - annotation.name - ), - annotation.span, - )); - } - } + fn normalize_contextual_arguments( + &mut self, + call_id: &str, + mut args: Vec, + ) -> Vec { + let mut index = 0; + while index < args.len() { + args[index] = self.normalize_contextual_argument(call_id, index, args[index]); + index += 1; } - Ok(()) + args } - fn subroutine_rule_name(&self, generated_name: &str) -> String { - if self.hir.preprocessing.rule_prefix_template.is_some() { - generated_name.to_string() - } else { - format!("Subroutine {generated_name}") - } + fn lower_custom_string(&mut self, value: String) -> ValueId { + let text = self.push_value(Value::String(value)); + self.push_call("customString", vec![text]) } - fn global_initializer_rule_name(&self) -> String { - directive_value(self.hir, "globalvarInitRuleName") - .map(str::to_string) - .unwrap_or_else(|| { - crate::lower::render_generated_rule_name( - "Initialize global variables", - &self.hir.preprocessing, - ) + fn fold_format_constants<'b>( + &self, + text: &str, + args: &'b [hir::Expr], + ) -> (String, Vec<&'b hir::Expr>) { + let values = args + .iter() + .map(|arg| { + let mut stack = Vec::new(); + crate::compile_time::evaluate(arg, &self.constants, &HashMap::new(), &mut stack) + .and_then(compile_time_value_text) }) - } - - fn player_initializer_rule_name(&self) -> String { - directive_value(self.hir, "playervarInitRuleName") - .map(str::to_string) - .unwrap_or_else(|| "Initialize player variables".to_string()) - } + .collect::>(); + let dynamic_indexes = values + .iter() + .enumerate() + .filter_map(|(index, value)| value.is_none().then_some(index)) + .collect::>(); + let dynamic_args = dynamic_indexes + .iter() + .map(|index| &args[*index]) + .collect::>(); + let dynamic_position = dynamic_indexes + .iter() + .enumerate() + .map(|(position, index)| (*index, position)) + .collect::>(); - fn lower_event( - &self, - event: &hir::Event, - annotations: &[hir::Annotation], - ) -> Result { - if !event.args.is_empty() { - return Err(self.unsupported( - "event arguments are not representable in canonical WIR; use structural event filters", - event.span, - )); - } - let team = self.lower_event_team(annotations)?; - let target = self.lower_event_target(annotations)?; - let has_filters = !matches!(team, EventTeam::All) || !matches!(target, EventTarget::All); - match event.name.as_str() { - "global" => { - if has_filters { - return Err( - self.unsupported("global events cannot have player filters", event.span) - ); - } - Ok(Event::Global) - } - "eachPlayer" => { - if has_filters { - Ok(Event::EachPlayerWithFilters { team, target }) - } else { - Ok(Event::EachPlayer) - } + let canonical = canonical_format_text(text); + let mut output = String::with_capacity(canonical.len()); + let mut cursor = 0; + while cursor < canonical.len() { + let Some(open_rel) = canonical[cursor..].find('{') else { + output.push_str(&canonical[cursor..]); + break; + }; + let open = cursor + open_rel; + output.push_str(&canonical[cursor..open]); + let Some(close_rel) = canonical[open + 1..].find('}') else { + output.push_str(&canonical[open..]); + break; + }; + let close = open + 1 + close_rel; + let marker = &canonical[open + 1..close]; + let Ok(index) = marker.parse::() else { + output.push_str(&canonical[open..=close]); + cursor = close + 1; + continue; + }; + if let Some(Some(value)) = values.get(index) { + output.push_str(value); + } else if let Some(position) = dynamic_position.get(&index) { + output.push('{'); + output.push_str(&position.to_string()); + output.push('}'); + } else { + output.push_str(&canonical[open..=close]); } - name => player_event_kind(name).map_or_else( - || { - Err(self.unsupported( - format!("event '{name}' is not supported by canonical WIR"), - event.span, - )) - }, - |kind| Ok(Event::Player { kind, team, target }), - ), + cursor = close + 1; } + (output, dynamic_args) } - fn lower_event_team( - &self, - annotations: &[hir::Annotation], - ) -> Result { - let team_annotations = annotations - .iter() - .filter(|annotation| annotation.name == "Team") - .collect::>(); - if team_annotations.len() > 1 { - return Err(self.unsupported( - "an event cannot have multiple @Team filters", - team_annotations[1].span.or(team_annotations[0].span), - )); - } - let Some(annotation) = team_annotations.first() else { - return Ok(EventTeam::All); - }; - let argument = annotation - .args - .first() - .ok_or_else(|| self.unsupported("@Team requires one filter value", annotation.span))?; - if annotation.args.len() != 1 { - return Err( - self.unsupported("@Team requires exactly one filter value", annotation.span) - ); - } - let spelling = match argument.text.as_str() { - "1" => "Team 1", - "2" => "Team 2", - value => value, - }; - let (_, member) = self - .compiler - .catalog - .resolve_enum_member("EventTeam", &Locale::new("en-US"), spelling) - .ok_or_else(|| { - self.unsupported( - format!("unknown EventTeam filter '{spelling}'"), - argument.span.or(annotation.span), - ) - })?; - match member.as_str() { - "ALL" => Ok(EventTeam::All), - "TEAM_1" => Ok(EventTeam::Team1), - "TEAM_2" => Ok(EventTeam::Team2), - _ => Err(self.unsupported( - format!("catalog EventTeam member '{member}' is not supported by canonical WIR"), - argument.span.or(annotation.span), - )), - } + fn push_number(&mut self, value: f64) -> ValueId { + self.push_value(Value::Number(value)) } - fn lower_event_target( - &self, - annotations: &[hir::Annotation], - ) -> Result { - let mut filters = Vec::new(); - for name in ["Slot", "Hero"] { - let matches = annotations - .iter() - .filter(|annotation| annotation.name == name) - .collect::>(); - if matches.len() > 1 { - return Err(self.unsupported( - format!("an event cannot have multiple @{name} filters"), - matches[1].span.or(matches[0].span), - )); - } - filters.extend(matches); - } - if filters.len() > 1 { - return Err(self.unsupported( - "an event cannot combine @Slot and @Hero filters", - filters[1].span.or(filters[0].span), - )); - } - let Some(annotation) = filters.first() else { - return Ok(EventTarget::All); - }; - let argument = annotation.args.first().ok_or_else(|| { - self.unsupported( - format!("@{} requires one filter value", annotation.name), - annotation.span, - ) - })?; - if annotation.args.len() != 1 { - return Err(self.unsupported( - format!("@{} requires exactly one filter value", annotation.name), - annotation.span, - )); - } - let spelling = if annotation.name == "Slot" { - match argument.text.as_str() { - value if value.parse::().is_ok() => { - format!("Slot {}", value.parse::().unwrap_or_default()) - } - value => value.to_string(), - } - } else { - argument.text.clone() - }; - let domain = if annotation.name == "Slot" { - "EventPlayer" - } else { - "Hero" - }; - let locale = Locale::new("en-US"); - let catalog_spelling = match (domain, spelling.as_str()) { - ("Hero", "mccree") => "CASSIDY", - ("Hero", "hammond") => "WRECKING_BALL", - ("Hero", "soldier") => "SOLDIER_76", - ("Hero", "domina") => "JINYU", - ("Hero", "dmon") => "D_MON", - _ => spelling.as_str(), + fn canonical_vector_member(&self, x: ValueId, y: ValueId, z: ValueId) -> Option<&'static str> { + let number = |id| match self.values.get(id)? { + Value::Number(value) => Some(*value), + _ => None, }; - let member = self - .compiler - .catalog - .resolve_enum_member(domain, &locale, catalog_spelling) - .map(|(_, member)| member) - .or_else(|| { - (domain == "Hero") - .then(|| { - self.compiler - .catalog - .enum_domain(domain) - .and_then(|domain| { - domain - .members - .iter() - .find(|member| { - member.member.eq_ignore_ascii_case(catalog_spelling) - || member.spellings(&locale).iter().any(|candidate| { - candidate.eq_ignore_ascii_case(catalog_spelling) - }) - || member - .member - .chars() - .filter(|c| c.is_ascii_alphanumeric()) - .collect::() - .eq_ignore_ascii_case( - &catalog_spelling - .chars() - .filter(|c| c.is_ascii_alphanumeric()) - .collect::(), - ) - }) - .map(|member| member.member.clone()) - }) - }) - .flatten() - }) - .ok_or_else(|| { - self.unsupported( - format!("unknown {domain} filter '{spelling}'"), - argument.span.or(annotation.span), - ) - })?; - if domain == "EventPlayer" { - if member == "ALL" { - Ok(EventTarget::All) - } else if let Some(slot) = member.strip_prefix("SLOT_") { - let slot = slot.parse::().map_err(|_| { - self.unsupported( - format!("catalog EventPlayer member '{member}' is not a slot"), - argument.span.or(annotation.span), - ) - })?; - Ok(EventTarget::Slot(slot)) - } else { - Err(self.unsupported( - format!( - "catalog EventPlayer member '{member}' is not supported by canonical WIR" - ), - argument.span.or(annotation.span), - )) - } - } else { - Ok(EventTarget::Hero(member)) + match (number(x), number(y), number(z)) { + (Some(1.0), Some(0.0), Some(0.0)) => Some("LEFT"), + (Some(-1.0), Some(0.0), Some(0.0)) => Some("RIGHT"), + (Some(0.0), Some(1.0), Some(0.0)) => Some("UP"), + (Some(0.0), Some(-1.0), Some(0.0)) => Some("DOWN"), + (Some(0.0), Some(0.0), Some(1.0)) => Some("FORWARD"), + (Some(0.0), Some(0.0), Some(-1.0)) => Some("BACKWARD"), + _ => None, } } - fn lower_actions( - &mut self, - statements: &[Stmt], - break_target: Option, - ) -> Result, IntegrationError> { - self.visible_labels.push( - statements - .iter() - .filter_map(|statement| match statement { - Stmt::Label { name, .. } => Some(name.clone()), - _ => None, - }) - .collect(), - ); - let mut actions = Vec::new(); - let mut labels = HashMap::new(); - let mut gotos = Vec::new(); - let mut index = 0; - while index < statements.len() { - let statement = &statements[index]; - let optimization = self.optimization_state_at(statement.span()); - if optimization.enabled - && optimization.for_size - && optimization.for_size_aggressive - && index + 1 == statements.len() - && let Stmt::If { - branches, - r#else: None, - span, - } = statement - && branches.len() == 1 - { - let branch = &branches[0]; - let is_not_condition = matches!( - &*branch.condition, - Expr::Unary { op, .. } if op == "not" - ); - let is_comparison = matches!( - &*branch.condition, - Expr::Binary { op, .. } - if matches!(op.as_str(), "==" | "!=" | "<" | "<=" | ">" | ">=") - ); - if (is_not_condition || is_comparison) && !branch.body.is_empty() { - let body = self.lower_actions(&branch.body, break_target)?; - if !body.is_empty() && (is_not_condition || branch.body.len() == 1) { - let condition = if is_not_condition { - let Expr::Unary { operand, .. } = &*branch.condition else { - unreachable!() - }; - self.lower_value(operand)? - } else { - let condition = self.lower_value(&branch.condition)?; - self.push_call("not", vec![condition]) - }; - let distance = self.canonical_action_width(&body, *span)?; - let distance = self.push_number(distance as f64); - let skip = self.push_call_action("skipIf", &[condition, distance]); - self.mark_action_origins(std::slice::from_ref(&skip), *span); - actions.push(skip); - actions.extend(body); - index += 1; - continue; - } - } - } - if let Some((condition, label, span)) = direct_conditional_goto(statement) - && self - .visible_labels - .iter() - .any(|labels| labels.iter().any(|candidate| candidate == label)) - { - let condition = self.lower_value(condition)?; - let placeholder = self.push_number(0.0); - let skip = self.push_call_action("skipIf", &[condition, placeholder]); - self.mark_action_origins(std::slice::from_ref(&skip), span); - actions.push(skip); - self.deferred_gotos.push((skip, label.to_string(), span, 1)); - index += 1; - continue; - } - if let Some((condition, offset, span)) = direct_conditional_dynamic_goto(statement) { - let condition = self.lower_value(condition)?; - let offset = self.lower_value(offset)?; - let skip = self.push_call_action("skipIf", &[condition, offset]); - self.mark_action_origins(std::slice::from_ref(&skip), span); - actions.push(skip); - let goto = self.push_call_action("skip", &[offset]); - self.mark_action_origins(std::slice::from_ref(&goto), span); - actions.push(goto); - actions.push(self.push_call_action("disabledAbort", &[])); - index += 1; - continue; - } - match statement { - Stmt::Label { name, .. } => { - self.resolve_deferred_gotos(&actions, name, actions.len())?; - labels.insert(name.clone(), actions.len()); - } - Stmt::Goto { - label, - offset, - rule_start, - span, - } => { - if *rule_start { - let loop_action = self.push_call_action("loop", &[]); - self.mark_action_origins(std::slice::from_ref(&loop_action), *span); - actions.push(loop_action); - index += 1; - continue; - } - let placeholder = self.push_number(0.0); - let action = self.push_call_action("skip", &[placeholder]); - self.mark_action_origins(std::slice::from_ref(&action), *span); - let position = actions.len(); - actions.push(action); - gotos.push(( - action, - position, - label.clone(), - offset.clone(), - span.map(Into::into), - )); - } - _ => actions.extend(self.lower_action(statement, break_target)?), - } - index += 1; - } - for (action, position, label, offset, span) in gotos { - let distance = if let Some(offset) = offset { - self.lower_value(&offset)? - } else { - let Some(label) = label else { - return Err(self.unsupported("goto is missing a label or offset", span)); - }; - let Some(&target) = labels.get(&label) else { - if self - .visible_labels - .iter() - .any(|labels| labels.contains(&label)) - { - self.deferred_gotos.push((action, label, span, 0)); - continue; - } - return Err(self.unsupported(format!("unknown goto label '{label}'"), span)); - }; - if target < position { - return Err(self - .unsupported("backward goto is not representable in canonical WIR", span)); - } - let width = self.canonical_action_width(&actions[position + 1..target], span)?; - self.push_number(width as f64) - }; - let Some(Action::Call { args, .. }) = self.actions.get_mut(action) else { - unreachable!("goto placeholder must be a call action") - }; - args[0] = distance; - } - if self.visible_labels.len() == 1 && !self.deferred_gotos.is_empty() { - let (_, label, span, _) = self.deferred_gotos.remove(0); - return Err(self.unsupported(format!("unknown goto label '{label}'"), span)); - } - self.visible_labels.pop(); - Ok(actions) - } - - /// `if condition: return` and `if condition: loop()` lower to a single - /// conditional action; with optimization a constant condition removes the - /// condition entirely. - fn lower_terminal_if( - &mut self, - branch: &hir::types::IfBranch, - span: Option, - ) -> Result>, IntegrationError> { - let [child] = branch.body.as_slice() else { - return Ok(None); - }; - let is_loop_call = matches!( - child, - Stmt::Expr { expr, .. } - if matches!(expr.as_ref(), Expr::Call { name, args, .. } if name == "loop" && args.is_empty()) - ); - let (unconditional, conditional, on_true, on_false) = match child { - Stmt::Return { .. } => ( - "abort", - "abortIf", - "__abortIfConditionIsTrue__", - "__abortIfConditionIsFalse__", - ), - Stmt::Goto { - rule_start: true, .. - } => ( - "loop", - "loopIf", - "loopIfConditionIsTrue", - "__loopIfConditionIsFalse__", - ), - _ if is_loop_call => ( - "loop", - "loopIf", - "loopIfConditionIsTrue", - "__loopIfConditionIsFalse__", - ), - _ => return Ok(None), - }; - let is_rule_condition = - |expr: &Expr| matches!(expr, Expr::Call { name, .. } if name == "ruleCondition"); - let rule_condition = match branch.condition.as_ref() { - condition if is_rule_condition(condition) => Some(on_true), - Expr::Unary { op, operand, .. } - if op == "not" && is_rule_condition(operand.as_ref()) => - { - Some(on_false) - } - _ => None, - }; - if let Some(name) = rule_condition { - return Ok(Some(vec![self.push_call_action(name, &[])])); - } - let optimization = self.optimization_state_at(span.as_ref()); - if !optimization.enabled { - return Ok(None); - } - let condition = self.lower_value(&branch.condition)?; - let materialized = self.materialize_value(condition); - let operators = OperatorOptimizer::new(self.compiler, optimization.strict); - let action = match operators.constant_truth(&materialized) { - Some(false) => return Ok(Some(Vec::new())), - Some(true) => self.push_call_action(unconditional, &[]), - None => self.push_call_action(conditional, &[condition]), - }; - self.mark_action_origins(std::slice::from_ref(&action), span); - Ok(Some(vec![action])) - } - - fn resolve_deferred_gotos( - &mut self, - actions: &[ActionId], - label: &str, - target: usize, - ) -> Result<(), IntegrationError> { - let deferred = std::mem::take(&mut self.deferred_gotos); - let mut remaining = Vec::new(); - for (action, deferred_label, span, argument) in deferred { - if deferred_label != label { - remaining.push((action, deferred_label, span, argument)); - continue; - } - let Some(position) = actions.iter().position(|candidate| *candidate == action) else { - remaining.push((action, deferred_label, span, argument)); - continue; - }; - if target < position { - return Err( - self.unsupported("backward goto is not representable in canonical WIR", span) - ); - } - // A deferred goto may sit inside a structured action, so this - // slice is not necessarily a standalone valid action sequence. - // The flat lowering stream has one action id per native action, - // including the structural markers that the jump must cross. - // Instructions dropped from the output must not widen the jump. - let width = self.useful_actions(&actions[position + 1..target]).len(); - let distance = self.push_number(width as f64); - let Some(Action::Call { args, .. }) = self.actions.get_mut(action) else { - unreachable!("deferred goto placeholder must be a call action") - }; - args[argument] = distance; - } - self.deferred_gotos = remaining; - Ok(()) - } - - fn lower_action( - &mut self, - stmt: &Stmt, - break_target: Option, - ) -> Result, IntegrationError> { - let result = match stmt { - Stmt::Pass { .. } => Ok(Vec::new()), - Stmt::Assign { - target, - value, - span, - } => self.lower_assign(target, value, *span).map(|action| vec![action]), - Stmt::If { - branches, - r#else, - span, - } => { - if let ([branch], None) = (branches.as_slice(), r#else) - && let Some(actions) = self.lower_terminal_if(branch, *span)? - { - return Ok(actions); - } - let branches = branches - .iter() - .map(|branch| { - Ok(( - self.lower_value(&branch.condition)?, - self.lower_actions(&branch.body, break_target)?, - )) - }) - .collect::, IntegrationError>>()?; - let else_body = r#else - .as_ref() - .map(|body| self.lower_actions(body, break_target)) - .transpose()?; - Ok(self.push_if_actions(branches, else_body)) - } - Stmt::For { - variable, - iterable, - body, - span: _, - } => { - let (start, stop, step) = self.lower_range(iterable)?; - let body = self.lower_loop_body(body)?; - match variable.as_ref() { - Expr::GlobalVar { - name, - span: target_span, - } => { - let variable_id = *self.globals.get(name).ok_or_else(|| { - self.unsupported( - format!("unknown global variable '{name}'"), - *target_span, - ) - })?; - Ok(self.push_for_global_actions(variable_id, start, stop, step, body)) - } - Expr::PlayerVar { - player, - name, - span: target_span, - .. - } => { - let variable_id = *self.players.get(name).ok_or_else(|| { - self.unsupported( - format!("unknown player variable '{name}'"), - *target_span, - ) - })?; - let player = self.lower_value(player)?; - Ok(self.push_for_player_actions( - player, variable_id, start, stop, step, body, - )) - } - _ => Err(self.unsupported( - "range loops require a global- or player-variable binder in canonical WIR", - variable.span().copied(), - )), - } - } - Stmt::While { - condition, - body, - span: _, - } => { - let condition = self.lower_value(condition)?; - let body = self.lower_loop_body(body)?; - Ok(self.push_while_actions(condition, body)) - } - Stmt::DoWhile { - condition, - body, - span: _, - } => { - let body = self.lower_do_while_body(body)?; - let condition = self.lower_value(condition)?; - let loop_if = self.push_call_action("loopIf", &[condition]); - // OverPy's pinned lowering expands do/while into its body - // followed by the canonical Loop If action. - let mut actions = body; - actions.push(loop_if); - Ok(actions) - } - Stmt::Switch { - value, - arms, - span, - } => self.lower_switch(value, arms, *span, break_target, None, false), - Stmt::Delete { target, span } => self.lower_delete(target, *span).map(|action| vec![action]), - Stmt::Continue { span } => Err(self.unsupported( - "continue statements are only lowered while constructing a loop body", - *span, - )), - Stmt::Goto { - label, - offset, - rule_start, - span, - } => { - if *rule_start { - Ok(vec![self.push_call_action("loop", &[])]) - } else if label.is_none() { - let offset = offset.as_ref().ok_or_else(|| { - self.unsupported("goto is missing a label or offset", *span) - })?; - let offset = self.lower_value(offset)?; - Ok(vec![self.push_call_action("skip", &[offset])]) - } else { - Err(self.unsupported( - "goto statements are not representable in canonical WIR", - *span, - )) - } - } - Stmt::Label { span, .. } => Err(self.unsupported( - "labels are not representable in canonical WIR", - *span, - )), - Stmt::Break { span } => match break_target { - Some(BreakTarget::Loop) => Ok(vec![self.push_call_action("break", &[])]), - Some(BreakTarget::DoWhile) => Err(self.unsupported( - "break inside a do-while must be a direct statement or a single conditional break", - *span, - )), - Some(BreakTarget::Switch) => Ok(vec![self.push_action(Action::Else)]), - None => Err(self.unsupported( - "break has no enclosing canonical loop or switch", - *span, - )), - }, - Stmt::Return { span: _ } => Ok(vec![self.push_call_action("abort", &[])]), - Stmt::Expr { expr, span } => match expr.as_ref() { - Expr::Call { - name, - args, - debug_source, - .. - } => { - if name == "disableInspector" && args.is_empty() { - Ok(vec![self.push_call_action("disableInspector", &[])]) - } else if name == "pass" && args.is_empty() { - Ok(Vec::new()) - } else if name == "debug" && args.len() == 1 { - Ok(vec![self.lower_debug(&args[0], debug_source.as_deref())?]) - } else if name == "print" && args.len() == 1 { - Ok(vec![self.lower_print(&args[0], *span)?]) - } else if name == "createCasedProgressBarIwt" { - self.lower_cased_progress_bar(args, *span) - } else { - self.lower_action_call(name, args, *span).map(|action| vec![action]) - } - } - Expr::ReceiverCall { - receiver, - name, - args, - span: call_span, - } => self - .lower_receiver_action_call(receiver, name, args, *call_span) - .map(|action| vec![action]), - _ => Err(self.unsupported( - "only action calls are currently representable as expression statements in canonical WIR", - *span, - )), - }, - Stmt::CallSubroutine { name, span } => { - let subroutine = *self.subroutines.get(name).ok_or_else(|| { - self.unsupported(format!("unknown subroutine '{name}'"), *span) - })?; - Ok(vec![self.push_action(Action::CallSubroutine { - subroutine: self.subroutine_names[subroutine].clone(), - })]) - } - }; - if let Ok(actions) = &result { - self.mark_action_origins(actions, stmt.span().copied()); - self.mark_statement_argument_origins(stmt, actions); - } - result - } - - fn mark_statement_argument_origins(&mut self, statement: &Stmt, actions: &[ActionId]) { - match statement { - Stmt::Assign { target, value, .. } => { - let (target_span, index_span) = match &**target { - Expr::Index { array, index, .. } => { - (array.span().copied(), index.span().copied()) - } - _ => (target.span().copied(), None), - }; - let value_span = value.span().copied(); - let modified_value_span = match &**value { - Expr::Binary { right, .. } => right.span().copied(), - _ => value_span, - }; - for action in actions { - let spans = match self.actions.get(*action) { - Some(Action::SetGlobalVariable { .. }) => vec![value_span], - Some(Action::ModifyGlobalVariable { .. }) => vec![modified_value_span], - Some(Action::SetPlayerVariable { .. }) => { - let player_span = match &**target { - Expr::PlayerVar { player, .. } => player.span().copied(), - _ => None, - }; - vec![player_span, value_span] - } - Some(Action::ModifyPlayerVariable { .. }) => { - let player_span = match &**target { - Expr::PlayerVar { player, .. } => player.span().copied(), - _ => None, - }; - vec![player_span, modified_value_span] - } - Some(Action::Call { name, .. }) - if name == "setGlobalVariableAtIndex" - || name == "setPlayerVariableAtIndex" => - { - vec![target_span, index_span, value_span] - } - Some(Action::Call { name, .. }) - if name == "modifyGlobalVariableAtIndex" - || name == "modifyPlayerVariableAtIndex" => - { - vec![target_span, index_span, None, modified_value_span] - } - _ => continue, - }; - self.mark_action_argument_origins(*action, spans); - } - } - Stmt::If { branches, .. } => { - let mut depth = 0usize; - let mut branch = 0usize; - for action in actions { - match self.actions.get(*action) { - Some(Action::If { .. }) => { - if depth == 0 { - self.mark_action_argument_origins( - *action, - [branches - .first() - .and_then(|branch| branch.condition.span().copied())], - ); - } - depth += 1; - } - Some(Action::ElseIf { .. }) if depth == 1 => { - branch += 1; - self.mark_action_argument_origins( - *action, - [branches - .get(branch) - .and_then(|branch| branch.condition.span().copied())], - ); - } - Some(Action::End) => depth = depth.saturating_sub(1), - _ => {} - } - } - } - Stmt::While { condition, .. } => { - if let Some(action) = actions.first() { - if matches!(self.actions.get(*action), Some(Action::While { .. })) { - self.mark_action_argument_origins(*action, [condition.span().copied()]); - } - } - } - Stmt::For { - variable, iterable, .. - } => { - let range_spans = match &**iterable { - Expr::Call { args, .. } => match args.as_slice() { - [stop] => vec![None, stop.span().copied(), None], - [start, stop] => { - vec![start.span().copied(), stop.span().copied(), None] - } - [start, stop, step] => vec![ - start.span().copied(), - stop.span().copied(), - step.span().copied(), - ], - _ => return, - }, - _ => return, - }; - let spans = match &**variable { - Expr::PlayerVar { player, .. } => std::iter::once(player.span().copied()) - .chain(range_spans) - .collect::>(), - _ => range_spans, - }; - if let Some(action) = actions.first() { - if matches!( - self.actions.get(*action), - Some(Action::ForGlobalVariable { .. }) - | Some(Action::ForPlayerVariable { .. }) - ) { - self.mark_action_argument_origins(*action, spans); - } - } - } - Stmt::DoWhile { condition, .. } => { - if let Some(action) = actions.last() { - if matches!( - self.actions.get(*action), - Some(Action::Call { name, .. }) if name == "loopIf" - ) { - self.mark_action_argument_origins(*action, [condition.span().copied()]); - } - } - } - Stmt::Delete { target, .. } => { - let mut indices = Vec::new(); - let _ = indexed_target_parts(target, &mut indices); - indices.reverse(); - for action in actions { - let spans = match self.actions.get(*action) { - Some(Action::SetGlobalVariable { .. }) - | Some(Action::ModifyGlobalVariable { .. }) => { - vec![target.span().copied()] - } - Some(Action::SetPlayerVariable { .. }) - | Some(Action::ModifyPlayerVariable { .. }) => { - vec![None, target.span().copied()] - } - Some(Action::Call { name, .. }) - if name == "setGlobalVariableAtIndex" - || name == "setPlayerVariableAtIndex" => - { - vec![ - target.span().copied(), - indices.first().and_then(|index| index.span().copied()), - target.span().copied(), - ] - } - Some(Action::Call { name, .. }) - if name == "modifyGlobalVariableAtIndex" - || name == "modifyPlayerVariableAtIndex" => - { - vec![ - target.span().copied(), - indices.first().and_then(|index| index.span().copied()), - None, - indices.last().and_then(|index| index.span().copied()), - ] - } - _ => continue, - }; - self.mark_action_argument_origins(*action, spans); - } - } - _ => {} - } - } - - fn lower_loop_body(&mut self, statements: &[Stmt]) -> Result, IntegrationError> { - self.lower_loop_sequence_with_break_target(statements, &[], 0, BreakTarget::Loop) - } - - fn lower_loop_sequence_with_break_target( - &mut self, - statements: &[Stmt], - after: &[ActionId], - structural_after: usize, - break_target: BreakTarget, - ) -> Result, IntegrationError> { - let mut actions = Vec::new(); - let mut index = 0; - while index < statements.len() { - let statement = &statements[index]; - let tail = &statements[index + 1..]; - if let Some(conditions) = pure_continue_conditions(statement) { - let tail = self.lower_loop_sequence_with_break_target( - tail, - after, - structural_after, - break_target, - )?; - let distance = self.canonical_action_width(&tail, statement.span().copied())? - + structural_after - + self.canonical_action_width(after, statement.span().copied())?; - if distance > 0 { - let mut args = Vec::with_capacity(conditions.len() + 1); - if let Some((first, rest)) = conditions.split_first() { - let mut condition = self.lower_value(first)?; - for expression in rest { - let right = self.lower_value(expression)?; - condition = self.push_call("and", vec![condition, right]); - } - args.push(condition); - } - let distance = self.push_number(distance as f64); - args.push(distance); - let skip = self.push_call_action( - if conditions.is_empty() { - "skip" - } else { - "skipIf" - }, - &args, - ); - self.mark_action_origins( - std::slice::from_ref(&skip), - statement.span().copied(), - ); - actions.push(skip); - } - actions.extend(tail); - return Ok(actions); - } - if contains_loop_continue(statement) { - let tail = self.lower_loop_sequence_with_break_target( - tail, - after, - structural_after, - break_target, - )?; - let mut continuation_after = tail.clone(); - continuation_after.extend_from_slice(after); - let lowered = if let Stmt::Switch { value, arms, span } = statement { - self.lower_switch( - value, - arms, - *span, - Some(break_target), - Some((&continuation_after, structural_after)), - false, - )? - } else { - self.lower_if_with_loop_continue( - statement, - &continuation_after, - structural_after, - break_target, - )? - }; - self.mark_action_origins(&lowered, statement.span().copied()); - actions.extend(lowered); - actions.extend(tail); - return Ok(actions); - } - if let Some((conditions, label)) = pure_goto_conditions(statement) { - if let Some(target) = statements[index + 1..] - .iter() - .position( - |candidate| matches!(candidate, Stmt::Label { name, .. } if name == label), - ) - .map(|offset| index + 1 + offset) - { - let middle = self.lower_loop_sequence_with_break_target( - &statements[index + 1..target], - after, - structural_after, - break_target, - )?; - let suffix = self.lower_loop_sequence_with_break_target( - &statements[target + 1..], - after, - structural_after, - break_target, - )?; - let distance = - self.canonical_action_width(&middle, statement.span().copied())?; - let mut args = Vec::with_capacity(conditions.len() + 1); - if let Some((first, rest)) = conditions.split_first() { - let mut condition = self.lower_value(first)?; - for expression in rest { - let right = self.lower_value(expression)?; - condition = self.push_call("and", vec![condition, right]); - } - args.push(condition); - } - args.push(self.push_number(distance as f64)); - let skip = self.push_call_action( - if conditions.is_empty() { - "skip" - } else { - "skipIf" - }, - &args, - ); - self.mark_action_origins( - std::slice::from_ref(&skip), - statement.span().copied(), - ); - actions.push(skip); - actions.extend(middle); - actions.extend(suffix); - return Ok(actions); - } - } - if let Some((condition, label, span)) = direct_conditional_goto(statement) - && self - .visible_labels - .iter() - .any(|labels| labels.iter().any(|candidate| candidate == label)) - { - let condition = self.lower_value(condition)?; - let placeholder = self.push_number(0.0); - let skip = self.push_call_action("skipIf", &[condition, placeholder]); - self.mark_action_origins(std::slice::from_ref(&skip), span); - actions.push(skip); - self.deferred_gotos.push((skip, label.to_string(), span, 1)); - index += 1; - continue; - } - if matches!(statement, Stmt::Label { .. }) { - index += 1; - continue; - } - actions.extend(self.lower_action(statement, Some(break_target))?); - index += 1; - } - Ok(actions) - } - - fn lower_if_with_loop_continue( - &mut self, - statement: &Stmt, - after: &[ActionId], - structural_after: usize, - break_target: BreakTarget, - ) -> Result, IntegrationError> { - let Stmt::If { - branches, - r#else, - span: _, - } = statement - else { - unreachable!("continue-containing loop statement must be an if") - }; - let mut lowered_branches = Vec::with_capacity(branches.len()); - let mut suffix = after.to_vec(); - let mut suffix_structural = structural_after + 1; - let mut lowered_else = None; - if let Some(body) = r#else { - let body = self.lower_loop_sequence_with_break_target( - body, - after, - suffix_structural, - break_target, - )?; - suffix.splice(0..0, body.iter().copied()); - suffix_structural += 1; - lowered_else = Some(body); - } - for index in (0..branches.len()).rev() { - let body = self.lower_loop_sequence_with_break_target( - &branches[index].body, - &suffix, - suffix_structural, - break_target, - )?; - suffix_structural += 1; - suffix.splice(0..0, body.iter().copied()); - lowered_branches.push(body); - } - lowered_branches.reverse(); - let mut branch_actions = Vec::with_capacity(branches.len()); - for (branch, body) in branches.iter().zip(lowered_branches) { - branch_actions.push((self.lower_value(&branch.condition)?, body)); - } - Ok(self.push_if_actions(branch_actions, lowered_else)) - } - - fn lower_do_while_body( - &mut self, - statements: &[Stmt], - ) -> Result, IntegrationError> { - let mut actions = Vec::new(); - for (index, statement) in statements.iter().enumerate() { - if let Some(conditions) = pure_continue_conditions(statement) { - let tail = self.lower_do_while_body(&statements[index + 1..])?; - let mut condition = None; - for expression in conditions { - let value = self.lower_value(expression)?; - condition = Some(match condition { - Some(left) => self.push_call("and", vec![left, value]), - None => value, - }); - } - let action = if let Some(condition) = condition { - self.push_call_action("loopIf", &[condition]) - } else { - self.push_call_action("loop", &[]) - }; - self.mark_action_origins(std::slice::from_ref(&action), statement.span().copied()); - actions.push(action); - actions.extend(tail); - return Ok(actions); - } - if contains_loop_continue(statement) { - if let Stmt::Switch { value, arms, span } = statement { - let lowered = self.lower_switch( - value, - arms, - *span, - Some(BreakTarget::DoWhile), - None, - true, - )?; - self.mark_action_origins(&lowered, statement.span().copied()); - actions.extend(lowered); - continue; - } - let Stmt::If { - branches, r#else, .. - } = statement - else { - unreachable!("continue-containing do-while statement must be an if") - }; - let branches = branches - .iter() - .map(|branch| { - Ok(( - self.lower_value(&branch.condition)?, - self.lower_do_while_body(&branch.body)?, - )) - }) - .collect::, IntegrationError>>()?; - let else_body = r#else - .as_ref() - .map(|body| self.lower_do_while_body(body)) - .transpose()?; - let lowered = self.push_if_actions(branches, else_body); - self.mark_action_origins(&lowered, statement.span().copied()); - actions.extend(lowered); - continue; - } - let direct_break = matches!(statement, Stmt::Break { .. }); - let conditional_break = match statement { - Stmt::If { - branches, - r#else: None, - .. - } if branches.len() == 1 => { - matches!(branches[0].body.as_slice(), [Stmt::Break { .. }]) - } - _ => false, - }; - - if direct_break || conditional_break { - let tail = self.lower_do_while_body(&statements[index + 1..])?; - let distance = self.canonical_action_width(&tail, statement.span().copied())? + 1; - let (name, args, _span) = if let Stmt::Break { span } = statement { - ("skip", Vec::new(), *span) - } else if let Stmt::If { branches, span, .. } = statement { - ( - "skipIf", - vec![self.lower_value(&branches[0].condition)?], - *span, - ) - } else { - unreachable!("break shape was checked above") - }; - let distance = self.push_number(distance as f64); - let mut args = args; - args.push(distance); - let skip = self.push_call_action(name, &args); - self.mark_action_origins(std::slice::from_ref(&skip), statement.span().copied()); - actions.push(skip); - actions.extend(tail); - return Ok(actions); - } - - actions.extend(self.lower_action(statement, Some(BreakTarget::DoWhile))?); - } - Ok(actions) - } - - fn lower_range( - &mut self, - iterable: &Expr, - ) -> Result<(ValueId, ValueId, ValueId), IntegrationError> { - let Expr::Call { name, args, .. } = iterable else { - return Err(self.unsupported( - "range loop iterable must be a range(...) call", - iterable.span().copied(), - )); - }; - if name != "range" || !(1..=3).contains(&args.len()) { - return Err(self.unsupported( - "range loop requires one to three arguments", - iterable.span().copied(), - )); - } - let number = |this: &mut Self, value: f64| this.push_number(value); - match args.as_slice() { - [stop] => Ok(( - number(self, 0.0), - self.lower_value(stop)?, - number(self, 1.0), - )), - [start, stop] => Ok(( - self.lower_value(start)?, - self.lower_value(stop)?, - number(self, 1.0), - )), - [start, stop, step] => Ok(( - self.lower_value(start)?, - self.lower_value(stop)?, - self.lower_value(step)?, - )), - _ => unreachable!("range arity checked above"), - } - } - - fn lower_switch( - &mut self, - value: &Expr, - arms: &[SwitchArm], - span: Option, - break_target: Option, - loop_continue: Option<(&[ActionId], usize)>, - do_while_continue: bool, - ) -> Result, IntegrationError> { - if break_target.is_none() - && arms.iter().all(|arm| match arm { - SwitchArm::Case { body, .. } | SwitchArm::Default { body, .. } => { - switch_body_is_noop(body) - } - }) - { - return Ok(Vec::new()); - } - let selector = self.lower_value(value)?; - let mut case_values = Vec::new(); - let mut lowered_arms = Vec::with_capacity(arms.len()); - let mut has_default = false; - let mut legacy_case_offsets = Vec::new(); - let mut legacy_offset = 0; - let mut legacy_default_offset = None; - - let mut reverse_bodies = (loop_continue.is_some() && !do_while_continue).then(|| { - (0..arms.len()) - .map(|_| None) - .collect::>>() - }); - if let Some((outer_after, structural_after)) = loop_continue { - let mut future = Vec::new(); - for index in (0..arms.len()).rev() { - let body = match &arms[index] { - SwitchArm::Case { body, .. } | SwitchArm::Default { body, .. } => body, - }; - let mut after = future.clone(); - after.extend_from_slice(outer_after); - let lowered = self.lower_switch_body( - body, - Some((&after, structural_after)), - do_while_continue, - )?; - let mut next_future = lowered.0.clone(); - next_future.extend_from_slice(&future); - future = next_future; - reverse_bodies.as_mut().unwrap()[index] = Some(lowered); - } - } - for (index, arm) in arms.iter().enumerate() { - let (value, (body, break_at)) = match arm { - SwitchArm::Case { value, body, .. } => { - case_values.push(self.lower_value(value)?); - let lowered = if let Some(bodies) = reverse_bodies.as_mut() { - bodies[index].take().unwrap() - } else { - self.lower_switch_body(body, loop_continue, do_while_continue)? - }; - (Some(value), lowered) - } - SwitchArm::Default { body, span } => { - if has_default { - return Err( - self.unsupported("a switch may contain at most one default arm", *span) - ); - } - has_default = true; - legacy_default_offset = Some(legacy_offset); - let lowered = if let Some(bodies) = reverse_bodies.as_mut() { - bodies[index].take().unwrap() - } else { - self.lower_switch_body(body, loop_continue, do_while_continue)? - }; - (None, lowered) - } - }; - if value.is_some() { - legacy_case_offsets.push(legacy_offset); - } - legacy_offset += - self.canonical_action_width(&body, span)? + usize::from(break_at.is_some()); - lowered_arms.push((value.map(Box::as_ref), body, break_at)); - } - - let break_arms: Vec<_> = lowered_arms - .iter() - .enumerate() - .filter_map(|(index, (_, _, break_at))| break_at.map(|break_at| (index, break_at))) - .collect(); - let first_break = break_arms.first().copied(); - let has_later_reachable_actions = - first_break.is_some_and(|(break_index, (break_at, _))| { - lowered_arms[break_index].1.len() > break_at - || lowered_arms - .iter() - .skip(break_index + 1) - .any(|(_, body, _)| !body.is_empty()) - }); - let use_shared_exit = break_arms.len() > 1 && has_later_reachable_actions; - - let case_values = self.lower_array(case_values); - if !use_shared_exit { - let default_offset = legacy_default_offset.unwrap_or(legacy_offset); - let offset_values = std::iter::once(default_offset) - .chain(legacy_case_offsets) - .map(|value| self.push_number(value as f64)) - .collect(); - let offsets = self.lower_array(offset_values); - let skip = self.lower_switch_selector(selector, case_values, offsets); - let true_value = self.push_value(Value::Bool(true)); - let mut branch_body = vec![skip]; - let else_body = if let Some((break_index, (break_at, _))) = first_break { - for (index, (_, body, _)) in lowered_arms.iter().enumerate() { - if index < break_index { - branch_body.extend(body.iter().copied()); - } else if index == break_index { - branch_body.extend(body[..break_at].iter().copied()); - } - } - let mut tail = Vec::new(); - tail.extend(lowered_arms[break_index].1[break_at..].iter().copied()); - for (_, body, _) in lowered_arms.iter().skip(break_index + 1) { - tail.extend(body.iter().copied()); - } - Some(tail) - } else { - for (_, body, _) in &lowered_arms { - branch_body.extend(body.iter().copied()); - } - None - }; - let result = self.push_if_actions(vec![(true_value, branch_body)], else_body); - return Ok(result); - } - - let offsets = self.push_value(Value::Array(Vec::new())); - let skip = self.lower_switch_selector(selector, case_values, offsets); - let mut arm_offsets = vec![None; lowered_arms.len()]; - let (switch, switch_end) = - self.lower_switch_level(&lowered_arms, 0, Some(skip), 0, &mut arm_offsets, span)?; - - let default_offset = lowered_arms - .iter() - .enumerate() - .find_map(|(index, (value, _, _))| value.is_none().then(|| arm_offsets[index].unwrap())) - .unwrap_or(switch_end); - let offset_values = std::iter::once(default_offset) - .chain( - lowered_arms - .iter() - .enumerate() - .filter(|(_, (value, _, _))| value.is_some()) - .map(|(index, _)| arm_offsets[index].unwrap()), - ) - .map(|value| self.push_number(value as f64)) - .collect(); - let offset_values = self.lower_array(offset_values); - let offset_value = self.value(offset_values).clone(); - let Some(node) = self.values.get_mut(offsets) else { - unreachable!("switch offset placeholder must exist") - }; - *node = offset_value; - - let Some(Action::Call { args, .. }) = self.actions.get_mut(skip) else { - unreachable!("switch selector must be a call action") - }; - let Some(selector_id) = args.first().copied() else { - unreachable!("switch selector condition must be a value call") - }; - let Some(Value::Call { args, .. }) = self.values.get_mut(selector_id) else { - unreachable!("switch selector condition must be a value call") - }; - args[0] = offsets; - - Ok(switch) - } - - fn lower_switch_selector( - &mut self, - selector: ValueId, - case_values: ValueId, - offsets: ValueId, - ) -> ActionId { - let one = self.push_number(1.0); - let index = self.push_call("indexOfArrayValue", vec![case_values, selector]); - let case_offset = self.push_call("add", vec![one, index]); - let skip_condition = self.push_call("valueInArray", vec![offsets, case_offset]); - self.push_call_action("skip", &[skip_condition]) - } - - fn lower_switch_level( - &mut self, - arms: &[LoweredSwitchArm<'_>], - start: usize, - selector_skip: Option, - level_offset: usize, - arm_offsets: &mut [Option], - span: Option, - ) -> Result<(Vec, usize), IntegrationError> { - let break_index = (start..arms.len()) - .find(|index| arms[*index].2.is_some()) - .expect("switch level must contain a break"); - let mut branch_body = Vec::new(); - if let Some(selector_skip) = selector_skip { - branch_body.push(selector_skip); - } - let mut branch_offset = 0; - for index in start..=break_index { - arm_offsets[index] = Some(if selector_skip.is_some() { - level_offset + branch_offset - } else if index == start { - level_offset - } else { - level_offset + 1 + branch_offset - }); - let (_, body, break_at) = &arms[index]; - let body = if index == break_index { - &body[..break_at.as_ref().unwrap().0] - } else { - body.as_slice() - }; - branch_offset += self.canonical_action_width(body, span)?; - branch_body.extend(body.iter().copied()); - } - let branch_width = self.canonical_action_width(&branch_body, span)?; - let (_, break_body, Some((break_at, _))) = &arms[break_index] else { - unreachable!("break index must point to a switch break") - }; - let mut else_body = break_body[*break_at..].to_vec(); - let tail_width = self.canonical_action_width(&else_body, span)?; - let else_content_start = if selector_skip.is_some() { - level_offset + branch_width + tail_width - } else { - level_offset + branch_width + tail_width + 2 - }; - let has_next_break = (break_index + 1..arms.len()).any(|index| arms[index].2.is_some()); - let end_offset = if has_next_break { - let (child, child_end) = self.lower_switch_level( - arms, - break_index + 1, - None, - else_content_start, - arm_offsets, - span, - )?; - else_body.extend(child); - child_end - } else { - let mut offset = else_content_start; - for index in break_index + 1..arms.len() { - arm_offsets[index] = Some(offset); - let (_, body, _) = &arms[index]; - offset += self.canonical_action_width(body, span)?; - else_body.extend(body.iter().copied()); - } - offset - }; - let true_value = self.push_value(Value::Bool(true)); - let switch = self.push_if_actions(vec![(true_value, branch_body)], Some(else_body)); - Ok((switch, end_offset)) - } - - fn lower_switch_body( - &mut self, - statements: &[Stmt], - loop_continue: Option<(&[ActionId], usize)>, - do_while_continue: bool, - ) -> Result { - let mut actions = Vec::new(); - let break_index = statements - .iter() - .position(|statement| matches!(statement, Stmt::Break { .. })); - let body_end = break_index.unwrap_or(statements.len()); - if do_while_continue { - actions.extend(self.lower_do_while_body(&statements[..body_end])?); - } else if let Some((after, structural_after)) = loop_continue { - actions.extend(self.lower_loop_sequence_with_break_target( - &statements[..body_end], - after, - structural_after + 1, - BreakTarget::Switch, - )?); - } else { - for statement in &statements[..body_end] { - actions.extend(self.lower_action(statement, Some(BreakTarget::Switch))?); - } - } - let mut break_at = None; - if let Some(index) = break_index { - let Stmt::Break { span } = &statements[index] else { - unreachable!("switch break index must point to a break") - }; - if statements[index + 1..] - .iter() - .any(|statement| matches!(statement, Stmt::Break { .. })) - { - return Err(self.unsupported( - "multiple switch breaks in one arm require canonical switch targets", - *span, - )); - } - break_at = Some(( - actions.len(), - span.ok_or_else(|| { - self.unsupported("switch break is missing source provenance", None) - })?, - )); - for statement in statements[index + 1..].iter() { - actions.extend(self.lower_action(statement, Some(BreakTarget::Switch))?); - } - } - Ok((actions, break_at)) - } - - fn canonical_action_width( - &self, - actions: &[ActionId], - fallback_span: Option, - ) -> Result { - let public_actions = self.public_actions(actions); - let mut program = self.program.clone(); - program.settings = None; - program.rules.push(rule_from_parts( - "action layout".to_string(), - false, - workshop_rs::Event::Global, - Vec::new(), - public_actions.clone(), - )); - workshop_rs::emitter::action_width( - &program, - self.compiler.catalog, - &Locale::new("en-US"), - &public_actions, - ) - .map(|layout| layout.width) - .map_err(|error| { - let span = fallback_span; - IntegrationError::new("workshop-action-layout", error.to_string(), span) - }) - } - - fn lower_array(&mut self, elements: Vec) -> ValueId { - let name = if elements.is_empty() { - "emptyArray" - } else { - "array" - }; - let elements = self.normalize_contextual_arguments(name, elements); - self.push_value(Value::Call { - name: name.to_string(), - args: self.value_args(&elements), - }) - } - - fn lower_translation_helper( - &mut self, - translations: &hir::TranslationState, - ) -> Result { - let translated_white = translations - .languages - .iter() - .map(|language| { - let locale = match language.as_str() { - "de" => "de-DE", - "en" => "en-US", - "es" | "es_mx" => "es-MX", - "es_es" => "es-ES", - "fr" => "fr-FR", - "it" => "it-IT", - "ja" => "ja-JP", - "ko" => "ko-KR", - "pl" => "pl-PL", - "pt" => "pt-BR", - "ru" => "ru-RU", - "th" => "th-TH", - "tr" => "tr-TR", - "zh" | "zh_cn" => "zh-CN", - "zh_tw" => "zh-TW", - _ => { - return Err(IntegrationError::new( - "translations-invalid", - format!("unsupported translation language '{language}'"), - translations.span, - )); - } - }; - self.compiler - .catalog - .localized_enum_spelling( - "Color", - &workshop_rs::catalog::Locale::new(locale), - "WHITE", - ) - .ok_or_else(|| { - IntegrationError::new( - "translations-invalid", - format!("unsupported translation locale '{locale}'"), - translations.span, - ) - }) - }) - .collect::, _>>()? - .join("0"); - let text = self.push_value(Value::String(format!("\u{ec48}0{translated_white}"))); - let custom_string = self.push_call("customString", vec![text]); - let null = self.push_value(Value::Null); - let separator = self.push_call("firstOf", vec![null]); - Ok(self.push_call("stringSplit", vec![custom_string, separator])) - } - - fn lower_translation( - &mut self, - name: &str, - args: &[Expr], - span: Option, - ) -> Result { - let Some(translations) = self.hir.preprocessing.translations.as_ref() else { - return Err(IntegrationError::new( - "translations-invalid", - format!("translation function '{name}' requires #!translations"), - span, - )); - }; - let (context, target) = match args { - [target] => (None, target), - [Expr::String { value: context, .. }, target] => (Some(context.as_str()), target), - _ => { - return Err(IntegrationError::new( - "translations-invalid", - format!("translation function '{name}' expects one or two arguments"), - span, - )); - } - }; - let (literal, format_args) = match target { - Expr::String { value, .. } => (value.clone(), Vec::new()), - Expr::Format { text, args, .. } => { - let (text, args) = self.fold_format_constants(text, args); - (text, args) - } - _ => { - let target = self.lower_value(target)?; - if name == "___" { - return Ok(target); - } - return Ok(self.select_translation(target)); - } - }; - if format_args.len() > 16 { - return Err(IntegrationError::new( - "translations-invalid", - "translated format strings support at most sixteen dynamic arguments", - span, - )); - } - let literal = literal.as_str(); - let msgid = literal.trim(); - if literal.contains('\u{ec48}') { - return Err(IntegrationError::new( - "translations-invalid", - "translation strings must not contain the reserved translation separator", - span, - )); - } - if !self - .translation_uses - .iter() - .any(|(existing_msgid, existing)| { - existing_msgid == msgid && existing.as_deref() == context - }) - { - self.translation_uses - .push((msgid.to_string(), context.map(str::to_string))); - } - let use_tl_err = !self.translation_player_options().2; - let mut localized = translations - .languages - .iter() - .map(|language| { - translations - .entries - .iter() - .find(|entry| entry.msgid == msgid && entry.context.as_deref() == context) - .and_then(|entry| entry.translations.get(language)) - .filter(|value| !value.is_empty()) - .cloned() - .unwrap_or_else(|| literal.to_string()) - }) - .collect::>(); - let tl_err_prefix = if use_tl_err { - "\u{ff34}\u{ff2c}\u{ff25}\u{ff52}\u{ff52}\u{ec48}" - } else { - "" - }; - let raw_string = format!("{tl_err_prefix}{}", localized.join("\u{ec48}")); - let replacement_mode = raw_string.chars().count() > 128 || format_args.len() > 3; - if replacement_mode { - for index in 0..format_args.len() { - let marker = format_number_marker(index); - for value in &mut localized { - *value = value.replace(&format!("{{{index}}}"), &marker); - } - } - let encoded_segments = localized.iter().enumerate().map(|(index, value)| { - if index == 0 { - format!("{tl_err_prefix}{value}") - } else { - value.clone() - } - }); - for (index, segment) in encoded_segments.enumerate() { - if segment.len() > 511 { - return Err(IntegrationError::new( - "translations-invalid", - format!( - "translated string for language '{}' is too long, maximum length is 511 bytes", - translations.languages[index] - ), - span, - )); - } - } - } - let encoded = format!("{tl_err_prefix}{}", localized.join("\u{ec48}")); - let text = self.push_value(Value::String(encoded)); - let custom = if replacement_mode { - let mut value = self.push_call("customString", vec![text]); - for (index, arg) in format_args.iter().enumerate() { - let marker = self.push_number(format_number_marker_value(index)); - let marker = self.push_call("updateEveryFrame", vec![marker]); - let replacement = self.lower_value(arg)?; - value = self.push_call("stringReplace", vec![value, marker, replacement]); - } - value - } else { - let mut custom_args = vec![text]; - custom_args.extend(self.lower_values(format_args.iter().copied())?); - self.push_call("customString", custom_args) - }; - let helper_id = *self.globals.get(TRANSLATION_HELPER_NAME).ok_or_else(|| { - IntegrationError::new( - "translations-invalid", - "translation helper variable was not allocated", - span, - ) - })?; - let helper = self.push_value(Value::GlobalVariable(self.global_names[helper_id].clone())); - let translated = self.push_call("stringSplit", vec![custom, helper]); - if name == "___" { - return Ok(translated); - } - if name == "_" - && self - .hir - .preprocessing - .directives - .iter() - .any(|directive| directive.name == "translateWithPlayerVar") - { - let variable = *self - .players - .get("__languageIndex__") - .expect("translation player variable is allocated"); - let player = self.push_call("localPlayer", Vec::new()); - let index = self.push_value(Value::PlayerVariable { - player, - variable: self.player_names[variable].clone(), - }); - return Ok(self.push_call("valueInArray", vec![translated, index])); - } - Ok(self.select_translation(translated)) - } - - fn select_translation(&mut self, values: ValueId) -> ValueId { - let helper_id = *self - .globals - .get(TRANSLATION_HELPER_NAME) - .expect("translation helper variable is allocated"); - let helper = self.push_value(Value::GlobalVariable(self.global_names[helper_id].clone())); - let color = self.push_value(Value::Enum { - value_type: "Color".to_string(), - value: "WHITE".to_string(), - }); - let empty_array = self.push_call("emptyArray", Vec::new()); - let color = self.push_call("stringSplit", vec![color, empty_array]); - let index = self.push_call("indexOfArrayValue", vec![helper, color]); - let index = self.push_call("absoluteValue", vec![index]); - self.push_call("valueInArray", vec![values, index]) - } - - fn translation_language_index( - &mut self, - translations: &hir::TranslationState, - ) -> Result { - let helper = self.lower_translation_helper(translations)?; - let color = self.push_value(Value::Enum { - value_type: "Color".to_string(), - value: "WHITE".to_string(), - }); - let empty_array = self.push_call("emptyArray", Vec::new()); - let color = self.push_call("stringSplit", vec![color, empty_array]); - Ok(self.push_call("indexOfArrayValue", vec![helper, color])) - } - - pub(super) fn translation_files(&self) -> Vec<(String, String)> { - let Some(translations) = self.hir.preprocessing.translations.as_ref() else { - return Vec::new(); - }; - let keep_unused = self - .hir - .preprocessing - .directives - .iter() - .any(|directive| directive.name == "keepUnusedTranslations"); - translations - .languages - .iter() - .skip(1) - .map(|language| { - let mut keys = self.translation_uses.clone(); - if keep_unused { - keys.extend( - translations - .entries - .iter() - .map(|entry| (entry.msgid.clone(), entry.context.clone())), - ); - } - keys.sort(); - keys.dedup(); - let mut output = String::from( - "msgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n", - ); - output.push_str(&format!("\"Language: {language}\\n\"\n\n")); - for (msgid, context) in keys { - if let Some(ref context) = context { - output.push_str(&format!( - "msgctxt {}\n", - serde_json::to_string(&context).unwrap() - )); - } - let translated = translations - .entries - .iter() - .find(|entry| { - entry.msgid == msgid && entry.context.as_deref() == context.as_deref() - }) - .and_then(|entry| entry.translations.get(language)) - .cloned() - .unwrap_or_default(); - output.push_str(&format!( - "msgid {}\n", - serde_json::to_string(&msgid).unwrap() - )); - output.push_str(&format!( - "msgstr {}\n\n", - serde_json::to_string(&translated).unwrap() - )); - } - (language.clone(), output) - }) - .collect() - } - - fn lower_debug( - &mut self, - expr: &Expr, - debug_source: Option<&str>, - ) -> Result { - let argument_span = expr.span().copied(); - let value = self.lower_text_value(expr)?; - let array_text = if self.debug_value_is_array(value) { - self.lower_debug_array_text(value, 6) - } else { - value - }; - let debug_label_text = debug_source - .map(str::to_string) - .unwrap_or_else(|| debug_expr_text(expr)); - let debug_label = canonical_debug_text(&debug_label_text); - let debug_prefix = format!("{debug_label}\u{2028}= {{0}}"); - let inline_padding = 128 - debug_prefix.chars().count() - "{1}".chars().count(); - let padding_text = self.push_value(Value::String(" ".repeat(170 - inline_padding))); - let padding = self.push_call("customString", vec![padding_text]); - let debug_label = self.push_value(Value::String(format!( - "{debug_prefix}{}{{1}}", - " ".repeat(inline_padding) - ))); - let text = self.push_call("customString", vec![debug_label, array_text, padding]); - let all_players = self.lower_all_players(); - let null_value = self.push_value(Value::Null); - let null_value_2 = self.push_value(Value::Null); - let null_value_3 = self.push_value(Value::Null); - let null_value_4 = self.push_value(Value::Null); - let hud_position = self.push_value(Value::Enum { - value_type: "HudPosition".to_string(), - value: "LEFT".to_string(), - }); - let sort_order = self.push_number(-9999.0); - let color = self.push_value(Value::Enum { - value_type: "Color".to_string(), - value: "WHITE".to_string(), - }); - let reevaluation = self.push_value(Value::Enum { - value_type: "HudReeval".to_string(), - value: "VISIBILITY_SORT_ORDER_STRING_AND_COLOR".to_string(), - }); - let visibility = self.push_value(Value::Enum { - value_type: "SpecVisibility".to_string(), - value: "DEFAULT".to_string(), - }); - let args = self.normalize_contextual_arguments( - "createHudText", - vec![ - all_players, - null_value, - text, - null_value_2, - hud_position, - sort_order, - null_value_3, - color, - null_value_4, - reevaluation, - visibility, - ], - ); - Ok(self.push_call_action_with_spans( - "createHudText", - &args, - [ - None, - None, - argument_span, - None, - None, - None, - None, - None, - None, - None, - None, - ], - )) - } - - fn lower_print( - &mut self, - expr: &Expr, - span: Option, - ) -> Result { - let argument_span = expr.span().copied(); - let empty_string = matches!(expr, Expr::String { value, .. } if value.is_empty()); - let value = self.lower_value(expr)?; - let value = if empty_string { - self.push_value(Value::Null) - } else { - value - }; - let padding_text = self.push_value(Value::String(" ".repeat(45))); - let padding = self.push_call("customString", vec![padding_text]); - let body_text = self.push_value(Value::String(format!("{}{{0}}", " ".repeat(125)))); - let body = self.push_call("customString", vec![body_text, padding]); - let all_players = self.lower_all_players(); - let null_value = self.push_value(Value::Null); - let null_value_2 = self.push_value(Value::Null); - let null_value_3 = self.push_value(Value::Null); - let hud_position = self.push_value(Value::Enum { - value_type: "HudPosition".to_string(), - value: "LEFT".to_string(), - }); - let sort_order = self.push_number(-9999.0); - let color = if empty_string { - self.push_value(Value::Null) - } else { - self.push_value(Value::Enum { - value_type: "Color".to_string(), - value: "ORANGE".to_string(), - }) - }; - let reevaluation = self.push_value(Value::Enum { - value_type: "HudReeval".to_string(), - value: "VISIBILITY_AND_STRING".to_string(), - }); - let visibility = self.push_value(Value::Enum { - value_type: "SpecVisibility".to_string(), - value: "DEFAULT".to_string(), - }); - let mut args = self.normalize_contextual_arguments( - "createHudText", - vec![ - all_players, - value, - body, - null_value, - hud_position, - sort_order, - color, - null_value_2, - null_value_3, - reevaluation, - visibility, - ], - ); - self.apply_replacements_to_values("createHudText", &mut args, span); - Ok(self.push_call_action_with_spans( - "createHudText", - &args, - [ - None, - argument_span, - None, - None, - None, - None, - None, - None, - None, - None, - None, - ], - )) - } - - fn lower_debug_array_text(&mut self, value: ValueId, max_length: usize) -> ValueId { - macro_rules! call { - ($name:literal $(, $arg:expr)* $(,)?) => {{ - let args = vec![$($arg),*]; - self.push_call($name, args) - }}; - } - - let current_count = call!("countOf", call!("currentArrayElement")); - let is_single = call!( - "==", - call!("countOf", call!("currentArrayElement")), - self.push_number(1.0) - ); - let is_empty = call!("==", call!("currentArrayElement"), call!("emptyArray")); - let not_null = call!( - "!=", - call!("currentArrayElement"), - self.push_value(Value::Null) - ); - let has_empty_array = call!("and", is_empty, not_null); - let brackets = call!("or", is_single, has_empty_array); - let first_element = call!( - "customString", - self.push_value(Value::String("[{0}]".to_string())), - call!("currentArrayElement"), - ); - let many_elements = call!( - "customString", - self.push_value(Value::String("[{0}, …+{1}]".to_string())), - call!("currentArrayElement"), - call!( - "subtract", - call!("countOf", call!("currentArrayElement")), - self.push_number(1.0), - ), - ); - let element_text = call!( - "ifThenElse", - brackets, - first_element, - call!( - "ifThenElse", - current_count, - many_elements, - call!("currentArrayElement"), - ), - ); - let mapped_elements = call!("mappedArray", value, element_text,); - let mapped_input = call!("array", mapped_elements); - let current_array = call!("currentArrayElement"); - let actual_array = call!( - "or", - call!("countOf", current_array), - call!( - "and", - call!("==", call!("currentArrayElement"), call!("emptyArray")), - call!( - "!=", - call!("currentArrayElement"), - self.push_value(Value::Null) - ), - ), - ); - let empty_length = call!( - "ifThenElse", - call!( - "and", - call!("not", call!("countOf", call!("currentArrayElement"))), - call!("!=", call!("currentArrayElement"), call!("emptyArray"),), - ), - self.push_number(3.0), - call!( - "multiply", - call!("countOf", call!("currentArrayElement")), - self.push_number(3.0), - ), - ); - let x = call!( - "appendToArray", - call!("appendToArray", actual_array, empty_length), - current_array, - ); - let x_input = call!("mappedArray", mapped_input, x); - let x_length = |this: &mut Self| { - let current = this.push_call("currentArrayElement", Vec::new()); - let index = this.push_number(1.0); - this.push_call("valueInArray", vec![current, index]) - }; - let x_value = |this: &mut Self, index: f64| { - let current = this.push_call("currentArrayElement", Vec::new()); - let index_value = this.push_number(index); - this.push_call("valueInArray", vec![current, index_value]) - }; - let first = call!("firstOf", call!("currentArrayElement")); - let array_head = if max_length == 6 { - let array_tail = call!( - "customString", - self.push_value(Value::String("{0}, {1}, {2}".to_string())), - x_value(self, 4.0), - x_value(self, 5.0), - call!( - "customString", - self.push_value(Value::String("{0}, {1}, …\u{0001}".to_string())), - x_value(self, 6.0), - x_value(self, 7.0), - ), - ); - call!( - "customString", - self.push_value(Value::String("{0}, {1}, {2}".to_string())), - x_value(self, 2.0), - x_value(self, 3.0), - array_tail, - ) - } else if max_length <= 3 { - let display = format!( - "{}…\u{0001}", - (0..max_length) - .map(|index| format!("{{{index}}}, ")) - .collect::() - ); - let mut args = vec![self.push_value(Value::String(display))]; - for index in 0..max_length { - args.push(x_value(self, (index + 2) as f64)); - } - self.push_call("customString", args) - } else { - let mut array_head = self.push_value(Value::String("…\u{0001}".to_string())); - for index in (0..max_length).rev() { - array_head = call!( - "customString", - self.push_value(Value::String("{0}, {1}".to_string())), - x_value(self, (index + 2) as f64), - array_head, - ); - } - array_head - }; - let placeholder_text = format!( - "{}\u{2026}\u{0001}", - (0..max_length).map(|_| "0, ").collect::() - ); - let placeholder = call!( - "customString", - self.push_value(Value::String(placeholder_text.clone())), - ); - let length_for_slice = x_length(self); - let end_length_for_slice = x_length(self); - let start = self.push_number( - (placeholder_text.chars().count() as isize - 4 - 3 * max_length as isize) as f64, - ); - let end = self.push_number((max_length * 3 + 4) as f64); - let slice = call!( - "stringSlice", - placeholder, - call!("add", start, length_for_slice), - call!("subtract", end, end_length_for_slice,), - ); - let replaced = call!("stringReplace", array_head, slice, call!("emptyArray"),); - let length_for_compare = x_length(self); - let length_for_divide = x_length(self); - let plus = call!( - "ifThenElse", - call!( - ">", - length_for_compare, - self.push_number((max_length * 3) as f64), - ), - call!( - "customString", - self.push_value(Value::String("+{0}".to_string())), - call!( - "subtract", - call!("divide", length_for_divide, self.push_number(3.0)), - self.push_number(max_length as f64), - ), - ), - call!("emptyArray"), - ); - let formatted_array = call!( - "customString", - self.push_value(Value::String("[{0}{1}]".to_string())), - replaced, - plus, - ); - let current_for_split = call!("currentArrayElement"); - let rendered = call!( - "ifThenElse", - first, - formatted_array, - call!( - "stringSplit", - call!("valueInArray", current_for_split, self.push_number(2.0)), - call!("emptyArray"), - ), - ); - call!("mappedArray", x_input, rendered) - } - - fn lower_text_value(&mut self, expr: &Expr) -> Result { - let value = self.lower_value(expr)?; - let Value::Call { name, args } = self.value(value) else { - return Ok(value); - }; - if name == "customString" && args.len() == 1 { - Ok(args[0]) - } else { - Ok(value) - } - } - - fn debug_value_is_array(&self, value: ValueId) -> bool { - match self.value(value) { - Value::GlobalVariable(_) | Value::Array(_) => true, - Value::Call { name, .. } if matches!(name.as_str(), "array" | "emptyArray") => true, - Value::Call { name, .. } => self - .compiler - .catalog - .entry(Kind::Value, name) - .and_then(|entry| entry.return_type()) - .is_some_and(|return_type| { - return_type.split('|').any(|part| part.trim() == "Array") - }), - _ => false, - } - } - - fn value_is_known_player(&self, value: ValueId) -> bool { - match self.value(value) { - Value::EventPlayer => true, - Value::Call { name, .. } => self - .compiler - .catalog - .entry(Kind::Value, name) - .and_then(|entry| entry.return_type()) - .is_some_and(|return_type| { - return_type.split('|').any(|part| part.trim() == "Player") - }), - _ => false, - } - } - - fn push_value(&mut self, value: Value) -> ValueId { - let id = self.values.len(); - self.values.push(value); - #[cfg(test)] - crate::resource_metrics::record_lowering_values(self.values.len()); - id - } - - fn push_call(&mut self, name: &str, args: Vec) -> ValueId { - let args = self.normalize_contextual_arguments(name, args); - self.push_value(Value::Call { - name: name.to_string(), - args, - }) - } - - fn normalize_contextual_argument( - &mut self, - call_id: &str, - arg_index: usize, - value_id: ValueId, - ) -> ValueId { - let domain = [Kind::Action, Kind::Value].into_iter().find_map(|kind| { - self.compiler - .catalog - .entry(kind, call_id) - .and_then(|entry| entry.param_domain(arg_index)) - }); - if domain == Some("BarrierLos") { - let value = match self.value(value_id) { - Value::Bool(true) => Some("PASS_THROUGH_BARRIERS"), - Value::Bool(false) => Some("BLOCKED_BY_ALL_BARRIERS"), - _ => None, - }; - if let Some(value) = value { - return self.push_value(Value::Enum { - value_type: "BarrierLos".to_string(), - value: value.to_string(), - }); - } - } - value_id - } - - fn normalize_contextual_arguments( - &mut self, - call_id: &str, - mut args: Vec, - ) -> Vec { - let mut index = 0; - while index < args.len() { - args[index] = self.normalize_contextual_argument(call_id, index, args[index]); - index += 1; - } - args - } - - fn lower_custom_string(&mut self, value: String) -> ValueId { - let text = self.push_value(Value::String(value)); - self.push_call("customString", vec![text]) - } - - fn fold_format_constants<'b>( - &self, - text: &str, - args: &'b [hir::Expr], - ) -> (String, Vec<&'b hir::Expr>) { - let values = args - .iter() - .map(|arg| { - let mut stack = Vec::new(); - crate::compile_time::evaluate(arg, &self.constants, &HashMap::new(), &mut stack) - .and_then(compile_time_value_text) - }) - .collect::>(); - let dynamic_indexes = values - .iter() - .enumerate() - .filter_map(|(index, value)| value.is_none().then_some(index)) - .collect::>(); - let dynamic_args = dynamic_indexes - .iter() - .map(|index| &args[*index]) - .collect::>(); - let dynamic_position = dynamic_indexes - .iter() - .enumerate() - .map(|(position, index)| (*index, position)) - .collect::>(); - - let canonical = canonical_format_text(text); - let mut output = String::with_capacity(canonical.len()); - let mut cursor = 0; - while cursor < canonical.len() { - let Some(open_rel) = canonical[cursor..].find('{') else { - output.push_str(&canonical[cursor..]); - break; - }; - let open = cursor + open_rel; - output.push_str(&canonical[cursor..open]); - let Some(close_rel) = canonical[open + 1..].find('}') else { - output.push_str(&canonical[open..]); - break; - }; - let close = open + 1 + close_rel; - let marker = &canonical[open + 1..close]; - let Ok(index) = marker.parse::() else { - output.push_str(&canonical[open..=close]); - cursor = close + 1; - continue; - }; - if let Some(Some(value)) = values.get(index) { - output.push_str(value); - } else if let Some(position) = dynamic_position.get(&index) { - output.push('{'); - output.push_str(&position.to_string()); - output.push('}'); - } else { - output.push_str(&canonical[open..=close]); - } - cursor = close + 1; - } - (output, dynamic_args) - } - - fn push_number(&mut self, value: f64) -> ValueId { - self.push_value(Value::Number(value)) - } - - fn canonical_vector_member(&self, x: ValueId, y: ValueId, z: ValueId) -> Option<&'static str> { - let number = |id| match self.values.get(id)? { - Value::Number(value) => Some(*value), - _ => None, - }; - match (number(x), number(y), number(z)) { - (Some(1.0), Some(0.0), Some(0.0)) => Some("LEFT"), - (Some(-1.0), Some(0.0), Some(0.0)) => Some("RIGHT"), - (Some(0.0), Some(1.0), Some(0.0)) => Some("UP"), - (Some(0.0), Some(-1.0), Some(0.0)) => Some("DOWN"), - (Some(0.0), Some(0.0), Some(1.0)) => Some("FORWARD"), - (Some(0.0), Some(0.0), Some(-1.0)) => Some("BACKWARD"), - _ => None, - } - } - - fn fold_numeric_binary(&self, op: &str, left: ValueId, right: ValueId) -> Option { - let number = |id| match self.values.get(id)? { - Value::Number(value) => Some(*value), - _ => None, - }; - let left = number(left)?; - let right = number(right)?; - let value = match op { - "+" => left + right, - "-" => left - right, - "*" => left * right, - "/" if right != 0.0 => left / right, - "%" if right != 0.0 => left % right, - "**" => left.powf(right), - _ => return None, - }; - value.is_finite().then_some(value) - } - - fn value_is_number(&self, id: ValueId, expected: f64) -> bool { - matches!(self.values.get(id), Some(Value::Number(value)) if *value == expected) - } - - fn value_is_empty_string(&self, id: ValueId) -> bool { - matches!(self.values.get(id), Some(Value::String(value)) if value.is_empty()) - } - - fn lower_delete( - &mut self, - target: &Expr, - span: Option, - ) -> Result { - let mut indices = Vec::new(); - let Some(root) = indexed_target_parts(target, &mut indices) else { - return Err(self.unsupported( - "delete statements require an indexed global or player variable", - span, - )); - }; - if indices.len() > 4 { - return Err(self.unsupported("Cannot delete index of 4d array", span)); - } - indices.reverse(); - if hir::visit::has_random_nested_delete(root, &indices) { - return Err(self.unsupported( - "Cannot delete from nested array with a random outer or middle index", - span, - )); - } - let (root_value, action_name) = match root { - Expr::GlobalVar { - name, - span: target_span, - } => { - let variable = *self.globals.get(name).ok_or_else(|| { - self.unsupported(format!("unknown global variable '{name}'"), *target_span) - })?; - let root_value = - self.push_value(Value::GlobalVariable(self.global_names[variable].clone())); - (root_value, "modifyGlobalVariableAtIndex") - } - Expr::PlayerVar { - player, - name, - span: target_span, - .. - } => { - let variable = *self.players.get(name).ok_or_else(|| { - self.unsupported(format!("unknown player variable '{name}'"), *target_span) - })?; - let player = self.lower_value(player)?; - let value = self.push_value(Value::PlayerVariable { - player, - variable: self.player_names[variable].clone(), - }); - (value, "modifyPlayerVariableAtIndex") - } - _ => { - return Err(self.unsupported( - "delete statements are only representable for global or player variables", - target.span().copied(), - )); - } - }; - let index = self.lower_value(indices[0])?; - if indices.len() == 1 { - let op = ModifyOp::RemoveFromArrayByIndex; - return Ok(if action_name == "modifyGlobalVariableAtIndex" { - let variable = match self.values.get(root_value) { - Some(Value::GlobalVariable(variable)) => variable.clone(), - _ => unreachable!("global delete root must be a global variable value"), - }; - self.push_action(Action::ModifyGlobalVariable { - variable, - op, - value: index, - }) - } else { - let (player, variable) = match self.values.get(root_value) { - Some(Value::PlayerVariable { player, variable }) => (*player, variable.clone()), - _ => unreachable!("player delete root must be a player variable value"), - }; - self.push_action(Action::ModifyPlayerVariable { - player, - variable, - op, - value: index, - }) - }); - } - - let op = self.push_call("removeFromArrayByIndex", Vec::new()); - if indices.len() == 2 { - let inner_index = self.lower_value(indices[1])?; - let args = self.normalize_contextual_arguments( - action_name, - vec![root_value, index, op, inner_index], - ); - return Ok(self.push_call_action(action_name, &args)); - } - - let outer_array = self.lower_indexed_read(root_value, indices[0], index); - if indices.len() == 4 { - let replacement = self.rebuild_deleted_array(outer_array, &indices[1..])?; - let action_name = if action_name == "modifyGlobalVariableAtIndex" { - "setGlobalVariableAtIndex" - } else { - "setPlayerVariableAtIndex" - }; - let args = self - .normalize_contextual_arguments(action_name, vec![root_value, index, replacement]); - return Ok(self.push_call_action(action_name, &args)); - } - let inner_index = self.lower_value(indices[1])?; - let row = self.lower_indexed_read(outer_array, indices[1], inner_index); - let leaf_index = self.lower_value(indices[2])?; - let current_index = self.push_call("currentArrayIndex", Vec::new()); - let condition = self.push_call("!=", vec![current_index, leaf_index]); - let filtered = self.push_call("filteredArray", vec![row, condition]); - let replacement = if let Some(number) = hir::visit::literal_number(indices[1]) { - let middle = self.lower_array(vec![filtered]); - let maximum = self.push_number(999_999_999_999.0); - let suffix_start = self.push_number(number + 1.0); - let suffix = self.push_call("slice", vec![outer_array, suffix_start, maximum]); - if number == 0.0 { - self.push_call("appendToArray", vec![middle, suffix]) - } else { - let zero = self.push_number(0.0); - let prefix = self.push_call("slice", vec![outer_array, zero, inner_index]); - let with_replacement = self.push_call("appendToArray", vec![prefix, middle]); - self.push_call("appendToArray", vec![with_replacement, suffix]) - } - } else { - self.replace_array_element(outer_array, inner_index, filtered) - }; - let action_name = if action_name == "modifyGlobalVariableAtIndex" { - "setGlobalVariableAtIndex" - } else { - "setPlayerVariableAtIndex" - }; - let args = - self.normalize_contextual_arguments(action_name, vec![root_value, index, replacement]); - Ok(self.push_call_action(action_name, &args)) - } - - fn lower_assign( - &mut self, - target: &Expr, - value: &Expr, - span: Option, - ) -> Result { - let mut indices = Vec::new(); - if let Some(root) = indexed_target_parts(target, &mut indices) { - if indices.len() > 3 { - return Err(self.unsupported("Cannot assign to 4d array", target.span().copied())); - } - if indices.len() > 1 { - indices.reverse(); - return self.lower_nested_indexed_assign(root, &indices, target, value); - } - } - match target { - Expr::GlobalVar { - name, - span: target_span, - } => { - let variable = *self.globals.get(name).ok_or_else(|| { - self.unsupported(format!("unknown global variable '{name}'"), *target_span) - })?; - if let Expr::Binary { - op, left, right, .. - } = value - && matches!(left.as_ref(), Expr::GlobalVar { name: left_name, .. } if left_name == name) - && let Some((modify_op, _)) = modify_operator(op) - { - let value = self.lower_value(right)?; - return Ok(self.push_action(Action::ModifyGlobalVariable { - variable: self.global_names[variable].clone(), - op: modify_op, - value, - })); - } - let val = self.lower_value(value)?; - Ok(self.push_action(Action::SetGlobalVariable { - variable: self.global_names[variable].clone(), - value: val, - })) - } - Expr::PlayerVar { - player, - name, - span: target_span, - .. - } => { - let variable = *self.players.get(name).ok_or_else(|| { - self.unsupported(format!("unknown player variable '{name}'"), *target_span) - })?; - let player_val = self.lower_value(player)?; - if let Expr::Binary { - op, left, right, .. - } = value - && matches!(left.as_ref(), Expr::PlayerVar { player: left_player, name: left_name, .. } if left_name == name && left_player.as_ref() == player.as_ref()) - && let Some((modify_op, _)) = modify_operator(op) - { - let value = self.lower_value(right)?; - return Ok(self.push_action(Action::ModifyPlayerVariable { - player: player_val, - variable: self.player_names[variable].clone(), - op: modify_op, - value, - })); - } - let val = self.lower_value(value)?; - Ok(self.push_action(Action::SetPlayerVariable { - player: player_val, - variable: self.player_names[variable].clone(), - value: val, - })) - } - Expr::Index { - array, - index, - span: target_span, - } => match array.as_ref() { - Expr::GlobalVar { - name, - span: arr_span, - } => { - let variable = *self.globals.get(name).ok_or_else(|| { - self.unsupported(format!("unknown global variable '{name}'"), *arr_span) - })?; - let var_node = self.push_value(Value::GlobalVariable( - self.global_names[variable].clone(), - )); - let index_val = self.lower_value(index)?; - self.lower_indexed_assignment( - target, - var_node, - index_val, - value, - ("setGlobalVariableAtIndex", "modifyGlobalVariableAtIndex"), - ) - } - Expr::PlayerVar { - player, - name, - span: arr_span, - .. - } => { - let player_val = self.lower_value(player)?; - let variable = *self.players.get(name).ok_or_else(|| { - self.unsupported(format!("unknown player variable '{name}'"), *arr_span) - })?; - let var_node = self.push_value(Value::PlayerVariable { - player: player_val, - variable: self.player_names[variable].clone(), - }); - let index_val = self.lower_value(index)?; - self.lower_indexed_assignment( - target, - var_node, - index_val, - value, - ("setPlayerVariableAtIndex", "modifyPlayerVariableAtIndex"), - ) - } - _ => Err(self.unsupported( - "indexing assignment is only representable for global or player variables", - *target_span, - )), - }, - _ => Err(self.unsupported( - "only global-variable, player-variable, or index assignment is currently representable in canonical WIR", - span, - )), - } - } - - fn lower_indexed_assignment( - &mut self, - target: &Expr, - variable: ValueId, - index_value: ValueId, - value: &Expr, - actions: (&str, &str), - ) -> Result { - let Expr::Index { array, index, .. } = target else { - unreachable!("indexed assignment target was matched before lowering") - }; - let (set_action, modify_action) = actions; - if let Expr::Binary { - op, left, right, .. - } = value - && let Expr::Index { - array: left_array, - index: left_index, - .. - } = left.as_ref() - && left_array.as_ref() == array.as_ref() - && left_index.as_ref() == index.as_ref() - && let Some((_, call_name)) = modify_operator(op) - { - let operator = self.push_call(call_name, Vec::new()); - let value = self.lower_value(right)?; - let args = self.normalize_contextual_arguments( - modify_action, - vec![variable, index_value, operator, value], - ); - return Ok(self.push_call_action(modify_action, &args)); - } - - let value = self.lower_value(value)?; - let args = - self.normalize_contextual_arguments(set_action, vec![variable, index_value, value]); - Ok(self.push_call_action(set_action, &args)) - } - - fn lower_nested_indexed_assign( - &mut self, - root: &Expr, - indices: &[&Expr], - target: &Expr, - value: &Expr, - ) -> Result { - let (action_name, root_value) = match root { - Expr::GlobalVar { - name, - span: target_span, - } => { - let variable = *self.globals.get(name).ok_or_else(|| { - self.unsupported(format!("unknown global variable '{name}'"), *target_span) - })?; - let root_value = - self.push_value(Value::GlobalVariable(self.global_names[variable].clone())); - ("setGlobalVariableAtIndex", root_value) - } - Expr::PlayerVar { - player, - name, - span: target_span, - .. - } => { - let player_value = self.lower_value(player)?; - let variable = *self.players.get(name).ok_or_else(|| { - self.unsupported(format!("unknown player variable '{name}'"), *target_span) - })?; - let root_value = self.push_value(Value::PlayerVariable { - player: player_value, - variable: self.player_names[variable].clone(), - }); - ("setPlayerVariableAtIndex", root_value) - } - _ => { - return Err(self.unsupported( - "indexing assignment is only representable for global or player variables", - target.span().copied(), - )); - } - }; - - let outer_index = self.lower_value(indices[0])?; - let outer_array = self.lower_indexed_read(root_value, indices[0], outer_index); - let replacement = self.rebuild_indexed_value(outer_array, &indices[1..], target, value)?; - let args = self.normalize_contextual_arguments( - action_name, - vec![root_value, outer_index, replacement], - ); - Ok(self.push_call_action(action_name, &args)) - } - - fn rebuild_indexed_value( - &mut self, - array: ValueId, - indices: &[&Expr], - target: &Expr, - value: &Expr, - ) -> Result { - let index = indices - .first() - .copied() - .expect("nested indexed assignment has an inner index"); - let index_value = self.lower_value(index)?; - let replacement = if indices.len() == 1 { - if let Expr::Binary { - op, left, right, .. - } = value - && left.as_ref() == target - && let Some((_, call_name)) = modify_operator(op) - { - let current = self.lower_indexed_read(array, index, index_value); - let right = self.lower_value(right)?; - self.push_call(call_name, vec![current, right]) - } else { - self.lower_value(value)? - } - } else { - let child = self.lower_indexed_read(array, index, index_value); - self.rebuild_indexed_value(child, &indices[1..], target, value)? - }; - Ok(self.replace_array_element(array, index_value, replacement)) - } - - fn lower_indexed_read( - &mut self, - array: ValueId, - index: &Expr, - index_value: ValueId, - ) -> ValueId { - if matches!(index, Expr::Number { value, .. } if *value == 0.0) { - self.push_call("firstOf", vec![array]) - } else { - self.push_call("valueInArray", vec![array, index_value]) - } - } - - fn replace_array_element( - &mut self, - array: ValueId, - index: ValueId, - replacement: ValueId, - ) -> ValueId { - let zero = self.push_number(0.0); - let one = self.push_number(1.0); - let end = self.push_call("add", vec![index, one]); - let maximum = self.push_number(999_999_999_999.0); - let prefix = self.push_call("slice", vec![array, zero, index]); - let middle = self.lower_array(vec![replacement]); - let suffix = self.push_call("slice", vec![array, end, maximum]); - let with_replacement = self.push_call("appendToArray", vec![prefix, middle]); - self.push_call("appendToArray", vec![with_replacement, suffix]) - } - - fn rebuild_deleted_array( - &mut self, - array: ValueId, - indices: &[&Expr], - ) -> Result { - let index = self.lower_value(indices[0])?; - if indices.len() == 1 { - let current_index = self.push_call("currentArrayIndex", Vec::new()); - let condition = self.push_call("!=", vec![current_index, index]); - return Ok(self.push_call("filteredArray", vec![array, condition])); - } - let child = self.lower_indexed_read(array, indices[0], index); - let replacement = self.rebuild_deleted_array(child, &indices[1..])?; - Ok(self.replace_array_element_for_delete(array, indices[0], index, replacement)) - } - - fn replace_array_element_for_delete( - &mut self, - array: ValueId, - index_expr: &Expr, - index: ValueId, - replacement: ValueId, - ) -> ValueId { - if let Some(number) = hir::visit::literal_number(index_expr) { - let middle = self.lower_array(vec![replacement]); - let maximum = self.push_number(999_999_999_999.0); - let suffix_start = self.push_number(number + 1.0); - let suffix = self.push_call("slice", vec![array, suffix_start, maximum]); - if number == 0.0 { - return self.push_call("appendToArray", vec![middle, suffix]); - } - let zero = self.push_number(0.0); - let prefix = self.push_call("slice", vec![array, zero, index]); - let with_replacement = self.push_call("appendToArray", vec![prefix, middle]); - return self.push_call("appendToArray", vec![with_replacement, suffix]); - } - self.replace_array_element(array, index, replacement) - } - - fn lower_cased_progress_bar( - &mut self, - args: &[Expr], - span: Option, - ) -> Result, IntegrationError> { - let [ - Expr::Number { - value: text_count, .. - }, - visible_to, - Expr::String { value: text, .. }, - position, - scale, - clipping, - text_color, - reevaluation, - spectators, - ] = args - else { - return Err(self.unsupported( - "createCasedProgressBarIwt requires a literal text count and text", - span, - )); - }; - let text_count_value = *text_count; - if !text_count_value.is_finite() - || text_count_value.fract() != 0.0 - || !(2.0..=6.0).contains(&text_count_value) - { - return Err(self.unsupported( - "createCasedProgressBarIwt text count must be between 2 and 6", - span, - )); - } - let text_count = text_count_value as usize; - if args.iter().any(hir::visit::contains_random) { - return Err(self.unsupported( - "Cannot use random functions in createCasedProgressBarIwt", - span, - )); - } - let visible_to = self.lower_value(visible_to)?; - let position = self.lower_value(position)?; - let scale = self.lower_value(scale)?; - let clipping = self.lower_value(clipping)?; - let text_color = self.lower_value(text_color)?; - let reevaluation = self.lower_value(reevaluation)?; - let spectators = self.lower_value(spectators)?; - let header_color = self.push_value(Value::Enum { - value_type: "Color".to_string(), - value: "WHITE".to_string(), - }); - let texts = text - .replace('\n', " \n ") - .split('\n') - .map(|line| { - cased_line(line, text_count) - .into_iter() - .map(|line| format!("{line}\u{ad}")) - .collect::>() - }) - .reduce(|mut all, lines| { - for (index, line) in lines.into_iter().enumerate() { - if index < all.len() { - all[index].push('\n'); - all[index].push_str(&line); - } - } - all - }) - .unwrap_or_else(|| vec![String::new(); text_count]); - let mut actions = Vec::with_capacity(text_count); - for (index, text) in texts.into_iter().enumerate() { - let value = self.push_number(index as f64); - let text = self.lower_custom_string(text); - let values = self.normalize_contextual_arguments( - "createProgressBarInWorldText", - vec![ - visible_to, - value, - text, - position, - scale, - clipping, - header_color, - text_color, - reevaluation, - spectators, - ], - ); - actions.push(self.push_call_action_with_spans( - "createProgressBarInWorldText", - &values, - [None; 10], - )); - } - Ok(actions) - } - - fn lower_action_call( - &mut self, - name: &str, - args: &[Expr], - span: Option, - ) -> Result { - if args.is_empty() { - if let Some(&subroutine) = self.subroutines.get(name) { - return Ok(self.push_action(Action::CallSubroutine { - subroutine: self.subroutine_names[subroutine].clone(), - })); - } - } - if name == "chaseAtRate" { - let spans = args - .iter() - .map(|expr| expr.span().copied()) - .collect::>(); - let args = self.lower_values(args)?; - return Ok(self.push_call_action_with_spans(name, &args, spans)); - } - let function = self - .compiler - .manifest - .resolve_function(name) - .ok_or_else(|| self.unsupported(format!("unknown action '{name}'"), span))?; - if !matches!(function.kind, FunctionKind::Action) { - return Err(self.unsupported(format!("'{name}' is not a generic OPY action"), span)); - } - if matches!(function.id.as_str(), "async" | "startRule") { - let [subroutine, behavior] = args else { - return Err(self.unsupported( - format!( - "{} requires a subroutine and a start-rule behavior", - function.id - ), - span, - )); - }; - let subroutine_name = match subroutine { - Expr::Call { name, args, .. } if args.is_empty() => name, - _ => { - return Err(self.unsupported( - format!("{} requires a declared subroutine", function.id), - subroutine.span().copied(), - )); - } - }; - let subroutine_id = *self.subroutines.get(subroutine_name).ok_or_else(|| { - self.unsupported( - format!("unknown subroutine '{subroutine_name}'"), - subroutine.span().copied(), - ) - })?; - let subroutine_span = subroutine.span().copied(); - let behavior_span = behavior.span().copied(); - let subroutine = self.push_value(Value::Subroutine( - self.subroutine_names[subroutine_id].clone(), - )); - let behavior = self.lower_value(behavior)?; - return Ok(self.push_call_action_with_spans( - "startRule", - &[subroutine, behavior], - [subroutine_span, behavior_span], - )); - } - if matches!( - function.id.as_str(), - "hudHeader" | "hudSubheader" | "hudSubtext" - ) { - let text_slot = match function.id.as_str() { - "hudHeader" => 1, - "hudSubheader" => 2, - "hudSubtext" => 3, - _ => unreachable!(), - }; - return self.lower_hud_text(args, span, text_slot, &function.id); - } - if function.id == "createDummy" && args.len() == 4 { - let spans = args - .iter() - .map(|expr| expr.span().copied()) - .chain(std::iter::once(None)); - let mut lowered = self.lower_values(args)?; - let mut zero_vector = Vec::with_capacity(3); - for value in [0.0, 0.0, 0.0] { - zero_vector.push(self.push_number(value)); - } - lowered.push(self.push_call("vector", zero_vector)); - let args = self.normalize_contextual_arguments("createDummyBot", lowered); - return Ok(self.push_call_action_with_spans("createDummyBot", &args, spans)); - } - let spans = args - .iter() - .map(|expr| expr.span().copied()) - .collect::>(); - let args = self.lower_values(args)?; - let catalog_id = if matches!(function.id.as_str(), "stopChasingVariable" | "stopChasing") { - match args.first().map(|value| self.value(*value)) { - Some(Value::GlobalVariable(_)) => "stopChasingGlobalVariable", - Some(Value::PlayerVariable { .. }) => "stopChasingPlayerVariable", - _ => { - return Err(self.unsupported( - "stopChasingVariable requires a global or player variable", - span, - )); - } - } - } else { - function.catalog_id.as_deref().ok_or_else(|| { - self.unsupported( - format!( - "action '{}' requires a special lowering not in #46", - function.id - ), - span, - ) - })? - }; - let mut args = self.normalize_contextual_arguments(catalog_id, args); - self.apply_replacements_to_values(catalog_id, &mut args, span); - self.optimize_wait_duration(catalog_id, &mut args, span); - Ok(self.push_call_action_with_spans(catalog_id, &args, spans)) - } - - fn optimize_wait_duration( - &mut self, - catalog_id: &str, - args: &mut [ValueId], - span: Option, - ) { - const DEFAULT_WAIT_SECONDS: f64 = 0.016; - - let optimization = self.optimization_state_at(span.as_ref()); - if catalog_id != "wait" || !optimization.enabled || !optimization.for_size { - return; - } - let Some(duration) = args.first().copied() else { - return; - }; - match self.value(duration) { - Value::Number(value) if *value <= DEFAULT_WAIT_SECONDS => { - let value = self.push_value(Value::Bool(false)); - args[0] = self.normalize_contextual_argument(catalog_id, 0, value); - } - Value::Number(value) if *value == 1.0 => { - let value = self.push_value(Value::Bool(true)); - args[0] = self.normalize_contextual_argument(catalog_id, 0, value); - } - _ => {} - } - } - - fn lower_hud_text( - &mut self, - args: &[Expr], - span: Option, - text_slot: usize, - function_name: &str, - ) -> Result { - let [ - visible_to, - text, - position, - sort_order, - color, - reevaluation, - spectators, - ] = args - else { - return Err(self.unsupported( - format!("{function_name} requires exactly seven bound arguments"), - span, - )); - }; - let visible_to_span = visible_to.span().copied(); - let visible_to = self.lower_hud_visible_to(visible_to)?; - let mut text_slots = [ - self.push_value(Value::Null), - self.push_value(Value::Null), - self.push_value(Value::Null), - ]; - let text_value = self.lower_text_value(text)?; - text_slots[text_slot - 1] = if matches!(self.value(text_value), Value::String(_)) { - self.push_call("customString", vec![text_value]) - } else { - text_value - }; - let mut colors = [ - self.push_value(Value::Null), - self.push_value(Value::Null), - self.push_value(Value::Null), - ]; - colors[text_slot - 1] = self.lower_value(color)?; - let args = vec![ - visible_to, - text_slots[0], - text_slots[1], - text_slots[2], - self.lower_value(position)?, - self.lower_value(sort_order)?, - colors[0], - colors[1], - colors[2], - self.lower_value(reevaluation)?, - self.lower_value(spectators)?, - ]; - let args = self.normalize_contextual_arguments("createHudText", args); - let text_span = text.span().copied(); - let color_span = color.span().copied(); - Ok(self.push_call_action_with_spans( - "createHudText", - &args, - [ - visible_to_span, - (text_slot == 1).then_some(text_span).flatten(), - (text_slot == 2).then_some(text_span).flatten(), - (text_slot == 3).then_some(text_span).flatten(), - position.span().copied(), - sort_order.span().copied(), - (text_slot == 1).then_some(color_span).flatten(), - (text_slot == 2).then_some(color_span).flatten(), - (text_slot == 3).then_some(color_span).flatten(), - reevaluation.span().copied(), - spectators.span().copied(), - ], - )) - } - - fn lower_hud_visible_to(&mut self, expr: &Expr) -> Result { - if let Expr::Call { name, args, .. } = expr { - if name == "getAllPlayers" && args.is_empty() { - return Ok(self.lower_all_players()); - } - } - self.lower_value(expr) - } - - fn lower_all_players(&mut self) -> ValueId { - let all_teams = self.push_value(Value::Enum { - value_type: "Team".to_string(), - value: "ALL".to_string(), - }); - self.push_call("allPlayers", vec![all_teams]) - } - - fn lower_receiver_action_call( - &mut self, - receiver: &Expr, - name: &str, - args: &[Expr], - span: Option, - ) -> Result { - let function = self - .compiler - .manifest - .resolve_member(name) - .ok_or_else(|| self.unsupported(format!("unknown member action '{name}'"), span))?; - if !matches!(function.kind, FunctionKind::MemberAction) { - return Err(self.unsupported(format!("'{name}' is not a member action"), span)); - } - - // `append` is an OPY mutation, represented by the canonical variable - // modify actions rather than a catalog action call. - if matches!(function.id.as_str(), "append" | "remove") { - let [value] = args else { - return Err(self.unsupported( - format!("{} requires exactly one argument", function.id), - span, - )); - }; - let op = if function.id == "append" { - ModifyOp::AppendToArray - } else { - ModifyOp::RemoveFromArrayByValue - }; - let value_span = value.span().copied(); - let value = self.lower_value(value)?; - return match receiver { - Expr::GlobalVar { - name, - span: target_span, - } => { - let variable = *self.globals.get(name).ok_or_else(|| { - self.unsupported(format!("unknown global variable '{name}'"), *target_span) - })?; - let action = self.push_action(Action::ModifyGlobalVariable { - variable: self.global_names[variable].clone(), - op, - value, - }); - self.mark_action_argument_origins(action, [value_span]); - Ok(action) - } - Expr::PlayerVar { - player, - name, - span: target_span, - .. - } => { - let variable = *self.players.get(name).ok_or_else(|| { - self.unsupported(format!("unknown player variable '{name}'"), *target_span) - })?; - let player_span = player.span().copied(); - let player = self.lower_value(player)?; - let action = self.push_action(Action::ModifyPlayerVariable { - player, - variable: self.player_names[variable].clone(), - op, - value, - }); - self.mark_action_argument_origins(action, [player_span, value_span]); - Ok(action) - } - Expr::Index { array, index, .. } => { - let op_name = if function.id == "append" { - "appendToArray" - } else { - "removeFromArray" - }; - let op_node = self.push_call(op_name, Vec::new()); - let index_span = index.span().copied(); - let target_span = array.span().copied(); - let index = self.lower_value(index)?; - match array.as_ref() { - Expr::GlobalVar { - name, - span: array_span, - } => { - let variable = *self.globals.get(name).ok_or_else(|| { - self.unsupported( - format!("unknown global variable '{name}'"), - *array_span, - ) - })?; - let variable = self.push_value(Value::GlobalVariable( - self.global_names[variable].clone(), - )); - let args = self.normalize_contextual_arguments( - "modifyGlobalVariableAtIndex", - vec![variable, index, op_node, value], - ); - let action = - self.push_call_action("modifyGlobalVariableAtIndex", &args); - self.mark_action_argument_origins( - action, - [target_span, index_span, None, value_span], - ); - return Ok(action); - } - Expr::PlayerVar { - player, - name, - span: array_span, - .. - } => { - let variable = *self.players.get(name).ok_or_else(|| { - self.unsupported( - format!("unknown player variable '{name}'"), - *array_span, - ) - })?; - let player = self.lower_value(player)?; - let variable = self.push_value(Value::PlayerVariable { - player, - variable: self.player_names[variable].clone(), - }); - let args = self.normalize_contextual_arguments( - "modifyPlayerVariableAtIndex", - vec![variable, index, op_node, value], - ); - let action = - self.push_call_action("modifyPlayerVariableAtIndex", &args); - self.mark_action_argument_origins( - action, - [target_span, index_span, None, value_span], - ); - return Ok(action); - } - _ => { - return Err(self.unsupported( - format!( - "{} requires a global or player variable receiver", - function.id - ), - receiver.span().copied().or(span), - )); - } - } - } - _ => Err(self.unsupported( - format!( - "{} requires a global or player variable receiver", - function.id - ), - receiver.span().copied().or(span), - )), - }; - } - - let catalog_id = function.catalog_id.as_ref().ok_or_else(|| { - self.unsupported( - format!( - "member action '{}' has no canonical catalog identity", - function.id - ), - span, - ) - })?; - let argument_spans = std::iter::once(receiver.span().copied()) - .chain(args.iter().map(|arg| arg.span().copied())) - .collect::>(); - let mut lowered = Vec::with_capacity(args.len() + 1); - lowered.push(self.lower_value(receiver)?); - lowered.extend(self.lower_values(args.iter())?); - let mut args = self.normalize_contextual_arguments(catalog_id, lowered); - self.apply_replacements_to_values(catalog_id, &mut args, span); - Ok(self.push_call_action_with_spans(catalog_id.clone(), &args, argument_spans)) - } - - fn lower_values<'expr>( - &mut self, - expressions: impl IntoIterator, - ) -> Result, IntegrationError> { - expressions - .into_iter() - .map(|expr| self.lower_value(expr)) - .collect() - } - - fn lower_value(&mut self, expr: &Expr) -> Result { - let value_id = self.lower_value_unoptimized(expr)?; - let optimization = self.optimization_state_at(expr.span()); - if optimization.enabled { - self.optimized_nodes - .entry(value_id) - .or_insert(optimization.strict); - } - Ok(value_id) - } - - fn lower_value_unoptimized(&mut self, expr: &Expr) -> Result { - let span = expr.span().copied(); - let optimization = self.optimization_state_at(span.as_ref()); - if optimization.enabled - && !optimization.strict - && (matches!(expr, Expr::Binary { .. } | Expr::Unary { .. }) - || matches!(expr, Expr::Call { name, .. } if matches!(name.as_str(), "len" | "countOf"))) - { - let bindings = HashMap::new(); - let mut stack = Vec::new(); - if let Some(value) = - crate::compile_time::evaluate(expr, &self.constants, &bindings, &mut stack) - { - match value { - crate::compile_time::Value::Number(value) if value.is_finite() => { - return Ok(self.push_number(value)); - } - crate::compile_time::Value::String(value) => { - return Ok(self.lower_custom_string(value)); - } - crate::compile_time::Value::Bool(value) => { - return Ok(self.push_value(Value::Bool(value))); - } - crate::compile_time::Value::Array(_) - | crate::compile_time::Value::Object(_) - | crate::compile_time::Value::Number(_) => {} - } - } - } - let value = match expr { - Expr::Number { value, .. } => Value::Number(*value), - Expr::String { value, .. } => { - return Ok(self.lower_custom_string(value.clone())); - } - Expr::Bool { value, .. } => Value::Bool(*value), - Expr::Null { .. } => Value::Null, - Expr::Local { name, .. } => { - let binding = self.array_bindings.iter().rev().find(|binding| { - binding.element == *name || binding.index.as_deref() == Some(name) - }); - match binding { - Some(binding) if binding.element == *name => { - return Ok(self.push_call("currentArrayElement", Vec::new())); - } - Some(_) => return Ok(self.push_call("currentArrayIndex", Vec::new())), - None => { - return Err(self.unsupported( - format!("local '{name}' is not inside a supported array callback"), - span, - )); - } - } - } - Expr::Type { .. } => { - return Err(self.unsupported( - "type expressions are only valid as createWorkshopSetting type arguments", - span, - )); - } - Expr::GlobalVar { name, .. } => { - let id = *self.globals.get(name).ok_or_else(|| { - self.unsupported(format!("unknown global variable '{name}'"), span) - })?; - Value::GlobalVariable(self.global_names[id].clone()) - } - Expr::PlayerVar { player, name, .. } => { - let player = self.lower_value(player)?; - let id = *self.players.get(name).ok_or_else(|| { - self.unsupported(format!("unknown player variable '{name}'"), span) - })?; - Value::PlayerVariable { - player, - variable: self.player_names[id].clone(), - } - } - Expr::EventPlayer { .. } => Value::EventPlayer, - Expr::HostPlayer { .. } => Value::Call { - name: "hostPlayer".to_string(), - args: Vec::new(), - }, - Expr::Enum { - value_type, value, .. - } => { - let value = match (value_type.as_str(), value.as_str()) { - ("Clipping", "NONE") => "DO_NOT_CLIP", - ("Clipping", "SURFACES") => "CLIP_AGAINST_SURFACES", - _ => value, - }; - if self - .compiler - .catalog - .enum_spelling(value_type, &Locale::new("en-US"), value) - .is_none() - { - return Err(self.unsupported( - format!("unknown catalog enum member '{value_type}.{value}'"), - span, - )); - } - let member = Value::Enum { - value_type: value_type.clone(), - value: value.to_string(), - }; - if value_type == "Gamemode" { - let member = self.push_value(member); - Value::Call { - name: "gameMode".to_string(), - args: vec![member], - } - } else { - member - } - } - Expr::Array { elements, .. } => { - let elements = self.lower_values(elements)?; - return Ok(self.lower_array(elements)); - } - Expr::Vector { x, y, z, .. } => { - let x = self.lower_value(x)?; - let y = self.lower_value(y)?; - let z = self.lower_value(z)?; - if let Some(member) = self.canonical_vector_member(x, y, z) { - Value::Enum { - value_type: "Vector".to_string(), - value: member.to_string(), - } - } else { - Value::Call { - name: "vector".to_string(), - args: self.value_args(&[x, y, z]), - } - } - } - Expr::Constant { name, .. } => { - let const_expr = *self - .constants - .get(name) - .ok_or_else(|| self.unsupported(format!("unknown constant '{name}'"), span))?; - return self.lower_value(const_expr); - } - Expr::Index { array, index, .. } => { - if let Expr::Dict { entries, .. } = array.as_ref() - && is_literal_key(index) - && entries.iter().all(|entry| is_literal_key(&entry.key)) - { - if let Some(value) = entries - .iter() - .find(|entry| literal_key_matches(&entry.key, index)) - .map(|entry| &entry.value) - { - return self.lower_value(value); - } - return Ok(self.push_value(Value::Null)); - } - // The pinned OverPy oracle lowers a literal zero-index read - // (`arr[0]`, `arr[0.0]`) to `firstOf(arr)`; non-zero indexes - // and indexed writes keep the indexed forms. - if matches!(index.as_ref(), Expr::Number { value, .. } if *value == 0.0) { - let array = self.lower_value(array)?; - Value::Call { - name: "firstOf".to_string(), - args: self.value_args(&[array]), - } - } else { - let array = self.lower_value(array)?; - let index = self.lower_value(index)?; - Value::Call { - name: "valueInArray".to_string(), - args: self.value_args(&[array, index]), - } - } - } - Expr::Format { text, args, .. } => { - let (format_text, dynamic_args) = self.fold_format_constants(text, args); - if dynamic_args.is_empty() { - let value = format_text; - return Ok(self.lower_custom_string(value)); - } - if dynamic_args.len() <= 3 { - let text_node = self.push_value(Value::String(format_text)); - let mut call_args = vec![text_node]; - for arg in dynamic_args { - let arg = self.lower_value(arg)?; - call_args.push(arg); - } - Value::Call { - name: "customString".to_string(), - args: call_args, - } - } else { - let chunks = split_format_chunks(&format_text, dynamic_args.len()).ok_or_else(|| { - self.unsupported( - "format strings with more than three replacements require sequential placeholders", - span, - ) - })?; - let lowered_args = dynamic_args.iter().copied(); - let lowered_args = self.lower_values(lowered_args)?; - let mut parts = Vec::with_capacity(chunks.len()); - for (chunk, indices) in chunks { - let text = self.push_value(Value::String(chunk)); - let mut call_args = vec![text]; - call_args.extend(indices.into_iter().map(|index| lowered_args[index])); - let call_args = - self.normalize_contextual_arguments("customString", call_args); - parts.push(self.push_value(Value::Call { - name: "customString".to_string(), - args: call_args, - })); - } - let separator = self.push_value(Value::String("{0}{1}".to_string())); - let mut value = parts[0]; - for part in parts.into_iter().skip(1) { - value = self.push_call("customString", vec![separator, value, part]); - } - return Ok(value); - } - } - Expr::Conditional { - then_value, - condition, - else_value, - .. - } => Value::Call { - name: "ifThenElse".to_string(), - args: { - let condition = self.lower_value(condition)?; - let then_value = self.lower_value(then_value)?; - let else_value = self.lower_value(else_value)?; - self.value_args(&[condition, then_value, else_value]) - }, - }, - Expr::Binary { - op, left, right, .. - } => { - if self.optimization_state_at(span.as_ref()).enabled - && matches!(op.as_str(), "in" | "not in") - { - if let Expr::Array { elements, .. } = right.as_ref() { - if elements - .iter() - .any(|elem| literal_key_matches(elem, left.as_ref())) - { - return Ok(self.push_value(Value::Bool(op == "in"))); - } - let strict = self.strict_optimization_active(expr); - if is_membership_literal(left.as_ref(), strict) - && elements - .iter() - .all(|elem| is_membership_literal(elem, strict)) - { - return Ok(self.push_value(Value::Bool(op == "not in"))); - } - } - } - if op == "==" && self.optimization_state_at(span.as_ref()).enabled { - if let Some(value) = self.lower_current_map_equality(left, right) { - return Ok(value); - } - } - let left = self.lower_value(left)?; - let right = self.lower_value(right)?; - if self.optimization_state_at(span.as_ref()).enabled - && let Some(value) = self.fold_numeric_binary(op, left, right) - { - Value::Number(value) - } else { - if op == "in" { - Value::Call { - name: "arrayContains".to_string(), - args: self.value_args(&[right, left]), - } - } else if op == "not in" { - let contains = self.push_call("arrayContains", vec![right, left]); - Value::Call { - name: "not".to_string(), - args: self.value_args(&[contains]), - } - } else { - let name = match op.as_str() { - "==" | "!=" | "<" | "<=" | ">" | ">=" | "and" | "or" => op, - "+" => "add", - "-" => "subtract", - "*" => "multiply", - "/" => "divide", - "%" => "modulo", - "**" => "raiseToPower", - _ => { - return Err(self.unsupported( - format!( - "binary operator '{op}' is not currently representable in canonical WIR" - ), - span, - )); - } - }; - Value::Call { - name: name.to_string(), - args: self.value_args(&[left, right]), - } - } - } - } - Expr::Unary { op, operand, .. } => match op.as_str() { - "not" => { - // The pinned OverPy 9.7.10 oracle lowers `not (a == b)` - // to the negated comparison (`a != b`), flipping every - // ordering comparison; `in` membership stays wrapped in - // `not`. Mirror that observable lowering. - if let Expr::Binary { - op: comparison, - left, - right, - .. - } = operand.as_ref() - { - if let Some(negated) = negated_comparison(comparison) { - let left = self.lower_value(left)?; - let right = self.lower_value(right)?; - Value::Call { - name: negated.to_string(), - args: self.value_args(&[left, right]), - } - } else { - let operand = self.lower_value(operand)?; - Value::Call { - name: "not".to_string(), - args: self.value_args(&[operand]), - } - } - } else { - let operand = self.lower_value(operand)?; - Value::Call { - name: "not".to_string(), - args: self.value_args(&[operand]), - } - } - } - "-" => { - let operand = self.lower_value(operand)?; - if let Value::Number(number) = self.value(operand) { - Value::Number(-number) - } else { - Value::Call { - name: "-".to_string(), - args: self.value_args(&[operand]), - } - } - } - "+" => return self.lower_value(operand), - _ => { - return Err(self.unsupported( - format!( - "unary operator '{op}' is not currently representable in canonical WIR" - ), - span, - )); - } - }, - Expr::Call { name, args, .. } => { - if matches!(name.as_str(), "_" | "__" | "___") { - return self.lower_translation(name, args, span); - } - if name == "createWorkshopSetting" { - return self.lower_workshop_setting(args, span); - } - if name == "buttonToString" { - let [button] = args.as_slice() else { - return Err(self.unsupported("buttonToString requires one button", span)); - }; - let button = self.lower_value(button)?; - return Ok(self.push_call("inputBindingString", vec![button])); - } - if matches!( - name.as_str(), - "getRealClosestPlayer" - | "getRealClosestPlayers" - | "getRealFarthestPlayer" - | "getRealFarthestPlayers" - ) { - let [center, team] = args.as_slice() else { - return Err( - self.unsupported(format!("{name} requires center and team"), span) - ); - }; - let center = self.lower_value(center)?; - let team = self.lower_value(team)?; - let players = self.push_call("getLivingPlayers", vec![team]); - let current = self.push_call("currentArrayElement", Vec::new()); - let spawned = self.push_call("hasSpawned", vec![current]); - let players = self.push_call("filteredArray", vec![players, spawned]); - let distance = self.push_call("distance", vec![current, center]); - let key = if matches!( - name.as_str(), - "getRealFarthestPlayer" | "getRealFarthestPlayers" - ) { - let negative_one = self.push_number(-1.0); - self.push_call("multiply", vec![negative_one, distance]) - } else { - distance - }; - let sorted = self.push_call("sortedArray", vec![players, key]); - return if matches!( - name.as_str(), - "getRealClosestPlayer" | "getRealFarthestPlayer" - ) { - Ok(self.push_call("firstOf", vec![sorted])) - } else { - Ok(sorted) - }; - } - if name == "getRealPlayersInRadius" { - let lowered = self.lower_values(args)?; - let players = self.push_call("getPlayersInRadius", lowered); - let current = self.push_call("currentArrayElement", Vec::new()); - let alive = self.push_call("isAlive", vec![current]); - let spawned = self.push_call("hasSpawned", vec![current]); - let condition = self.push_call("and", vec![alive, spawned]); - return Ok(self.push_call("filteredArray", vec![players, condition])); - } - if name == "lineIntersectsSphere" { - let [line_start, line_direction, sphere_center, sphere_radius] = - args.as_slice() - else { - return Err( - self.unsupported("lineIntersectsSphere requires four arguments", span) - ); - }; - let line_start = self.lower_value(line_start)?; - let line_direction = self.lower_value(line_direction)?; - let sphere_center = self.lower_value(sphere_center)?; - let sphere_radius = self.lower_value(sphere_radius)?; - let center_direction = - self.push_call("subtract", vec![sphere_center, line_start]); - let angle = self.push_call( - "angleBetweenVectors", - vec![line_direction, center_direction], - ); - let distance = self.push_call("distance", vec![line_start, sphere_center]); - let ratio = self.push_call("divide", vec![sphere_radius, distance]); - let limit = self.push_call("asinDeg", vec![ratio]); - return Ok(self.push_call("<=", vec![angle, limit])); - } - if name == "arrayToString" { - let (array, max_length) = match args.as_slice() { - [array] => (array, 12), - [array, Expr::Number { value, .. }] => { - if !value.is_finite() || *value < 0.0 || value.fract() != 0.0 { - return Err(self.unsupported( - "arrayToString maxLength must be a non-negative integer literal", - span, - )); - } - (array, (*value).min(1000.0) as usize) - } - _ => return Err(self.unsupported("arrayToString requires an array", span)), - }; - let array = self.lower_value(array)?; - return Ok(self.lower_debug_array_text(array, max_length)); - } - if matches!(name.as_str(), "decompressNumbers" | "decompressVectors") { - let [text] = args.as_slice() else { - return Err(self.unsupported(format!("{name} requires one string"), span)); - }; - return self.lower_decompression(text, name == "decompressVectors"); - } - if name == "strVisualLength" { - let [Expr::String { value, .. }] = args.as_slice() else { - return Err( - self.unsupported("strVisualLength requires one literal string", span) - ); - }; - let width = value.chars().map(blizzard_global::width).sum::(); - return Ok(self.push_number(width as f64)); - } - if name == "spacesForLength" { - let [Expr::Number { value, .. }] = args.as_slice() else { - return Err( - self.unsupported("spacesForLength requires one literal number", span) - ); - }; - if !value.is_finite() || *value < 0.0 || value.fract() != 0.0 { - return Err(self.unsupported( - "spacesForLength requires a non-negative integer literal", - span, - )); - } - return Ok(self.lower_custom_string(blizzard_global::spaces(*value as i32))); - } - if name == "spacesForString" { - let [Expr::String { value, .. }] = args.as_slice() else { - if let [ - Expr::Call { - name: translation, - args: translation_args, - .. - }, - ] = args.as_slice() - && matches!(translation.as_str(), "_" | "__" | "___") - && let Some(text) = translation_args.last() - && let Expr::String { - value, - span: text_span, - } = text - { - let replacement = Expr::String { - value: blizzard_global::spaces( - value.chars().map(blizzard_global::width).sum(), - ), - span: *text_span, - }; - let mut translated_args = translation_args.clone(); - *translated_args.last_mut().expect("translation text exists") = - replacement; - return self.lower_value(&Expr::Call { - name: translation.clone(), - args: translated_args, - debug_source: None, - span, - }); - } - return Err( - self.unsupported("spacesForString requires one literal string", span) - ); - }; - return Ok(self.lower_custom_string(blizzard_global::spaces( - value.chars().map(blizzard_global::width).sum(), - ))); - } - if name == "hsl" { - let (hue, saturation, lightness, alpha) = match args.as_slice() { - [hue, saturation, lightness] => (hue, saturation, lightness, None), - [hue, saturation, lightness, alpha] => { - (hue, saturation, lightness, Some(alpha)) - } - _ => { - return Err( - self.unsupported("hsl requires three or four arguments", span) - ); - } - }; - let hue = self.lower_value(hue)?; - let saturation = self.lower_value(saturation)?; - let lightness = self.lower_value(lightness)?; - let alpha = match alpha { - Some(alpha) => self.lower_value(alpha)?, - None => self.push_number(255.0), - }; - let one = self.push_number(1.0); - let thirty = self.push_number(30.0); - let hue_thirtieths = self.push_call("divide", vec![hue, thirty]); - let lightness_complement = self.push_call("subtract", vec![one, lightness]); - let lightness_limit = - self.push_call("min", vec![lightness, lightness_complement]); - let channel = |this: &mut Self, offset: f64| { - let offset = this.push_number(offset); - let phase = this.push_call("add", vec![offset, hue_thirtieths]); - let twelve = this.push_number(12.0); - let phase = this.push_call("modulo", vec![phase, twelve]); - let three = this.push_number(3.0); - let lower = this.push_call("subtract", vec![phase, three]); - let nine = this.push_number(9.0); - let upper = this.push_call("subtract", vec![nine, phase]); - let clamped = this.push_call("min", vec![lower, upper]); - let negative_one = this.push_number(-1.0); - let clamped = this.push_call("max", vec![clamped, negative_one]); - let saturation_limit = - this.push_call("multiply", vec![saturation, lightness_limit]); - let adjustment = - this.push_call("multiply", vec![saturation_limit, clamped]); - let value = this.push_call("subtract", vec![lightness, adjustment]); - let scale = this.push_number(255.0); - this.push_call("multiply", vec![scale, value]) - }; - let red = channel(self, 0.0); - let green = channel(self, 8.0); - let blue = channel(self, 4.0); - return Ok(self.push_call("customColor", vec![red, green, blue, alpha])); - } - if name == "timeToString" { - let [time] = args.as_slice() else { - return Err(self.unsupported("timeToString requires one argument", span)); - }; - let time = self.lower_value(time)?; - let three_thousand_six_hundred = self.push_number(3600.0); - let sixty = self.push_number(60.0); - let hour_value = - self.push_call("divide", vec![time, three_thousand_six_hundred]); - let down = self.push_value(Value::Enum { - value_type: "Rounding".to_string(), - value: "DOWN".to_string(), - }); - let hour = self.push_call("roundToInteger", vec![hour_value, down]); - let minute_remainder = - self.push_call("modulo", vec![time, three_thousand_six_hundred]); - let minute_value = self.push_call("divide", vec![minute_remainder, sixty]); - let minute = self.push_call("roundToInteger", vec![minute_value, down]); - let second = self.push_call("modulo", vec![time, sixty]); - let hundred = self.push_number(100.0); - let first_digit = self.push_number(1.0); - let two = self.push_number(2.0); - let minute_with_padding = self.push_call("add", vec![minute, hundred]); - let padding_template = self.push_value(Value::String("{0}".to_string())); - let minute_with_padding = - self.push_call("customString", vec![padding_template, minute_with_padding]); - let minute_text = - self.push_call("stringSlice", vec![minute_with_padding, first_digit, two]); - let second_with_padding = self.push_call("add", vec![second, hundred]); - let second_with_padding = - self.push_call("customString", vec![padding_template, second_with_padding]); - let all_digits = self.push_number(9999.0); - let second_text = self.push_call( - "stringSlice", - vec![second_with_padding, first_digit, all_digits], - ); - let template = self.push_value(Value::String("{0}:{1}:{2}".to_string())); - return Ok(self.push_call( - "customString", - vec![template, hour, minute_text, second_text], - )); - } - if name == "compressed" { - return self.lower_compressed(args, span); - } - if name == "compress" { - return self.lower_compress(args, span); - } - if name == "getSign" { - let [number] = args.as_slice() else { - return Err(self.unsupported("getSign requires one argument", span)); - }; - let number = self.lower_value(number)?; - let zero = self.push_number(0.0); - let positive = self.push_call(">", vec![number, zero]); - let one = self.push_number(1.0); - let negative_one = self.push_number(-1.0); - let sign = self.push_call("ifThenElse", vec![positive, one, negative_one]); - let is_zero = self.push_call("==", vec![number, zero]); - return Ok(self.push_call("ifThenElse", vec![is_zero, zero, sign])); - } - if name == "lerp" { - let [start, end, t] = args.as_slice() else { - return Err(self.unsupported("lerp requires three arguments", span)); - }; - let start = self.lower_value(start)?; - let end = self.lower_value(end)?; - let t = self.lower_value(t)?; - let one = self.push_number(1.0); - let weight = self.push_call("subtract", vec![one, t]); - let start_part = self.push_call("multiply", vec![start, weight]); - let end_part = self.push_call("multiply", vec![end, t]); - return Ok(self.push_call("add", vec![start_part, end_part])); - } - if name == "log" { - let (number, base) = match args.as_slice() { - [number] => (number, None), - [number, base] => (number, Some(base)), - _ => { - return Err(self.unsupported("log requires one or two arguments", span)); - } - }; - let number = self.lower_value(number)?; - let exponent = self.push_number(0.0001); - let powered = self.push_call("raiseToPower", vec![number, exponent]); - let one = self.push_number(1.0); - let delta = self.push_call("subtract", vec![powered, one]); - let scale = self.push_number(10000.0); - let approximation = self.push_call("multiply", vec![scale, delta]); - if let Some(base) = base { - let base = self.lower_value(base)?; - let base_powered = self.push_call("raiseToPower", vec![base, exponent]); - let base_one = self.push_number(1.0); - let base_delta = self.push_call("subtract", vec![base_powered, base_one]); - let base_scale = self.push_number(10000.0); - let base_log = self.push_call("multiply", vec![base_scale, base_delta]); - return Ok(self.push_call("divide", vec![approximation, base_log])); - } - return Ok(approximation); - } - if name == "getCurrentMap" && args.is_empty() && !self.used_maps.is_empty() { - return Ok(self.lower_bugged_current_map()); - } - if matches!(name.as_str(), "attacker" | "victim") && args.is_empty() { - return Ok(self.push_call(name, Vec::new())); - } - if name == "localPlayer" && args.is_empty() { - return Ok(self.push_call(name, Vec::new())); - } - if name == "ruleCondition" { - if !args.is_empty() { - return Err( - self.unsupported("ruleCondition does not accept arguments", span) - ); - } - let conditions = self.current_rule_conditions.clone().ok_or_else(|| { - self.unsupported("ruleCondition is only valid inside a rule", span) - })?; - let Some((first, rest)) = conditions.split_first() else { - return Ok(self.push_value(Value::Bool(true))); - }; - let mut combined = *first; - for condition in rest { - combined = self.push_call("and", vec![combined, *condition]); - } - return Ok(combined); - } - if name == "vect" && args.len() == 3 { - let x = self.lower_value(&args[0])?; - let y = self.lower_value(&args[1])?; - let z = self.lower_value(&args[2])?; - if let Some(member) = self.canonical_vector_member(x, y, z) { - Value::Enum { - value_type: "Vector".to_string(), - value: member.to_string(), - } - } else { - Value::Vector { x, y, z } - } - } else if matches!( - name.as_str(), - "createWorkshopSettingBool" - | "createWorkshopSettingEnum" - | "createWorkshopSettingInt" - | "createWorkshopSettingFloat" - | "createWorkshopSettingHero" - ) { - let mut lowered = self.lower_values(args)?; - // The sort order is the last parameter; OverPy writes 0 when omitted. - let (canonical, arity_without_sort_order) = workshop_setting_call(name); - if lowered.len() == arity_without_sort_order { - lowered.push(self.push_number(0.0)); - } - Value::Call { - name: canonical.to_string(), - args: self.value_args(&lowered), - } - } else if matches!(name.as_str(), "all" | "any") { - let call_name = if name == "all" { - "isTrueForAll" - } else { - "isTrueForAny" - }; - let [array] = args.as_slice() else { - return Err(self.unsupported( - format!("{name} requires exactly one array argument"), - span, - )); - }; - let (array, condition) = match array { - Expr::Comprehension { - element, - variable, - index, - iterable, - .. - } => { - if index.is_some() { - return Err(self.unsupported( - format!("{name} does not support an index binder"), - span, - )); - } - let iterable = self.lower_value(iterable)?; - self.array_bindings.push(ArrayBinding { - element: variable.clone(), - index: None, - }); - let condition = self.lower_value(element); - self.array_bindings.pop(); - (iterable, condition?) - } - array => ( - self.lower_value(array)?, - self.push_call("currentArrayElement", Vec::new()), - ), - }; - Value::Call { - name: call_name.to_string(), - args: self.value_args(&[array, condition]), - } - } else if matches!(name.as_str(), "ceil" | "floor" | "round") { - let [value] = args.as_slice() else { - return Err(self.unsupported( - format!("{name} requires exactly one numeric argument"), - span, - )); - }; - let rounding = match name.as_str() { - "ceil" => "UP", - "floor" => "DOWN", - "round" => "NEAREST", - _ => unreachable!(), - }; - let rounding = self.push_value(Value::Enum { - value_type: "Rounding".to_string(), - value: rounding.to_string(), - }); - let value = self.lower_value(value)?; - Value::Call { - name: "roundToInteger".to_string(), - args: self.value_args(&[value, rounding]), - } - } else if name == "sorted" { - let (array, key) = match args.as_slice() { - [array] => ( - self.lower_value(array)?, - self.push_call("currentArrayElement", Vec::new()), - ), - [ - array, - Expr::Lambda { - params, body, span, .. - }, - ] => { - let array = self.lower_value(array)?; - let key = self.lower_array_callback(params, body, *span)?; - (array, key) - } - _ => { - return Err(self.unsupported( - "sorted requires an array and an optional lambda key", - span, - )); - } - }; - Value::Call { - name: "sortedArray".to_string(), - args: self.value_args(&[array, key]), - } - } else { - let function = self - .compiler - .manifest - .resolve_function(name) - .ok_or_else(|| self.unsupported(format!("unknown value '{name}'"), span))?; - if !matches!(function.kind, FunctionKind::Value) { - return Err( - self.unsupported(format!("'{name}' is not a generic OPY value"), span) - ); - } - let catalog_id = function.catalog_id.as_ref().ok_or_else(|| { - self.unsupported( - format!( - "value '{}' requires a special lowering not in #46", - function.id - ), - span, - ) - })?; - if function.id == "getAllPlayers" { - return Ok(self.lower_all_players()); - } - let lowered_args = self.lower_values(args)?; - Value::Call { - name: catalog_id.clone(), - args: self.value_args(&lowered_args), - } - } - } - Expr::ReceiverCall { - receiver, - name, - args, - .. - } => { - if name == "getOppositeTeam" { - if !args.is_empty() { - return Err(self.unsupported("getOppositeTeam requires no arguments", span)); - } - let receiver = self.lower_value(receiver)?; - let team = self.push_call("teamOf", vec![receiver]); - return Ok(self.push_call("oppositeTeamOf", vec![team])); - } - if name == "toArray" { - if !args.is_empty() { - return Err(self.unsupported("toArray requires no arguments", span)); - } - let Expr::Type { - name: type_name, .. - } = receiver.as_ref() - else { - return Err( - self.unsupported("toArray requires an enum type receiver", span) - ); - }; - let domain_name = match type_name.as_str() { - "Clip" => "Clipping", - _ => type_name.as_str(), - }; - let Some(domain) = self.compiler.catalog.enum_domain(domain_name) else { - return Err( - self.unsupported(format!("unknown enum type '{type_name}'"), span) - ); - }; - let values = domain - .members - .iter() - .map(|member| { - self.push_value(Value::Enum { - value_type: domain_name.to_string(), - value: member.member.clone(), - }) - }) - .collect(); - return Ok(self.push_call("array", values)); - } - if matches!(name.as_str(), "all" | "any") { - let receiver = self.lower_value(receiver)?; - let condition = match args.as_slice() { - [] => self.push_call("currentArrayElement", Vec::new()), - [ - Expr::Lambda { - params, body, span, .. - }, - ] => self.lower_array_callback(params, body, *span)?, - _ => { - return Err(self.unsupported( - format!("{name} requires zero or one lambda argument"), - span, - )); - } - }; - let args = self.value_args(&[receiver, condition]); - return Ok(self.push_value(Value::Call { - name: if name == "all" { - "isTrueForAll" - } else { - "isTrueForAny" - } - .to_string(), - args, - })); - } - let function = self.compiler.manifest.resolve_member(name).ok_or_else(|| { - self.unsupported(format!("unknown member value '{name}'"), span) - })?; - if !matches!(function.kind, FunctionKind::MemberValue) { - return Err(self.unsupported(format!("'{name}' is not a member value"), span)); - } - if function.id == "unique" { - if !args.is_empty() { - return Err(self.unsupported("unique requires no arguments", span)); - } - let receiver = self.lower_value(receiver)?; - let current_element = self.push_call("currentArrayElement", Vec::new()); - let first_index = - self.push_call("indexOfArrayValue", vec![receiver, current_element]); - let current_index = self.push_call("currentArrayIndex", Vec::new()); - let condition = self.push_call("==", vec![first_index, current_index]); - return Ok(self.push_call("filteredArray", vec![receiver, condition])); - } - if function.id == "reverse" { - if !args.is_empty() { - return Err(self.unsupported("reverse requires no arguments", span)); - } - let receiver = self.lower_value(receiver)?; - let index = self.push_call("currentArrayIndex", Vec::new()); - let key = self.push_call("-", vec![index]); - return Ok(self.push_call("sortedArray", vec![receiver, key])); - } - if function.id == "getEffectiveHero" { - if !args.is_empty() { - return Err( - self.unsupported("getEffectiveHero requires no arguments", span) - ); - } - let receiver = self.lower_value(receiver)?; - let duplicated = self.push_call("getHeroOfDuplication", vec![receiver]); - let hero = self.push_call("getHero", vec![receiver]); - let null = self.push_value(Value::Null); - let condition = self.push_call("==", vec![duplicated, null]); - return Ok(self.push_call("ifThenElse", vec![condition, hero, duplicated])); - } - if function.id == "getRealPlayersInViewAngle" { - let [team, view_angle] = args.as_slice() else { - return Err(self.unsupported( - "getRealPlayersInViewAngle requires team and view angle", - span, - )); - }; - let receiver = self.lower_value(receiver)?; - let team = self.lower_value(team)?; - let view_angle = self.lower_value(view_angle)?; - let players = - self.push_call("getPlayersInViewAngle", vec![receiver, team, view_angle]); - let current = self.push_call("currentArrayElement", Vec::new()); - let alive = self.push_call("isAlive", vec![current]); - let spawned = self.push_call("hasSpawned", vec![current]); - let condition = self.push_call("and", vec![alive, spawned]); - return Ok(self.push_call("filteredArray", vec![players, condition])); - } - if matches!( - function.id.as_str(), - "getRealPlayerClosestToReticle" | "getRealPlayersClosestToReticle" - ) { - let [team] = args.as_slice() else { - return Err(self - .unsupported("getRealPlayersClosestToReticle requires a team", span)); - }; - let receiver = self.lower_value(receiver)?; - let team = self.lower_value(team)?; - let players = self.push_call("getLivingPlayers", vec![team]); - let current = self.push_call("currentArrayElement", Vec::new()); - let spawned = self.push_call("hasSpawned", vec![current]); - let not_self = self.push_call("!=", vec![current, receiver]); - let condition = self.push_call("and", vec![spawned, not_self]); - let players = self.push_call("filteredArray", vec![players, condition]); - let facing = self.push_call("getFacingDirection", vec![receiver]); - let eye_position = self.push_call("getEyePosition", vec![receiver]); - let direction = self.push_call("subtract", vec![current, eye_position]); - let angle = self.push_call("angleBetweenVectors", vec![facing, direction]); - let sorted = self.push_call("sortedArray", vec![players, angle]); - return if function.id == "getRealPlayerClosestToReticle" { - Ok(self.push_call("firstOf", vec![sorted])) - } else { - Ok(sorted) - }; - } - if function.id == "map" { - let [ - Expr::Lambda { - params, body, span, .. - }, - ] = args.as_slice() - else { - return Err(self.unsupported("map requires one lambda argument", span)); - }; - let mapped = self.lower_array_callback(params, body, *span)?; - let receiver = self.lower_value(receiver)?; - return Ok(self.push_call("mappedArray", vec![receiver, mapped])); - } - if matches!( - function.id.as_str(), - "getHitPosition" | "getPlayerHit" | "getNormal" - ) { - let member_name = function.id.as_str(); - let Expr::Call { - name: receiver_name, - args: receiver_args, - .. - } = receiver.as_ref() - else { - return Err(self.unsupported( - format!("{member_name} requires a raycast receiver"), - span, - )); - }; - if receiver_name != "raycast" || !args.is_empty() { - return Err(self.unsupported( - format!("{member_name} requires raycast(...) with no member arguments"), - span, - )); - } - let catalog_id = function.catalog_id.clone().ok_or_else(|| { - self.unsupported( - format!("{member_name} has no canonical catalog identity"), - span, - ) - })?; - let lowered_args = self.lower_values(receiver_args)?; - return Ok(self.push_value(Value::Call { - name: catalog_id, - args: self.value_args(&lowered_args), - })); - } - if function.id == "filter" { - let [ - Expr::Lambda { - params, body, span, .. - }, - ] = args.as_slice() - else { - return Err(self.unsupported("filter requires one lambda argument", span)); - }; - let condition = self.lower_array_callback(params, body, *span)?; - let receiver = self.lower_value(receiver)?; - Value::Call { - name: "filteredArray".to_string(), - args: self.value_args(&[receiver, condition]), - } - } else if matches!(function.id.as_str(), "concat" | "exclude") { - let [value] = args.as_slice() else { - return Err(self.unsupported( - format!("{} requires exactly one argument", function.id), - span, - )); - }; - let receiver = self.lower_value(receiver)?; - let value = self.lower_value(value)?; - Value::Call { - name: if function.id == "concat" { - "appendToArray" - } else { - "removeFromArray" - } - .to_string(), - args: self.value_args(&[receiver, value]), - } - } else { - let catalog_id = function.catalog_id.as_ref().ok_or_else(|| { - self.unsupported( - format!( - "member value '{}' has no canonical catalog identity", - function.id - ), - span, - ) - })?; - let mut lowered = Vec::with_capacity(args.len() + 1); - lowered.push(self.lower_value(receiver)?); - lowered.extend(self.lower_values(args)?); - Value::Call { - name: catalog_id.clone(), - args: self.value_args(&lowered), - } - } - } - Expr::Member { - receiver, member, .. - } => { - let receiver = self.lower_value(receiver)?; - if let Some(name) = match member.as_str() { - "x" => Some("__xComponentOf__"), - "y" => Some("__yComponentOf__"), - "z" => Some("__zComponentOf__"), - _ => None, - } { - Value::Call { - name: name.to_string(), - args: self.value_args(&[receiver]), - } - } else { - let member = self.push_value(Value::String(member.clone())); - Value::Call { - name: "memberAccess".to_string(), - args: self.value_args(&[receiver, member]), - } - } - } - Expr::Comprehension { - element, - variable, - index, - iterable, - condition, - span: comprehension_span, - .. - } => { - if condition.is_some() && index.is_some() { - return Err(self.unsupported( - "comprehensions with both a filter and an index binder are not currently representable in canonical WIR", - *comprehension_span, - )); - } - let iterable = self.lower_value(iterable)?; - let iterable = if self.value_is_known_player(iterable) { - self.push_call("array", vec![iterable]) - } else { - iterable - }; - let binding = ArrayBinding { - element: variable.clone(), - index: index.clone(), - }; - self.array_bindings.push(binding); - let predicate = condition - .as_deref() - .map(|condition| self.lower_value(condition)); - let element = self.lower_value(element); - self.array_bindings.pop(); - let element = element?; - let iterable = if let Some(predicate) = predicate { - let predicate = predicate?; - let filtered = self.push_call("filteredArray", vec![iterable, predicate]); - let optimization = self.optimization_state_at(comprehension_span.as_ref()); - if optimization.enabled { - self.optimized_nodes.insert(filtered, optimization.strict); - } - filtered - } else { - iterable - }; - Value::Call { - name: "mappedArray".to_string(), - args: self.value_args(&[iterable, element]), - } - } - Expr::Lambda { span, .. } => { - return Err(self.unsupported( - "lambda expressions are only representable as supported array operation arguments", - *span, - )); - } - Expr::StringModifier { - modifier, - value, - span, - } => { - let value = match modifier.as_str() { - "b" => big_letters(value), - "c" => case_sensitive(value), - "w" => fullwidth(value), - _ => { - return Err(self.unsupported( - format!( - "string modifier '{modifier}' is not currently representable in canonical WIR" - ), - *span, - )); - } - }; - return Ok(self.lower_custom_string(value)); - } - _ => { - return Err(self.unsupported( - format!( - "expression '{}' is not currently representable in canonical WIR", - expr.kind_name() - ), - span, - )); - } - }; - let value_id = self.push_value(value); - let Some(Value::Call { name, args }) = self.values.get(value_id) else { - return Ok(value_id); - }; - let name = name.clone(); - let args = args.clone(); - let mut args = self.normalize_contextual_arguments(&name, args); - self.apply_replacements_to_values(&name, &mut args, span); - if let Some(Value::Call { - args: target_args, .. - }) = self.values.get_mut(value_id) - { - *target_args = args; - } - Ok(value_id) - } - - /// `getCurrentMap() == Map.X`: the maps whose value comparison the - /// Workshop gets wrong are compared as text instead. - fn lower_current_map_equality(&mut self, left: &Expr, right: &Expr) -> Option { - let is_current_map = |expr: &Expr| matches!(expr, Expr::Call { name, args, .. } if name == "getCurrentMap" && args.is_empty()); - let map_of = |expr: &Expr| match expr { - Expr::Enum { - value_type, value, .. - } if value_type == "Map" => Some(value.clone()), + fn fold_numeric_binary(&self, op: &str, left: ValueId, right: ValueId) -> Option { + let number = |id| match self.values.get(id)? { + Value::Number(value) => Some(*value), _ => None, }; - let map = match (map_of(left), map_of(right)) { - (Some(map), None) if is_current_map(right) => map, - (None, Some(map)) if is_current_map(left) => map, + let left = number(left)?; + let right = number(right)?; + let value = match op { + "+" => left + right, + "-" => left - right, + "*" => left * right, + "/" if right != 0.0 => left / right, + "%" if right != 0.0 => left % right, + "**" => left.powf(right), _ => return None, }; - let current = self.push_call("currentMap", Vec::new()); - let map_value = self.push_value(Value::Enum { - value_type: "Map".to_string(), - value: map.clone(), - }); - if !TEXT_COMPARED_MAPS.contains(&map.as_str()) { - return Some(self.push_call("==", vec![current, map_value])); - } - let format = self.push_value(Value::String("{0}".to_string())); - let current_text = self.push_call("customString", vec![format, current]); - let format = self.push_value(Value::String("{0}".to_string())); - let map_text = self.push_call("customString", vec![format, map_value]); - Some(self.push_call("==", vec![current_text, map_text])) - } - - /// A bare `getCurrentMap()` selects the used map from the bugged ones by - /// its text, since comparing the map values themselves fails for them. - fn lower_bugged_current_map(&mut self) -> ValueId { - let mut maps: Vec = self - .used_maps - .clone() - .into_iter() - .map(|map| { - self.push_value(Value::Enum { - value_type: "Map".to_string(), - value: map.to_string(), - }) - }) - .collect(); - maps.push(self.push_call("currentMap", Vec::new())); - let candidates = self.push_call("array", maps); - let current = self.push_call("currentMap", Vec::new()); - let format = self.push_value(Value::String("{0}".to_string())); - let current_text = self.push_call("customString", vec![format, current]); - let element = self.push_call("currentArrayElement", Vec::new()); - let empty = self.push_call("emptyArray", Vec::new()); - let element_text = self.push_call("stringSplit", vec![element, empty]); - let matches = self.push_call("==", vec![current_text, element_text]); - let filtered = self.push_call("filteredArray", vec![candidates, matches]); - self.push_call("firstOf", vec![filtered]) - } - - fn apply_replacements_to_values( - &mut self, - call_id: &str, - args: &mut [ValueId], - span: Option, - ) { - for value in args { - *value = self.apply_replacement(*value, call_id, span); - } - } - - fn apply_replacement( - &mut self, - value_id: ValueId, - call_id: &str, - span: Option, - ) -> ValueId { - let optimization = self.optimization_state_at(span.as_ref()); - if !optimization.enabled - || !optimization.for_size - || matches!( - call_id, - "workshopSettingToggle" - | "workshopSettingCombo" - | "workshopSettingInteger" - | "workshopSettingFloat" - ) - { - return value_id; - } - let replacement = |name: &str, hir: &hir::Program| { - hir.preprocessing - .replacements - .iter() - .find(|value| value.value == name) - .is_some() - }; - match self.value(value_id).clone() { - Value::Number(0.0) => { - let name = [ - "getCapturePercentage", - "getPayloadProgressPercentage", - "isMatchComplete", - ] - .into_iter() - .find(|name| replacement(name, self.hir)); - name.map_or(value_id, |name| self.push_call(name, Vec::new())) - } - Value::Number(1.0) => { - if replacement("getMatchRound", self.hir) { - self.push_call("getMatchRound", Vec::new()) - } else { - value_id - } - } - Value::Enum { value_type, value } if value_type == "Team" && value == "TEAM_1" => { - if replacement("getControlScoringTeam", self.hir) { - self.push_call("getControlScoringTeam", Vec::new()) - } else { - value_id - } - } - Value::String(value) if value.is_empty() => { - if replacement("emptyArray", self.hir) { - self.push_call("emptyArray", Vec::new()) - } else if replacement("variable", self.hir) { - self.push_value(Value::GlobalVariable(EMPTY_STRING_NAME.to_string())) - } else { - value_id - } - } - Value::Call { name, args } - if name == "customString" - && args.len() == 1 - && self.value_is_empty_string(args[0]) => - { - if replacement("emptyArray", self.hir) { - self.push_call("emptyArray", Vec::new()) - } else if replacement("variable", self.hir) { - self.push_value(Value::GlobalVariable(EMPTY_STRING_NAME.to_string())) - } else { - value_id - } - } - _ => value_id, - } - } - - fn lower_compressed( - &mut self, - args: &[Expr], - span: Option, - ) -> Result { - self.lower_compressed_mode(args, span, true) - } - - fn lower_decompression( - &mut self, - text: &Expr, - is_vector: bool, - ) -> Result { - let text = self.lower_value(text)?; - let null = self.push_value(Value::Null); - let separator = self.push_call("firstOf", vec![null]); - let split = self.push_call("stringSplit", vec![text, separator]); - let alphabet = if has_directive(self.hir, "useVariableForCompressionAlphabet") { - let variable = *self - .globals - .get(COMPRESSION_ALPHABET_NAME) - .expect("compression alphabet variable is created"); - self.push_value(Value::GlobalVariable(self.global_names[variable].clone())) - } else { - self.lower_custom_string(compression_alphabet()) - }; - let decoded = if has_directive(self.hir, "useVariableForCompressionAlphabet") { - split - } else { - let current = self.push_call("currentArrayElement", Vec::new()); - let alphabet = self.push_call("appendToArray", vec![current, alphabet]); - self.push_call("mappedArray", vec![split, alphabet]) - }; - let width = if is_vector { 3 } else { 4 }; - let min_decimal_place = if is_vector { -2.0 } else { -3.0 }; - let offset = if is_vector { 5000.0 } else { 50000.0 }; - let component = |this: &mut Self, component_offset: usize| { - let current = this.push_call("currentArrayElement", Vec::new()); - let mut terms = Vec::with_capacity(width); - for index in 0..width { - let position = this.push_number((index + component_offset) as f64); - let character = this.push_call("charAt", vec![current, position]); - let formula_alphabet = - if has_directive(this.hir, "useVariableForCompressionAlphabet") { - alphabet - } else { - this.push_call("lastOf", vec![current]) - }; - let digit = this.push_call("strIndex", vec![formula_alphabet, character]); - let power = 100_f64.powf(index as f64 + min_decimal_place / 2.0); - let power = this.push_number(power); - terms.push(this.push_call("multiply", vec![power, digit])); - } - let mut value = terms - .first() - .copied() - .unwrap_or_else(|| this.push_number(0.0)); - for term in terms.into_iter().skip(1) { - value = this.push_call("add", vec![value, term]); - } - let offset = this.push_number(offset); - this.push_call("subtract", vec![value, offset]) - }; - if is_vector { - let x = component(self, 0); - let y = component(self, width * 2); - let z = component(self, width); - let vector = self.push_call("vector", vec![x, y, z]); - Ok(self.push_call("mappedArray", vec![decoded, vector])) - } else { - let number = component(self, 0); - Ok(self.push_call("mappedArray", vec![decoded, number])) - } - } - - fn lower_compressed_mode( - &mut self, - args: &[Expr], - span: Option, - decode: bool, - ) -> Result { - let [Expr::Array { elements, .. }] = args else { - return Err(self.unsupported( - "compressed requires one literal array of numbers or vectors", - span, - )); - }; - if elements.is_empty() { - return Err(self.unsupported("cannot compress an empty array", span)); - } - - let Some(numbers) = elements - .iter() - .map(|element| match element { - Expr::Null { .. } => Some(vec![0.0]), - Expr::Number { value, .. } => Some(vec![*value]), - Expr::Unary { op, operand, .. } if matches!(op.as_str(), "+" | "-") => { - hir::visit::literal_number(operand) - .map(|value| vec![if op == "-" { -value } else { value }]) - } - Expr::Vector { x, y, z, .. } => Some(vec![ - hir::visit::literal_number(x)?, - hir::visit::literal_number(y)?, - hir::visit::literal_number(z)?, - ]), - _ => None, - }) - .collect::>>() - else { - return Err(self.unsupported("compressed requires literal numbers or vectors", span)); - }; - let is_vector = numbers.first().is_some_and(|value| value.len() == 3); - if numbers.iter().any(|value| (value.len() == 3) != is_vector) { - return Err(self.unsupported("compressed cannot mix numbers and vectors", span)); - } - let flattened = numbers.iter().flatten().copied().collect::>(); - let limit = if is_vector { 4999.0 } else { 49999.0 }; - if flattened.iter().any(|value| value.abs() >= limit) { - return Err(self.unsupported("compressed values exceed the supported magnitude", span)); - } - - let max_decimals = if is_vector { 2 } else { 3 }; - let compression_offset = if decode { - flattened.iter().copied().fold(0.0_f64, f64::min).min(0.0) - } else if is_vector { - -5000.0 - } else { - -50000.0 - }; - let adjusted = flattened - .iter() - .map(|value| value - compression_offset) - .collect::>(); - let mut strings = adjusted - .iter() - .map(|value| { - format!("{value:.precision$}", precision = max_decimals) - .replace('.', "") - .chars() - .rev() - .collect::() - }) - .collect::>(); - let mut min_decimal_place = -(max_decimals as i32); - if decode { - while strings.iter().all(|value| value.starts_with('0')) { - for value in &mut strings { - value.remove(0); - } - min_decimal_place += 1; - } - } else { - min_decimal_place = if is_vector { -2 } else { -3 }; - } - let max_decimal_place = if decode { - min_decimal_place + strings.iter().map(String::len).max().unwrap_or_default() as i32 - } else if is_vector { - 4 - } else { - 5 - }; - for value in &mut strings { - let trimmed = value.trim_end_matches('0'); - *value = if trimmed.is_empty() { - "0".to_string() - } else { - trimmed.to_string() - }; - } - - let alphabet = compression_alphabet_chars(); - let encode = |value: &str| -> Option { - let mut encoded = String::new(); - let chars = value.as_bytes(); - for pair in chars.chunks(2) { - let number = if pair.len() == 1 { - u16::from(pair[0] - b'0') - } else { - u16::from(pair[1] - b'0') * 10 + u16::from(pair[0] - b'0') - }; - encoded.push(*alphabet.get(number as usize)?); - } - Some(encoded) - }; - let compressed = if is_vector { - let width = (((max_decimal_place - min_decimal_place + 1) / 2) * 2) as usize; - strings - .chunks(3) - .map(|values| { - let mut grouped = String::new(); - for index in [0, 2, 1] { - let mut value = values[index].clone(); - if index != 1 { - value.push_str(&"0".repeat(width.saturating_sub(value.len()))); - } else { - value = value.trim_end_matches('0').to_string(); - if value.is_empty() { - value.push('0'); - } - } - grouped.push_str(&value); - } - encode(&grouped) - }) - .collect::>>() - .ok_or_else(|| self.unsupported("compressed value cannot be encoded", span))? - .join("0") - } else { - strings - .iter() - .map(|value| encode(value)) - .collect::>>() - .ok_or_else(|| self.unsupported("compressed value cannot be encoded", span))? - .join("0") - }; - if !decode { - return Ok(self.lower_custom_string(compressed)); - } - let compressed_string = self.lower_custom_string(compressed); - let null = self.push_value(Value::Null); - let separator = self.push_call("firstOf", vec![null]); - let split = self.push_call("stringSplit", vec![compressed_string, separator]); - let alphabet_value = if has_directive(self.hir, "useVariableForCompressionAlphabet") { - let variable = *self - .globals - .get(COMPRESSION_ALPHABET_NAME) - .expect("compression alphabet variable is created"); - self.push_value(Value::GlobalVariable(self.global_names[variable].clone())) - } else { - self.lower_custom_string(compression_alphabet()) - }; - let decoded = if has_directive(self.hir, "useVariableForCompressionAlphabet") { - split - } else { - let current = self.push_call("currentArrayElement", Vec::new()); - let alphabet = self.push_call("appendToArray", vec![current, alphabet_value]); - self.push_call("mappedArray", vec![split, alphabet]) - }; - let width = ((max_decimal_place - min_decimal_place + 1) / 2) as usize; - let optimization = self.optimization_state_at(span.as_ref()); - let component = |this: &mut Self, component_offset: usize| { - let current = this.push_call("currentArrayElement", Vec::new()); - let mut terms = Vec::with_capacity(width); - for index in 0..width { - let position = this.push_number((index + component_offset) as f64); - let character = this.push_call("charAt", vec![current, position]); - let formula_alphabet = - if has_directive(this.hir, "useVariableForCompressionAlphabet") { - alphabet_value - } else { - this.push_call("lastOf", vec![current]) - }; - let digit = this.push_call("strIndex", vec![formula_alphabet, character]); - let power = 100_f64.powf(index as f64 + f64::from(min_decimal_place) / 2.0); - let power = this.push_number(power); - let weighted = this.push_call("multiply", vec![power, digit]); - if optimization.enabled { - this.optimized_nodes.insert(weighted, optimization.strict); - } - terms.push(weighted); - } - let mut value = terms - .first() - .copied() - .unwrap_or_else(|| this.push_number(0.0)); - for term in terms.into_iter().skip(1) { - value = this.push_call("add", vec![value, term]); - } - if is_vector || compression_offset == 0.0 { - value - } else { - let offset = this.push_number(compression_offset); - this.push_call("add", vec![value, offset]) - } - }; - let value = if is_vector { - let x = component(self, 0); - let y = component(self, width * 2); - let z = component(self, width); - let vector = self.push_call("vector", vec![x, y, z]); - let value = if compression_offset == 0.0 { - vector - } else { - let offset = self.push_number(-compression_offset); - let offset = self.push_call("vector", vec![offset, offset, offset]); - self.push_call("subtract", vec![vector, offset]) - }; - self.push_call("mappedArray", vec![decoded, value]) - } else { - let number = component(self, 0); - self.push_call("mappedArray", vec![decoded, number]) - }; - Ok(value) - } - - fn lower_compress( - &mut self, - args: &[Expr], - span: Option, - ) -> Result { - self.lower_compressed_mode(args, span, false) - } - - fn strict_optimization_active(&self, expr: &Expr) -> bool { - self.optimization_state_at(expr.span()).strict - } - - fn optimization_state_at(&self, span: Option<&HirSpan>) -> OptimizationState { - let Some(span) = span else { - return self.hir.preprocessing.optimization.clone(); - }; - let mut active = None; - for directive in &self.hir.preprocessing.directives { - let Some(directive_span) = directive.span else { - continue; - }; - if directive_span.file != span.file { - continue; - } - if directive_span.start.line < span.start.line - || (directive_span.start.line == span.start.line - && directive_span.start.col <= span.start.col) - { - active = Some(directive.state.optimization.clone()); - } else { - break; - } - } - active - .or_else(|| { - self.hir - .preprocessing - .source_file_initial_optimization - .get(&span.file) - .cloned() - }) - .unwrap_or_else(|| self.hir.preprocessing.optimization.clone()) + value.is_finite().then_some(value) } - fn lower_array_callback( - &mut self, - params: &[String], - body: &Expr, - span: Option, - ) -> Result { - if !(1..=2).contains(¶ms.len()) { - return Err(self.unsupported( - "array callbacks require one element parameter and at most one index parameter", - span, - )); - } - if params.windows(2).any(|pair| pair[0] == pair[1]) { - return Err( - self.unsupported("array callback parameters must have distinct names", span) - ); - } - self.array_bindings.push(ArrayBinding { - element: params[0].clone(), - index: params.get(1).cloned(), - }); - let result = self.lower_value(body); - self.array_bindings.pop(); - result + fn value_is_number(&self, id: ValueId, expected: f64) -> bool { + matches!(self.values.get(id), Some(Value::Number(value)) if *value == expected) } - fn lower_workshop_setting( - &mut self, - args: &[Expr], - span: Option, - ) -> Result { - let [ - Expr::Type { - name: setting_type, - args: type_args, - span: type_span, - }, - category, - setting_name, - default, - sort_order, - ] = args - else { - return Err(self.unsupported( - "createWorkshopSetting requires a type and four value arguments", - span, - )); - }; - - let catalog_name = match (setting_type.as_str(), type_args.as_slice()) { - ("bool", []) => "createWorkshopSettingBool", - ("int", [_, _]) => "createWorkshopSettingInt", - ("float", [_, _]) => "createWorkshopSettingFloat", - ("int", []) | ("float", []) => { - return Err(self.unsupported( - format!("createWorkshopSetting type '{setting_type}' requires a numeric range"), - type_span.or(span), - )); - } - _ => { - return Err(self.unsupported( - format!("unsupported createWorkshopSetting type '{setting_type}'"), - type_span.or(span), - )); - } - }; - - // OverPy uses an ideographic space for an empty setting category so - // the generated Workshop setting has a non-empty category value. - let category = match category { - Expr::String { value, .. } if value.is_empty() => { - self.push_value(Value::String("\u{3000}".to_string())) - } - _ => self.lower_value(category)?, - }; - let mut lowered = vec![ - category, - self.lower_value(setting_name)?, - self.lower_value(default)?, - ]; - if let [minimum, maximum] = type_args.as_slice() { - lowered.push(self.lower_value(minimum)?); - lowered.push(self.lower_value(maximum)?); - } - lowered.push(self.lower_value(sort_order)?); - Ok(self.push_call(workshop_setting_call(catalog_name).0, lowered)) + fn value_is_empty_string(&self, id: ValueId) -> bool { + matches!(self.values.get(id), Some(Value::String(value)) if value.is_empty()) } fn push_action(&mut self, action: Action) -> ActionId { diff --git a/crates/opy-rs/src/compiler/lowering/action_calls.rs b/crates/opy-rs/src/compiler/lowering/action_calls.rs new file mode 100644 index 00000000..24dc6043 --- /dev/null +++ b/crates/opy-rs/src/compiler/lowering/action_calls.rs @@ -0,0 +1,522 @@ +use super::*; + +impl<'a> Lowering<'a> { + pub(super) fn lower_cased_progress_bar( + &mut self, + args: &[Expr], + span: Option, + ) -> Result, IntegrationError> { + let [ + Expr::Number { + value: text_count, .. + }, + visible_to, + Expr::String { value: text, .. }, + position, + scale, + clipping, + text_color, + reevaluation, + spectators, + ] = args + else { + return Err(self.unsupported( + "createCasedProgressBarIwt requires a literal text count and text", + span, + )); + }; + let text_count_value = *text_count; + if !text_count_value.is_finite() + || text_count_value.fract() != 0.0 + || !(2.0..=6.0).contains(&text_count_value) + { + return Err(self.unsupported( + "createCasedProgressBarIwt text count must be between 2 and 6", + span, + )); + } + let text_count = text_count_value as usize; + if args.iter().any(hir::visit::contains_random) { + return Err(self.unsupported( + "Cannot use random functions in createCasedProgressBarIwt", + span, + )); + } + let visible_to = self.lower_value(visible_to)?; + let position = self.lower_value(position)?; + let scale = self.lower_value(scale)?; + let clipping = self.lower_value(clipping)?; + let text_color = self.lower_value(text_color)?; + let reevaluation = self.lower_value(reevaluation)?; + let spectators = self.lower_value(spectators)?; + let header_color = self.push_value(Value::Enum { + value_type: "Color".to_string(), + value: "WHITE".to_string(), + }); + let texts = text + .replace('\n', " \n ") + .split('\n') + .map(|line| { + cased_line(line, text_count) + .into_iter() + .map(|line| format!("{line}\u{ad}")) + .collect::>() + }) + .reduce(|mut all, lines| { + for (index, line) in lines.into_iter().enumerate() { + if index < all.len() { + all[index].push('\n'); + all[index].push_str(&line); + } + } + all + }) + .unwrap_or_else(|| vec![String::new(); text_count]); + let mut actions = Vec::with_capacity(text_count); + for (index, text) in texts.into_iter().enumerate() { + let value = self.push_number(index as f64); + let text = self.lower_custom_string(text); + let values = self.normalize_contextual_arguments( + "createProgressBarInWorldText", + vec![ + visible_to, + value, + text, + position, + scale, + clipping, + header_color, + text_color, + reevaluation, + spectators, + ], + ); + actions.push(self.push_call_action_with_spans( + "createProgressBarInWorldText", + &values, + [None; 10], + )); + } + Ok(actions) + } + + pub(super) fn lower_action_call( + &mut self, + name: &str, + args: &[Expr], + span: Option, + ) -> Result { + if args.is_empty() { + if let Some(&subroutine) = self.subroutines.get(name) { + return Ok(self.push_action(Action::CallSubroutine { + subroutine: self.subroutine_names[subroutine].clone(), + })); + } + } + if name == "chaseAtRate" { + let spans = args + .iter() + .map(|expr| expr.span().copied()) + .collect::>(); + let args = self.lower_values(args)?; + return Ok(self.push_call_action_with_spans(name, &args, spans)); + } + let function = self + .compiler + .manifest + .resolve_function(name) + .ok_or_else(|| self.unsupported(format!("unknown action '{name}'"), span))?; + if !matches!(function.kind, FunctionKind::Action) { + return Err(self.unsupported(format!("'{name}' is not a generic OPY action"), span)); + } + if matches!(function.id.as_str(), "async" | "startRule") { + let [subroutine, behavior] = args else { + return Err(self.unsupported( + format!( + "{} requires a subroutine and a start-rule behavior", + function.id + ), + span, + )); + }; + let subroutine_name = match subroutine { + Expr::Call { name, args, .. } if args.is_empty() => name, + _ => { + return Err(self.unsupported( + format!("{} requires a declared subroutine", function.id), + subroutine.span().copied(), + )); + } + }; + let subroutine_id = *self.subroutines.get(subroutine_name).ok_or_else(|| { + self.unsupported( + format!("unknown subroutine '{subroutine_name}'"), + subroutine.span().copied(), + ) + })?; + let subroutine_span = subroutine.span().copied(); + let behavior_span = behavior.span().copied(); + let subroutine = self.push_value(Value::Subroutine( + self.subroutine_names[subroutine_id].clone(), + )); + let behavior = self.lower_value(behavior)?; + return Ok(self.push_call_action_with_spans( + "startRule", + &[subroutine, behavior], + [subroutine_span, behavior_span], + )); + } + if matches!( + function.id.as_str(), + "hudHeader" | "hudSubheader" | "hudSubtext" + ) { + let text_slot = match function.id.as_str() { + "hudHeader" => 1, + "hudSubheader" => 2, + "hudSubtext" => 3, + _ => unreachable!(), + }; + return self.lower_hud_text(args, span, text_slot, &function.id); + } + if function.id == "createDummy" && args.len() == 4 { + let spans = args + .iter() + .map(|expr| expr.span().copied()) + .chain(std::iter::once(None)); + let mut lowered = self.lower_values(args)?; + let mut zero_vector = Vec::with_capacity(3); + for value in [0.0, 0.0, 0.0] { + zero_vector.push(self.push_number(value)); + } + lowered.push(self.push_call("vector", zero_vector)); + let args = self.normalize_contextual_arguments("createDummyBot", lowered); + return Ok(self.push_call_action_with_spans("createDummyBot", &args, spans)); + } + let spans = args + .iter() + .map(|expr| expr.span().copied()) + .collect::>(); + let args = self.lower_values(args)?; + let catalog_id = if matches!(function.id.as_str(), "stopChasingVariable" | "stopChasing") { + match args.first().map(|value| self.value(*value)) { + Some(Value::GlobalVariable(_)) => "stopChasingGlobalVariable", + Some(Value::PlayerVariable { .. }) => "stopChasingPlayerVariable", + _ => { + return Err(self.unsupported( + "stopChasingVariable requires a global or player variable", + span, + )); + } + } + } else { + function.catalog_id.as_deref().ok_or_else(|| { + self.unsupported( + format!( + "action '{}' requires a special lowering not in #46", + function.id + ), + span, + ) + })? + }; + let mut args = self.normalize_contextual_arguments(catalog_id, args); + self.apply_replacements_to_values(catalog_id, &mut args, span); + self.optimize_wait_duration(catalog_id, &mut args, span); + Ok(self.push_call_action_with_spans(catalog_id, &args, spans)) + } + + fn optimize_wait_duration( + &mut self, + catalog_id: &str, + args: &mut [ValueId], + span: Option, + ) { + const DEFAULT_WAIT_SECONDS: f64 = 0.016; + + let optimization = self.optimization_state_at(span.as_ref()); + if catalog_id != "wait" || !optimization.enabled || !optimization.for_size { + return; + } + let Some(duration) = args.first().copied() else { + return; + }; + match self.value(duration) { + Value::Number(value) if *value <= DEFAULT_WAIT_SECONDS => { + let value = self.push_value(Value::Bool(false)); + args[0] = self.normalize_contextual_argument(catalog_id, 0, value); + } + Value::Number(value) if *value == 1.0 => { + let value = self.push_value(Value::Bool(true)); + args[0] = self.normalize_contextual_argument(catalog_id, 0, value); + } + _ => {} + } + } + + fn lower_hud_text( + &mut self, + args: &[Expr], + span: Option, + text_slot: usize, + function_name: &str, + ) -> Result { + let [ + visible_to, + text, + position, + sort_order, + color, + reevaluation, + spectators, + ] = args + else { + return Err(self.unsupported( + format!("{function_name} requires exactly seven bound arguments"), + span, + )); + }; + let visible_to_span = visible_to.span().copied(); + let visible_to = self.lower_hud_visible_to(visible_to)?; + let mut text_slots = [ + self.push_value(Value::Null), + self.push_value(Value::Null), + self.push_value(Value::Null), + ]; + let text_value = self.lower_text_value(text)?; + text_slots[text_slot - 1] = if matches!(self.value(text_value), Value::String(_)) { + self.push_call("customString", vec![text_value]) + } else { + text_value + }; + let mut colors = [ + self.push_value(Value::Null), + self.push_value(Value::Null), + self.push_value(Value::Null), + ]; + colors[text_slot - 1] = self.lower_value(color)?; + let args = vec![ + visible_to, + text_slots[0], + text_slots[1], + text_slots[2], + self.lower_value(position)?, + self.lower_value(sort_order)?, + colors[0], + colors[1], + colors[2], + self.lower_value(reevaluation)?, + self.lower_value(spectators)?, + ]; + let args = self.normalize_contextual_arguments("createHudText", args); + let text_span = text.span().copied(); + let color_span = color.span().copied(); + Ok(self.push_call_action_with_spans( + "createHudText", + &args, + [ + visible_to_span, + (text_slot == 1).then_some(text_span).flatten(), + (text_slot == 2).then_some(text_span).flatten(), + (text_slot == 3).then_some(text_span).flatten(), + position.span().copied(), + sort_order.span().copied(), + (text_slot == 1).then_some(color_span).flatten(), + (text_slot == 2).then_some(color_span).flatten(), + (text_slot == 3).then_some(color_span).flatten(), + reevaluation.span().copied(), + spectators.span().copied(), + ], + )) + } + + fn lower_hud_visible_to(&mut self, expr: &Expr) -> Result { + if let Expr::Call { name, args, .. } = expr { + if name == "getAllPlayers" && args.is_empty() { + return Ok(self.lower_all_players()); + } + } + self.lower_value(expr) + } + + pub(super) fn lower_all_players(&mut self) -> ValueId { + let all_teams = self.push_value(Value::Enum { + value_type: "Team".to_string(), + value: "ALL".to_string(), + }); + self.push_call("allPlayers", vec![all_teams]) + } + + pub(super) fn lower_receiver_action_call( + &mut self, + receiver: &Expr, + name: &str, + args: &[Expr], + span: Option, + ) -> Result { + let function = self + .compiler + .manifest + .resolve_member(name) + .ok_or_else(|| self.unsupported(format!("unknown member action '{name}'"), span))?; + if !matches!(function.kind, FunctionKind::MemberAction) { + return Err(self.unsupported(format!("'{name}' is not a member action"), span)); + } + + // `append` is an OPY mutation, represented by the canonical variable + // modify actions rather than a catalog action call. + if matches!(function.id.as_str(), "append" | "remove") { + let [value] = args else { + return Err(self.unsupported( + format!("{} requires exactly one argument", function.id), + span, + )); + }; + let op = if function.id == "append" { + ModifyOp::AppendToArray + } else { + ModifyOp::RemoveFromArrayByValue + }; + let value_span = value.span().copied(); + let value = self.lower_value(value)?; + return match receiver { + Expr::GlobalVar { + name, + span: target_span, + } => { + let variable = *self.globals.get(name).ok_or_else(|| { + self.unsupported(format!("unknown global variable '{name}'"), *target_span) + })?; + let action = self.push_action(Action::ModifyGlobalVariable { + variable: self.global_names[variable].clone(), + op, + value, + }); + self.mark_action_argument_origins(action, [value_span]); + Ok(action) + } + Expr::PlayerVar { + player, + name, + span: target_span, + .. + } => { + let variable = *self.players.get(name).ok_or_else(|| { + self.unsupported(format!("unknown player variable '{name}'"), *target_span) + })?; + let player_span = player.span().copied(); + let player = self.lower_value(player)?; + let action = self.push_action(Action::ModifyPlayerVariable { + player, + variable: self.player_names[variable].clone(), + op, + value, + }); + self.mark_action_argument_origins(action, [player_span, value_span]); + Ok(action) + } + Expr::Index { array, index, .. } => { + let op_name = if function.id == "append" { + "appendToArray" + } else { + "removeFromArray" + }; + let op_node = self.push_call(op_name, Vec::new()); + let index_span = index.span().copied(); + let target_span = array.span().copied(); + let index = self.lower_value(index)?; + match array.as_ref() { + Expr::GlobalVar { + name, + span: array_span, + } => { + let variable = *self.globals.get(name).ok_or_else(|| { + self.unsupported( + format!("unknown global variable '{name}'"), + *array_span, + ) + })?; + let variable = self.push_value(Value::GlobalVariable( + self.global_names[variable].clone(), + )); + let args = self.normalize_contextual_arguments( + "modifyGlobalVariableAtIndex", + vec![variable, index, op_node, value], + ); + let action = + self.push_call_action("modifyGlobalVariableAtIndex", &args); + self.mark_action_argument_origins( + action, + [target_span, index_span, None, value_span], + ); + return Ok(action); + } + Expr::PlayerVar { + player, + name, + span: array_span, + .. + } => { + let variable = *self.players.get(name).ok_or_else(|| { + self.unsupported( + format!("unknown player variable '{name}'"), + *array_span, + ) + })?; + let player = self.lower_value(player)?; + let variable = self.push_value(Value::PlayerVariable { + player, + variable: self.player_names[variable].clone(), + }); + let args = self.normalize_contextual_arguments( + "modifyPlayerVariableAtIndex", + vec![variable, index, op_node, value], + ); + let action = + self.push_call_action("modifyPlayerVariableAtIndex", &args); + self.mark_action_argument_origins( + action, + [target_span, index_span, None, value_span], + ); + return Ok(action); + } + _ => { + return Err(self.unsupported( + format!( + "{} requires a global or player variable receiver", + function.id + ), + receiver.span().copied().or(span), + )); + } + } + } + _ => Err(self.unsupported( + format!( + "{} requires a global or player variable receiver", + function.id + ), + receiver.span().copied().or(span), + )), + }; + } + + let catalog_id = function.catalog_id.as_ref().ok_or_else(|| { + self.unsupported( + format!( + "member action '{}' has no canonical catalog identity", + function.id + ), + span, + ) + })?; + let argument_spans = std::iter::once(receiver.span().copied()) + .chain(args.iter().map(|arg| arg.span().copied())) + .collect::>(); + let mut lowered = Vec::with_capacity(args.len() + 1); + lowered.push(self.lower_value(receiver)?); + lowered.extend(self.lower_values(args.iter())?); + let mut args = self.normalize_contextual_arguments(catalog_id, lowered); + self.apply_replacements_to_values(catalog_id, &mut args, span); + Ok(self.push_call_action_with_spans(catalog_id.clone(), &args, argument_spans)) + } +} diff --git a/crates/opy-rs/src/compiler/lowering/assignments.rs b/crates/opy-rs/src/compiler/lowering/assignments.rs new file mode 100644 index 00000000..0b2f7ffb --- /dev/null +++ b/crates/opy-rs/src/compiler/lowering/assignments.rs @@ -0,0 +1,467 @@ +use super::*; + +impl<'a> Lowering<'a> { + pub(super) fn lower_delete( + &mut self, + target: &Expr, + span: Option, + ) -> Result { + let mut indices = Vec::new(); + let Some(root) = indexed_target_parts(target, &mut indices) else { + return Err(self.unsupported( + "delete statements require an indexed global or player variable", + span, + )); + }; + if indices.len() > 4 { + return Err(self.unsupported("Cannot delete index of 4d array", span)); + } + indices.reverse(); + if hir::visit::has_random_nested_delete(root, &indices) { + return Err(self.unsupported( + "Cannot delete from nested array with a random outer or middle index", + span, + )); + } + let (root_value, action_name) = match root { + Expr::GlobalVar { + name, + span: target_span, + } => { + let variable = *self.globals.get(name).ok_or_else(|| { + self.unsupported(format!("unknown global variable '{name}'"), *target_span) + })?; + let root_value = + self.push_value(Value::GlobalVariable(self.global_names[variable].clone())); + (root_value, "modifyGlobalVariableAtIndex") + } + Expr::PlayerVar { + player, + name, + span: target_span, + .. + } => { + let variable = *self.players.get(name).ok_or_else(|| { + self.unsupported(format!("unknown player variable '{name}'"), *target_span) + })?; + let player = self.lower_value(player)?; + let value = self.push_value(Value::PlayerVariable { + player, + variable: self.player_names[variable].clone(), + }); + (value, "modifyPlayerVariableAtIndex") + } + _ => { + return Err(self.unsupported( + "delete statements are only representable for global or player variables", + target.span().copied(), + )); + } + }; + let index = self.lower_value(indices[0])?; + if indices.len() == 1 { + let op = ModifyOp::RemoveFromArrayByIndex; + return Ok(if action_name == "modifyGlobalVariableAtIndex" { + let variable = match self.values.get(root_value) { + Some(Value::GlobalVariable(variable)) => variable.clone(), + _ => unreachable!("global delete root must be a global variable value"), + }; + self.push_action(Action::ModifyGlobalVariable { + variable, + op, + value: index, + }) + } else { + let (player, variable) = match self.values.get(root_value) { + Some(Value::PlayerVariable { player, variable }) => (*player, variable.clone()), + _ => unreachable!("player delete root must be a player variable value"), + }; + self.push_action(Action::ModifyPlayerVariable { + player, + variable, + op, + value: index, + }) + }); + } + + let op = self.push_call("removeFromArrayByIndex", Vec::new()); + if indices.len() == 2 { + let inner_index = self.lower_value(indices[1])?; + let args = self.normalize_contextual_arguments( + action_name, + vec![root_value, index, op, inner_index], + ); + return Ok(self.push_call_action(action_name, &args)); + } + + let outer_array = self.lower_indexed_read(root_value, indices[0], index); + if indices.len() == 4 { + let replacement = self.rebuild_deleted_array(outer_array, &indices[1..])?; + let action_name = if action_name == "modifyGlobalVariableAtIndex" { + "setGlobalVariableAtIndex" + } else { + "setPlayerVariableAtIndex" + }; + let args = self + .normalize_contextual_arguments(action_name, vec![root_value, index, replacement]); + return Ok(self.push_call_action(action_name, &args)); + } + let inner_index = self.lower_value(indices[1])?; + let row = self.lower_indexed_read(outer_array, indices[1], inner_index); + let leaf_index = self.lower_value(indices[2])?; + let current_index = self.push_call("currentArrayIndex", Vec::new()); + let condition = self.push_call("!=", vec![current_index, leaf_index]); + let filtered = self.push_call("filteredArray", vec![row, condition]); + let replacement = if let Some(number) = hir::visit::literal_number(indices[1]) { + let middle = self.lower_array(vec![filtered]); + let maximum = self.push_number(999_999_999_999.0); + let suffix_start = self.push_number(number + 1.0); + let suffix = self.push_call("slice", vec![outer_array, suffix_start, maximum]); + if number == 0.0 { + self.push_call("appendToArray", vec![middle, suffix]) + } else { + let zero = self.push_number(0.0); + let prefix = self.push_call("slice", vec![outer_array, zero, inner_index]); + let with_replacement = self.push_call("appendToArray", vec![prefix, middle]); + self.push_call("appendToArray", vec![with_replacement, suffix]) + } + } else { + self.replace_array_element(outer_array, inner_index, filtered) + }; + let action_name = if action_name == "modifyGlobalVariableAtIndex" { + "setGlobalVariableAtIndex" + } else { + "setPlayerVariableAtIndex" + }; + let args = + self.normalize_contextual_arguments(action_name, vec![root_value, index, replacement]); + Ok(self.push_call_action(action_name, &args)) + } + + pub(super) fn lower_assign( + &mut self, + target: &Expr, + value: &Expr, + span: Option, + ) -> Result { + let mut indices = Vec::new(); + if let Some(root) = indexed_target_parts(target, &mut indices) { + if indices.len() > 3 { + return Err(self.unsupported("Cannot assign to 4d array", target.span().copied())); + } + if indices.len() > 1 { + indices.reverse(); + return self.lower_nested_indexed_assign(root, &indices, target, value); + } + } + match target { + Expr::GlobalVar { + name, + span: target_span, + } => { + let variable = *self.globals.get(name).ok_or_else(|| { + self.unsupported(format!("unknown global variable '{name}'"), *target_span) + })?; + if let Expr::Binary { + op, left, right, .. + } = value + && matches!(left.as_ref(), Expr::GlobalVar { name: left_name, .. } if left_name == name) + && let Some((modify_op, _)) = modify_operator(op) + { + let value = self.lower_value(right)?; + return Ok(self.push_action(Action::ModifyGlobalVariable { + variable: self.global_names[variable].clone(), + op: modify_op, + value, + })); + } + let val = self.lower_value(value)?; + Ok(self.push_action(Action::SetGlobalVariable { + variable: self.global_names[variable].clone(), + value: val, + })) + } + Expr::PlayerVar { + player, + name, + span: target_span, + .. + } => { + let variable = *self.players.get(name).ok_or_else(|| { + self.unsupported(format!("unknown player variable '{name}'"), *target_span) + })?; + let player_val = self.lower_value(player)?; + if let Expr::Binary { + op, left, right, .. + } = value + && matches!(left.as_ref(), Expr::PlayerVar { player: left_player, name: left_name, .. } if left_name == name && left_player.as_ref() == player.as_ref()) + && let Some((modify_op, _)) = modify_operator(op) + { + let value = self.lower_value(right)?; + return Ok(self.push_action(Action::ModifyPlayerVariable { + player: player_val, + variable: self.player_names[variable].clone(), + op: modify_op, + value, + })); + } + let val = self.lower_value(value)?; + Ok(self.push_action(Action::SetPlayerVariable { + player: player_val, + variable: self.player_names[variable].clone(), + value: val, + })) + } + Expr::Index { + array, + index, + span: target_span, + } => match array.as_ref() { + Expr::GlobalVar { + name, + span: arr_span, + } => { + let variable = *self.globals.get(name).ok_or_else(|| { + self.unsupported(format!("unknown global variable '{name}'"), *arr_span) + })?; + let var_node = self.push_value(Value::GlobalVariable( + self.global_names[variable].clone(), + )); + let index_val = self.lower_value(index)?; + self.lower_indexed_assignment( + target, + var_node, + index_val, + value, + ("setGlobalVariableAtIndex", "modifyGlobalVariableAtIndex"), + ) + } + Expr::PlayerVar { + player, + name, + span: arr_span, + .. + } => { + let player_val = self.lower_value(player)?; + let variable = *self.players.get(name).ok_or_else(|| { + self.unsupported(format!("unknown player variable '{name}'"), *arr_span) + })?; + let var_node = self.push_value(Value::PlayerVariable { + player: player_val, + variable: self.player_names[variable].clone(), + }); + let index_val = self.lower_value(index)?; + self.lower_indexed_assignment( + target, + var_node, + index_val, + value, + ("setPlayerVariableAtIndex", "modifyPlayerVariableAtIndex"), + ) + } + _ => Err(self.unsupported( + "indexing assignment is only representable for global or player variables", + *target_span, + )), + }, + _ => Err(self.unsupported( + "only global-variable, player-variable, or index assignment is currently representable in canonical WIR", + span, + )), + } + } + + fn lower_indexed_assignment( + &mut self, + target: &Expr, + variable: ValueId, + index_value: ValueId, + value: &Expr, + actions: (&str, &str), + ) -> Result { + let Expr::Index { array, index, .. } = target else { + unreachable!("indexed assignment target was matched before lowering") + }; + let (set_action, modify_action) = actions; + if let Expr::Binary { + op, left, right, .. + } = value + && let Expr::Index { + array: left_array, + index: left_index, + .. + } = left.as_ref() + && left_array.as_ref() == array.as_ref() + && left_index.as_ref() == index.as_ref() + && let Some((_, call_name)) = modify_operator(op) + { + let operator = self.push_call(call_name, Vec::new()); + let value = self.lower_value(right)?; + let args = self.normalize_contextual_arguments( + modify_action, + vec![variable, index_value, operator, value], + ); + return Ok(self.push_call_action(modify_action, &args)); + } + + let value = self.lower_value(value)?; + let args = + self.normalize_contextual_arguments(set_action, vec![variable, index_value, value]); + Ok(self.push_call_action(set_action, &args)) + } + + fn lower_nested_indexed_assign( + &mut self, + root: &Expr, + indices: &[&Expr], + target: &Expr, + value: &Expr, + ) -> Result { + let (action_name, root_value) = match root { + Expr::GlobalVar { + name, + span: target_span, + } => { + let variable = *self.globals.get(name).ok_or_else(|| { + self.unsupported(format!("unknown global variable '{name}'"), *target_span) + })?; + let root_value = + self.push_value(Value::GlobalVariable(self.global_names[variable].clone())); + ("setGlobalVariableAtIndex", root_value) + } + Expr::PlayerVar { + player, + name, + span: target_span, + .. + } => { + let player_value = self.lower_value(player)?; + let variable = *self.players.get(name).ok_or_else(|| { + self.unsupported(format!("unknown player variable '{name}'"), *target_span) + })?; + let root_value = self.push_value(Value::PlayerVariable { + player: player_value, + variable: self.player_names[variable].clone(), + }); + ("setPlayerVariableAtIndex", root_value) + } + _ => { + return Err(self.unsupported( + "indexing assignment is only representable for global or player variables", + target.span().copied(), + )); + } + }; + + let outer_index = self.lower_value(indices[0])?; + let outer_array = self.lower_indexed_read(root_value, indices[0], outer_index); + let replacement = self.rebuild_indexed_value(outer_array, &indices[1..], target, value)?; + let args = self.normalize_contextual_arguments( + action_name, + vec![root_value, outer_index, replacement], + ); + Ok(self.push_call_action(action_name, &args)) + } + + fn rebuild_indexed_value( + &mut self, + array: ValueId, + indices: &[&Expr], + target: &Expr, + value: &Expr, + ) -> Result { + let index = indices + .first() + .copied() + .expect("nested indexed assignment has an inner index"); + let index_value = self.lower_value(index)?; + let replacement = if indices.len() == 1 { + if let Expr::Binary { + op, left, right, .. + } = value + && left.as_ref() == target + && let Some((_, call_name)) = modify_operator(op) + { + let current = self.lower_indexed_read(array, index, index_value); + let right = self.lower_value(right)?; + self.push_call(call_name, vec![current, right]) + } else { + self.lower_value(value)? + } + } else { + let child = self.lower_indexed_read(array, index, index_value); + self.rebuild_indexed_value(child, &indices[1..], target, value)? + }; + Ok(self.replace_array_element(array, index_value, replacement)) + } + + fn lower_indexed_read( + &mut self, + array: ValueId, + index: &Expr, + index_value: ValueId, + ) -> ValueId { + if matches!(index, Expr::Number { value, .. } if *value == 0.0) { + self.push_call("firstOf", vec![array]) + } else { + self.push_call("valueInArray", vec![array, index_value]) + } + } + + fn replace_array_element( + &mut self, + array: ValueId, + index: ValueId, + replacement: ValueId, + ) -> ValueId { + let zero = self.push_number(0.0); + let one = self.push_number(1.0); + let end = self.push_call("add", vec![index, one]); + let maximum = self.push_number(999_999_999_999.0); + let prefix = self.push_call("slice", vec![array, zero, index]); + let middle = self.lower_array(vec![replacement]); + let suffix = self.push_call("slice", vec![array, end, maximum]); + let with_replacement = self.push_call("appendToArray", vec![prefix, middle]); + self.push_call("appendToArray", vec![with_replacement, suffix]) + } + + fn rebuild_deleted_array( + &mut self, + array: ValueId, + indices: &[&Expr], + ) -> Result { + let index = self.lower_value(indices[0])?; + if indices.len() == 1 { + let current_index = self.push_call("currentArrayIndex", Vec::new()); + let condition = self.push_call("!=", vec![current_index, index]); + return Ok(self.push_call("filteredArray", vec![array, condition])); + } + let child = self.lower_indexed_read(array, indices[0], index); + let replacement = self.rebuild_deleted_array(child, &indices[1..])?; + Ok(self.replace_array_element_for_delete(array, indices[0], index, replacement)) + } + + fn replace_array_element_for_delete( + &mut self, + array: ValueId, + index_expr: &Expr, + index: ValueId, + replacement: ValueId, + ) -> ValueId { + if let Some(number) = hir::visit::literal_number(index_expr) { + let middle = self.lower_array(vec![replacement]); + let maximum = self.push_number(999_999_999_999.0); + let suffix_start = self.push_number(number + 1.0); + let suffix = self.push_call("slice", vec![array, suffix_start, maximum]); + if number == 0.0 { + return self.push_call("appendToArray", vec![middle, suffix]); + } + let zero = self.push_number(0.0); + let prefix = self.push_call("slice", vec![array, zero, index]); + let with_replacement = self.push_call("appendToArray", vec![prefix, middle]); + return self.push_call("appendToArray", vec![with_replacement, suffix]); + } + self.replace_array_element(array, index, replacement) + } +} diff --git a/crates/opy-rs/src/compiler/lowering/presentation.rs b/crates/opy-rs/src/compiler/lowering/presentation.rs new file mode 100644 index 00000000..ec8a1344 --- /dev/null +++ b/crates/opy-rs/src/compiler/lowering/presentation.rs @@ -0,0 +1,716 @@ +use super::*; + +impl<'a> Lowering<'a> { + pub(super) fn lower_translation_helper( + &mut self, + translations: &hir::TranslationState, + ) -> Result { + let translated_white = translations + .languages + .iter() + .map(|language| { + let locale = match language.as_str() { + "de" => "de-DE", + "en" => "en-US", + "es" | "es_mx" => "es-MX", + "es_es" => "es-ES", + "fr" => "fr-FR", + "it" => "it-IT", + "ja" => "ja-JP", + "ko" => "ko-KR", + "pl" => "pl-PL", + "pt" => "pt-BR", + "ru" => "ru-RU", + "th" => "th-TH", + "tr" => "tr-TR", + "zh" | "zh_cn" => "zh-CN", + "zh_tw" => "zh-TW", + _ => { + return Err(IntegrationError::new( + "translations-invalid", + format!("unsupported translation language '{language}'"), + translations.span, + )); + } + }; + self.compiler + .catalog + .localized_enum_spelling( + "Color", + &workshop_rs::catalog::Locale::new(locale), + "WHITE", + ) + .ok_or_else(|| { + IntegrationError::new( + "translations-invalid", + format!("unsupported translation locale '{locale}'"), + translations.span, + ) + }) + }) + .collect::, _>>()? + .join("0"); + let text = self.push_value(Value::String(format!("\u{ec48}0{translated_white}"))); + let custom_string = self.push_call("customString", vec![text]); + let null = self.push_value(Value::Null); + let separator = self.push_call("firstOf", vec![null]); + Ok(self.push_call("stringSplit", vec![custom_string, separator])) + } + + pub(super) fn lower_translation( + &mut self, + name: &str, + args: &[Expr], + span: Option, + ) -> Result { + let Some(translations) = self.hir.preprocessing.translations.as_ref() else { + return Err(IntegrationError::new( + "translations-invalid", + format!("translation function '{name}' requires #!translations"), + span, + )); + }; + let (context, target) = match args { + [target] => (None, target), + [Expr::String { value: context, .. }, target] => (Some(context.as_str()), target), + _ => { + return Err(IntegrationError::new( + "translations-invalid", + format!("translation function '{name}' expects one or two arguments"), + span, + )); + } + }; + let (literal, format_args) = match target { + Expr::String { value, .. } => (value.clone(), Vec::new()), + Expr::Format { text, args, .. } => { + let (text, args) = self.fold_format_constants(text, args); + (text, args) + } + _ => { + let target = self.lower_value(target)?; + if name == "___" { + return Ok(target); + } + return Ok(self.select_translation(target)); + } + }; + if format_args.len() > 16 { + return Err(IntegrationError::new( + "translations-invalid", + "translated format strings support at most sixteen dynamic arguments", + span, + )); + } + let literal = literal.as_str(); + let msgid = literal.trim(); + if literal.contains('\u{ec48}') { + return Err(IntegrationError::new( + "translations-invalid", + "translation strings must not contain the reserved translation separator", + span, + )); + } + if !self + .translation_uses + .iter() + .any(|(existing_msgid, existing)| { + existing_msgid == msgid && existing.as_deref() == context + }) + { + self.translation_uses + .push((msgid.to_string(), context.map(str::to_string))); + } + let use_tl_err = !self.translation_player_options().2; + let mut localized = translations + .languages + .iter() + .map(|language| { + translations + .entries + .iter() + .find(|entry| entry.msgid == msgid && entry.context.as_deref() == context) + .and_then(|entry| entry.translations.get(language)) + .filter(|value| !value.is_empty()) + .cloned() + .unwrap_or_else(|| literal.to_string()) + }) + .collect::>(); + let tl_err_prefix = if use_tl_err { + "\u{ff34}\u{ff2c}\u{ff25}\u{ff52}\u{ff52}\u{ec48}" + } else { + "" + }; + let raw_string = format!("{tl_err_prefix}{}", localized.join("\u{ec48}")); + let replacement_mode = raw_string.chars().count() > 128 || format_args.len() > 3; + if replacement_mode { + for index in 0..format_args.len() { + let marker = format_number_marker(index); + for value in &mut localized { + *value = value.replace(&format!("{{{index}}}"), &marker); + } + } + let encoded_segments = localized.iter().enumerate().map(|(index, value)| { + if index == 0 { + format!("{tl_err_prefix}{value}") + } else { + value.clone() + } + }); + for (index, segment) in encoded_segments.enumerate() { + if segment.len() > 511 { + return Err(IntegrationError::new( + "translations-invalid", + format!( + "translated string for language '{}' is too long, maximum length is 511 bytes", + translations.languages[index] + ), + span, + )); + } + } + } + let encoded = format!("{tl_err_prefix}{}", localized.join("\u{ec48}")); + let text = self.push_value(Value::String(encoded)); + let custom = if replacement_mode { + let mut value = self.push_call("customString", vec![text]); + for (index, arg) in format_args.iter().enumerate() { + let marker = self.push_number(format_number_marker_value(index)); + let marker = self.push_call("updateEveryFrame", vec![marker]); + let replacement = self.lower_value(arg)?; + value = self.push_call("stringReplace", vec![value, marker, replacement]); + } + value + } else { + let mut custom_args = vec![text]; + custom_args.extend(self.lower_values(format_args.iter().copied())?); + self.push_call("customString", custom_args) + }; + let helper_id = *self.globals.get(TRANSLATION_HELPER_NAME).ok_or_else(|| { + IntegrationError::new( + "translations-invalid", + "translation helper variable was not allocated", + span, + ) + })?; + let helper = self.push_value(Value::GlobalVariable(self.global_names[helper_id].clone())); + let translated = self.push_call("stringSplit", vec![custom, helper]); + if name == "___" { + return Ok(translated); + } + if name == "_" + && self + .hir + .preprocessing + .directives + .iter() + .any(|directive| directive.name == "translateWithPlayerVar") + { + let variable = *self + .players + .get("__languageIndex__") + .expect("translation player variable is allocated"); + let player = self.push_call("localPlayer", Vec::new()); + let index = self.push_value(Value::PlayerVariable { + player, + variable: self.player_names[variable].clone(), + }); + return Ok(self.push_call("valueInArray", vec![translated, index])); + } + Ok(self.select_translation(translated)) + } + + fn select_translation(&mut self, values: ValueId) -> ValueId { + let helper_id = *self + .globals + .get(TRANSLATION_HELPER_NAME) + .expect("translation helper variable is allocated"); + let helper = self.push_value(Value::GlobalVariable(self.global_names[helper_id].clone())); + let color = self.push_value(Value::Enum { + value_type: "Color".to_string(), + value: "WHITE".to_string(), + }); + let empty_array = self.push_call("emptyArray", Vec::new()); + let color = self.push_call("stringSplit", vec![color, empty_array]); + let index = self.push_call("indexOfArrayValue", vec![helper, color]); + let index = self.push_call("absoluteValue", vec![index]); + self.push_call("valueInArray", vec![values, index]) + } + + pub(super) fn translation_language_index( + &mut self, + translations: &hir::TranslationState, + ) -> Result { + let helper = self.lower_translation_helper(translations)?; + let color = self.push_value(Value::Enum { + value_type: "Color".to_string(), + value: "WHITE".to_string(), + }); + let empty_array = self.push_call("emptyArray", Vec::new()); + let color = self.push_call("stringSplit", vec![color, empty_array]); + Ok(self.push_call("indexOfArrayValue", vec![helper, color])) + } + + pub(in crate::compiler) fn translation_files(&self) -> Vec<(String, String)> { + let Some(translations) = self.hir.preprocessing.translations.as_ref() else { + return Vec::new(); + }; + let keep_unused = self + .hir + .preprocessing + .directives + .iter() + .any(|directive| directive.name == "keepUnusedTranslations"); + translations + .languages + .iter() + .skip(1) + .map(|language| { + let mut keys = self.translation_uses.clone(); + if keep_unused { + keys.extend( + translations + .entries + .iter() + .map(|entry| (entry.msgid.clone(), entry.context.clone())), + ); + } + keys.sort(); + keys.dedup(); + let mut output = String::from( + "msgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n", + ); + output.push_str(&format!("\"Language: {language}\\n\"\n\n")); + for (msgid, context) in keys { + if let Some(ref context) = context { + output.push_str(&format!( + "msgctxt {}\n", + serde_json::to_string(&context).unwrap() + )); + } + let translated = translations + .entries + .iter() + .find(|entry| { + entry.msgid == msgid && entry.context.as_deref() == context.as_deref() + }) + .and_then(|entry| entry.translations.get(language)) + .cloned() + .unwrap_or_default(); + output.push_str(&format!( + "msgid {}\n", + serde_json::to_string(&msgid).unwrap() + )); + output.push_str(&format!( + "msgstr {}\n\n", + serde_json::to_string(&translated).unwrap() + )); + } + (language.clone(), output) + }) + .collect() + } + + pub(super) fn lower_debug( + &mut self, + expr: &Expr, + debug_source: Option<&str>, + ) -> Result { + let argument_span = expr.span().copied(); + let value = self.lower_text_value(expr)?; + let array_text = if self.debug_value_is_array(value) { + self.lower_debug_array_text(value, 6) + } else { + value + }; + let debug_label_text = debug_source + .map(str::to_string) + .unwrap_or_else(|| debug_expr_text(expr)); + let debug_label = canonical_debug_text(&debug_label_text); + let debug_prefix = format!("{debug_label}\u{2028}= {{0}}"); + let inline_padding = 128 - debug_prefix.chars().count() - "{1}".chars().count(); + let padding_text = self.push_value(Value::String(" ".repeat(170 - inline_padding))); + let padding = self.push_call("customString", vec![padding_text]); + let debug_label = self.push_value(Value::String(format!( + "{debug_prefix}{}{{1}}", + " ".repeat(inline_padding) + ))); + let text = self.push_call("customString", vec![debug_label, array_text, padding]); + let all_players = self.lower_all_players(); + let null_value = self.push_value(Value::Null); + let null_value_2 = self.push_value(Value::Null); + let null_value_3 = self.push_value(Value::Null); + let null_value_4 = self.push_value(Value::Null); + let hud_position = self.push_value(Value::Enum { + value_type: "HudPosition".to_string(), + value: "LEFT".to_string(), + }); + let sort_order = self.push_number(-9999.0); + let color = self.push_value(Value::Enum { + value_type: "Color".to_string(), + value: "WHITE".to_string(), + }); + let reevaluation = self.push_value(Value::Enum { + value_type: "HudReeval".to_string(), + value: "VISIBILITY_SORT_ORDER_STRING_AND_COLOR".to_string(), + }); + let visibility = self.push_value(Value::Enum { + value_type: "SpecVisibility".to_string(), + value: "DEFAULT".to_string(), + }); + let args = self.normalize_contextual_arguments( + "createHudText", + vec![ + all_players, + null_value, + text, + null_value_2, + hud_position, + sort_order, + null_value_3, + color, + null_value_4, + reevaluation, + visibility, + ], + ); + Ok(self.push_call_action_with_spans( + "createHudText", + &args, + [ + None, + None, + argument_span, + None, + None, + None, + None, + None, + None, + None, + None, + ], + )) + } + + pub(super) fn lower_print( + &mut self, + expr: &Expr, + span: Option, + ) -> Result { + let argument_span = expr.span().copied(); + let empty_string = matches!(expr, Expr::String { value, .. } if value.is_empty()); + let value = self.lower_value(expr)?; + let value = if empty_string { + self.push_value(Value::Null) + } else { + value + }; + let padding_text = self.push_value(Value::String(" ".repeat(45))); + let padding = self.push_call("customString", vec![padding_text]); + let body_text = self.push_value(Value::String(format!("{}{{0}}", " ".repeat(125)))); + let body = self.push_call("customString", vec![body_text, padding]); + let all_players = self.lower_all_players(); + let null_value = self.push_value(Value::Null); + let null_value_2 = self.push_value(Value::Null); + let null_value_3 = self.push_value(Value::Null); + let hud_position = self.push_value(Value::Enum { + value_type: "HudPosition".to_string(), + value: "LEFT".to_string(), + }); + let sort_order = self.push_number(-9999.0); + let color = if empty_string { + self.push_value(Value::Null) + } else { + self.push_value(Value::Enum { + value_type: "Color".to_string(), + value: "ORANGE".to_string(), + }) + }; + let reevaluation = self.push_value(Value::Enum { + value_type: "HudReeval".to_string(), + value: "VISIBILITY_AND_STRING".to_string(), + }); + let visibility = self.push_value(Value::Enum { + value_type: "SpecVisibility".to_string(), + value: "DEFAULT".to_string(), + }); + let mut args = self.normalize_contextual_arguments( + "createHudText", + vec![ + all_players, + value, + body, + null_value, + hud_position, + sort_order, + color, + null_value_2, + null_value_3, + reevaluation, + visibility, + ], + ); + self.apply_replacements_to_values("createHudText", &mut args, span); + Ok(self.push_call_action_with_spans( + "createHudText", + &args, + [ + None, + argument_span, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ], + )) + } + + pub(super) fn lower_debug_array_text(&mut self, value: ValueId, max_length: usize) -> ValueId { + macro_rules! call { + ($name:literal $(, $arg:expr)* $(,)?) => {{ + let args = vec![$($arg),*]; + self.push_call($name, args) + }}; + } + + let current_count = call!("countOf", call!("currentArrayElement")); + let is_single = call!( + "==", + call!("countOf", call!("currentArrayElement")), + self.push_number(1.0) + ); + let is_empty = call!("==", call!("currentArrayElement"), call!("emptyArray")); + let not_null = call!( + "!=", + call!("currentArrayElement"), + self.push_value(Value::Null) + ); + let has_empty_array = call!("and", is_empty, not_null); + let brackets = call!("or", is_single, has_empty_array); + let first_element = call!( + "customString", + self.push_value(Value::String("[{0}]".to_string())), + call!("currentArrayElement"), + ); + let many_elements = call!( + "customString", + self.push_value(Value::String("[{0}, …+{1}]".to_string())), + call!("currentArrayElement"), + call!( + "subtract", + call!("countOf", call!("currentArrayElement")), + self.push_number(1.0), + ), + ); + let element_text = call!( + "ifThenElse", + brackets, + first_element, + call!( + "ifThenElse", + current_count, + many_elements, + call!("currentArrayElement"), + ), + ); + let mapped_elements = call!("mappedArray", value, element_text,); + let mapped_input = call!("array", mapped_elements); + let current_array = call!("currentArrayElement"); + let actual_array = call!( + "or", + call!("countOf", current_array), + call!( + "and", + call!("==", call!("currentArrayElement"), call!("emptyArray")), + call!( + "!=", + call!("currentArrayElement"), + self.push_value(Value::Null) + ), + ), + ); + let empty_length = call!( + "ifThenElse", + call!( + "and", + call!("not", call!("countOf", call!("currentArrayElement"))), + call!("!=", call!("currentArrayElement"), call!("emptyArray"),), + ), + self.push_number(3.0), + call!( + "multiply", + call!("countOf", call!("currentArrayElement")), + self.push_number(3.0), + ), + ); + let x = call!( + "appendToArray", + call!("appendToArray", actual_array, empty_length), + current_array, + ); + let x_input = call!("mappedArray", mapped_input, x); + let x_length = |this: &mut Self| { + let current = this.push_call("currentArrayElement", Vec::new()); + let index = this.push_number(1.0); + this.push_call("valueInArray", vec![current, index]) + }; + let x_value = |this: &mut Self, index: f64| { + let current = this.push_call("currentArrayElement", Vec::new()); + let index_value = this.push_number(index); + this.push_call("valueInArray", vec![current, index_value]) + }; + let first = call!("firstOf", call!("currentArrayElement")); + let array_head = if max_length == 6 { + let array_tail = call!( + "customString", + self.push_value(Value::String("{0}, {1}, {2}".to_string())), + x_value(self, 4.0), + x_value(self, 5.0), + call!( + "customString", + self.push_value(Value::String("{0}, {1}, …\u{0001}".to_string())), + x_value(self, 6.0), + x_value(self, 7.0), + ), + ); + call!( + "customString", + self.push_value(Value::String("{0}, {1}, {2}".to_string())), + x_value(self, 2.0), + x_value(self, 3.0), + array_tail, + ) + } else if max_length <= 3 { + let display = format!( + "{}…\u{0001}", + (0..max_length) + .map(|index| format!("{{{index}}}, ")) + .collect::() + ); + let mut args = vec![self.push_value(Value::String(display))]; + for index in 0..max_length { + args.push(x_value(self, (index + 2) as f64)); + } + self.push_call("customString", args) + } else { + let mut array_head = self.push_value(Value::String("…\u{0001}".to_string())); + for index in (0..max_length).rev() { + array_head = call!( + "customString", + self.push_value(Value::String("{0}, {1}".to_string())), + x_value(self, (index + 2) as f64), + array_head, + ); + } + array_head + }; + let placeholder_text = format!( + "{}\u{2026}\u{0001}", + (0..max_length).map(|_| "0, ").collect::() + ); + let placeholder = call!( + "customString", + self.push_value(Value::String(placeholder_text.clone())), + ); + let length_for_slice = x_length(self); + let end_length_for_slice = x_length(self); + let start = self.push_number( + (placeholder_text.chars().count() as isize - 4 - 3 * max_length as isize) as f64, + ); + let end = self.push_number((max_length * 3 + 4) as f64); + let slice = call!( + "stringSlice", + placeholder, + call!("add", start, length_for_slice), + call!("subtract", end, end_length_for_slice,), + ); + let replaced = call!("stringReplace", array_head, slice, call!("emptyArray"),); + let length_for_compare = x_length(self); + let length_for_divide = x_length(self); + let plus = call!( + "ifThenElse", + call!( + ">", + length_for_compare, + self.push_number((max_length * 3) as f64), + ), + call!( + "customString", + self.push_value(Value::String("+{0}".to_string())), + call!( + "subtract", + call!("divide", length_for_divide, self.push_number(3.0)), + self.push_number(max_length as f64), + ), + ), + call!("emptyArray"), + ); + let formatted_array = call!( + "customString", + self.push_value(Value::String("[{0}{1}]".to_string())), + replaced, + plus, + ); + let current_for_split = call!("currentArrayElement"); + let rendered = call!( + "ifThenElse", + first, + formatted_array, + call!( + "stringSplit", + call!("valueInArray", current_for_split, self.push_number(2.0)), + call!("emptyArray"), + ), + ); + call!("mappedArray", x_input, rendered) + } + + pub(super) fn lower_text_value(&mut self, expr: &Expr) -> Result { + let value = self.lower_value(expr)?; + let Value::Call { name, args } = self.value(value) else { + return Ok(value); + }; + if name == "customString" && args.len() == 1 { + Ok(args[0]) + } else { + Ok(value) + } + } + + fn debug_value_is_array(&self, value: ValueId) -> bool { + match self.value(value) { + Value::GlobalVariable(_) | Value::Array(_) => true, + Value::Call { name, .. } if matches!(name.as_str(), "array" | "emptyArray") => true, + Value::Call { name, .. } => self + .compiler + .catalog + .entry(Kind::Value, name) + .and_then(|entry| entry.return_type()) + .is_some_and(|return_type| { + return_type.split('|').any(|part| part.trim() == "Array") + }), + _ => false, + } + } + + pub(super) fn value_is_known_player(&self, value: ValueId) -> bool { + match self.value(value) { + Value::EventPlayer => true, + Value::Call { name, .. } => self + .compiler + .catalog + .entry(Kind::Value, name) + .and_then(|entry| entry.return_type()) + .is_some_and(|return_type| { + return_type.split('|').any(|part| part.trim() == "Player") + }), + _ => false, + } + } +} diff --git a/crates/opy-rs/src/compiler/lowering/rules.rs b/crates/opy-rs/src/compiler/lowering/rules.rs new file mode 100644 index 00000000..fe569bab --- /dev/null +++ b/crates/opy-rs/src/compiler/lowering/rules.rs @@ -0,0 +1,1821 @@ +use super::*; + +impl<'a> Lowering<'a> { + pub(in crate::compiler) fn lower_rules(&mut self) -> Result<(), IntegrationError> { + for entry in &self.hir.rules { + match entry { + RuleEntry::Rule(rule) => self.lower_rule(rule)?, + RuleEntry::SubroutineDef { + name, + source_name, + span, + name_span, + body, + annotations, + .. + } => { + self.lower_subroutine(name, source_name, *span, *name_span, body, annotations)? + } + } + } + for rule in &mut self.program.rules { + rule.name = escape_bad_words(&rule.name); + } + Ok(()) + } + + fn lower_rule(&mut self, rule: &hir::Rule) -> Result<(), IntegrationError> { + self.reject_rule_metadata(rule)?; + let event = self.lower_event(&rule.event, &rule.annotations)?; + let mut condition_exprs = Vec::new(); + for expr in &rule.conditions { + Self::split_rule_condition(expr, &mut condition_exprs); + } + let conditions = condition_exprs.iter().copied(); + let conditions = self.lower_values(conditions)?; + let previous_conditions = self.current_rule_conditions.replace(conditions.clone()); + let lowered_actions = self.lower_actions(&rule.actions, None); + self.current_rule_conditions = previous_conditions; + let mut actions = Vec::new(); + actions.extend(lowered_actions?); + let optimization = self.optimization_state_at(rule.span.as_ref()); + if optimization.enabled + && !rule.delimiter + && !self.has_meaningful_rule_action(&self.useful_actions(&actions), &event) + { + return Ok(()); + } + let elide_noop_switch = actions.is_empty() + && rule.actions.len() == 1 + && matches!(rule.actions.first(), Some(Stmt::Switch { .. })); + if elide_noop_switch && !rule.disabled { + return Ok(()); + } + let rule_index = self.program.rules.len(); + self.program.rules.push(rule_from_parts( + rule.name.clone(), + rule.disabled, + event, + conditions + .iter() + .zip(&condition_exprs) + .map(|(value, expr)| { + let mut condition = self.materialize_value(*value); + split_all(&mut condition); + let optimization = self.optimization_state_at(expr.span()); + if optimization.enabled && optimization.for_size { + SizeOptimizer::new(self.compiler).condition(&mut condition); + } + let mut condition = OperatorOptimizer::new(self.compiler, optimization.strict) + .wrap_condition(condition); + trim_numbers(&mut condition); + workshop_rs::Condition::new(condition) + }) + .collect(), + self.public_actions(&actions), + )); + let action_provenance = self.action_provenance(&actions); + self.set_rule_provenance( + rule_index, + rule.span, + condition_exprs.iter().map(|expr| expr.span().copied()), + action_provenance, + )?; + Ok(()) + } + + fn split_rule_condition<'expr>(expr: &'expr Expr, conditions: &mut Vec<&'expr Expr>) { + match expr { + Expr::Binary { + op, left, right, .. + } if op == "and" => { + Self::split_rule_condition(left, conditions); + Self::split_rule_condition(right, conditions); + } + Expr::Binary { + op, left, right, .. + } if op == "==" + && matches!(right.as_ref(), Expr::Bool { value: true, .. }) + && matches!(left.as_ref(), Expr::Binary { op, .. } if op == "and") => + { + Self::split_rule_condition(left, conditions); + } + _ => conditions.push(expr), + } + } + + fn has_meaningful_rule_action(&self, actions: &[ActionId], event: &Event) -> bool { + actions + .iter() + .any(|action| match self.actions.get(*action) { + Some( + Action::If { .. } + | Action::ElseIf { .. } + | Action::Else + | Action::While { .. } + | Action::End, + ) + | None => false, + Some(Action::Call { name, .. }) => match name.as_str() { + "abort" | "abortIf" | "break" | "continue" | "loop" | "loopIf" | "return" + | "skip" | "skipIf" => false, + "wait" => matches!(event, Event::Subroutine(_)), + _ => true, + }, + Some(_) => true, + }) + } + + fn lower_subroutine( + &mut self, + name: &str, + source_name: &str, + span: Option, + name_span: Option, + body: &[Stmt], + annotations: &[hir::Annotation], + ) -> Result<(), IntegrationError> { + self.reject_subroutine_metadata(annotations)?; + let source_name = if source_name.is_empty() { + name + } else { + source_name + }; + let subroutine = *self.subroutines.get(source_name).ok_or_else(|| { + self.unsupported( + format!("subroutine definition '{source_name}' has no declaration"), + name_span.or(span), + ) + })?; + if !self.defined_subroutines.insert(subroutine) { + return Err(self.unsupported( + format!("subroutine '{source_name}' has multiple definitions"), + name_span.or(span), + )); + } + let mut actions = Vec::new(); + actions.extend(self.lower_actions(body, None)?); + let event = Event::Subroutine(self.subroutine_names[subroutine].clone()); + if self.optimization_state_at(span.as_ref()).enabled + && !self.has_meaningful_rule_action(&actions, &event) + { + return Ok(()); + } + let rule_index = self.program.rules.len(); + self.program.rules.push(rule_from_parts( + self.subroutine_rule_name(name), + false, + event, + Vec::new(), + self.public_actions(&actions), + )); + let action_provenance = self.action_provenance(&actions); + self.set_rule_provenance(rule_index, span, std::iter::empty(), action_provenance)?; + Ok(()) + } + + fn reject_rule_metadata(&self, rule: &hir::Rule) -> Result<(), IntegrationError> { + if rule.new_page.is_some() { + let span = rule + .annotations + .iter() + .find(|annotation| annotation.name == "NewPage") + .and_then(|annotation| annotation.span) + .or(rule.span); + return Err(self.unsupported( + "rule new-page metadata is not representable in canonical WIR", + span, + )); + } + for annotation in &rule.annotations { + match annotation.name.as_str() { + "Event" | "Condition" | "Team" | "Slot" | "Hero" | "Disabled" | "Delimiter" + | "SuppressWarnings" => {} + _ => { + return Err(self.unsupported( + format!( + "rule annotation '{}' is not representable in canonical WIR", + annotation.name + ), + annotation.span.or(rule.span), + )); + } + } + } + Ok(()) + } + + fn reject_subroutine_metadata( + &self, + annotations: &[hir::Annotation], + ) -> Result<(), IntegrationError> { + for annotation in annotations { + match annotation.name.as_str() { + "Name" | "SuppressWarnings" => {} + _ => { + return Err(self.unsupported( + format!( + "subroutine annotation '{}' is not representable in canonical WIR", + annotation.name + ), + annotation.span, + )); + } + } + } + Ok(()) + } + + fn subroutine_rule_name(&self, generated_name: &str) -> String { + if self.hir.preprocessing.rule_prefix_template.is_some() { + generated_name.to_string() + } else { + format!("Subroutine {generated_name}") + } + } + + pub(super) fn global_initializer_rule_name(&self) -> String { + directive_value(self.hir, "globalvarInitRuleName") + .map(str::to_string) + .unwrap_or_else(|| { + crate::lower::render_generated_rule_name( + "Initialize global variables", + &self.hir.preprocessing, + ) + }) + } + + pub(super) fn player_initializer_rule_name(&self) -> String { + directive_value(self.hir, "playervarInitRuleName") + .map(str::to_string) + .unwrap_or_else(|| "Initialize player variables".to_string()) + } + + fn lower_event( + &self, + event: &hir::Event, + annotations: &[hir::Annotation], + ) -> Result { + if !event.args.is_empty() { + return Err(self.unsupported( + "event arguments are not representable in canonical WIR; use structural event filters", + event.span, + )); + } + let team = self.lower_event_team(annotations)?; + let target = self.lower_event_target(annotations)?; + let has_filters = !matches!(team, EventTeam::All) || !matches!(target, EventTarget::All); + match event.name.as_str() { + "global" => { + if has_filters { + return Err( + self.unsupported("global events cannot have player filters", event.span) + ); + } + Ok(Event::Global) + } + "eachPlayer" => { + if has_filters { + Ok(Event::EachPlayerWithFilters { team, target }) + } else { + Ok(Event::EachPlayer) + } + } + name => player_event_kind(name).map_or_else( + || { + Err(self.unsupported( + format!("event '{name}' is not supported by canonical WIR"), + event.span, + )) + }, + |kind| Ok(Event::Player { kind, team, target }), + ), + } + } + + fn lower_event_team( + &self, + annotations: &[hir::Annotation], + ) -> Result { + let team_annotations = annotations + .iter() + .filter(|annotation| annotation.name == "Team") + .collect::>(); + if team_annotations.len() > 1 { + return Err(self.unsupported( + "an event cannot have multiple @Team filters", + team_annotations[1].span.or(team_annotations[0].span), + )); + } + let Some(annotation) = team_annotations.first() else { + return Ok(EventTeam::All); + }; + let argument = annotation + .args + .first() + .ok_or_else(|| self.unsupported("@Team requires one filter value", annotation.span))?; + if annotation.args.len() != 1 { + return Err( + self.unsupported("@Team requires exactly one filter value", annotation.span) + ); + } + let spelling = match argument.text.as_str() { + "1" => "Team 1", + "2" => "Team 2", + value => value, + }; + let (_, member) = self + .compiler + .catalog + .resolve_enum_member("EventTeam", &Locale::new("en-US"), spelling) + .ok_or_else(|| { + self.unsupported( + format!("unknown EventTeam filter '{spelling}'"), + argument.span.or(annotation.span), + ) + })?; + match member.as_str() { + "ALL" => Ok(EventTeam::All), + "TEAM_1" => Ok(EventTeam::Team1), + "TEAM_2" => Ok(EventTeam::Team2), + _ => Err(self.unsupported( + format!("catalog EventTeam member '{member}' is not supported by canonical WIR"), + argument.span.or(annotation.span), + )), + } + } + + fn lower_event_target( + &self, + annotations: &[hir::Annotation], + ) -> Result { + let mut filters = Vec::new(); + for name in ["Slot", "Hero"] { + let matches = annotations + .iter() + .filter(|annotation| annotation.name == name) + .collect::>(); + if matches.len() > 1 { + return Err(self.unsupported( + format!("an event cannot have multiple @{name} filters"), + matches[1].span.or(matches[0].span), + )); + } + filters.extend(matches); + } + if filters.len() > 1 { + return Err(self.unsupported( + "an event cannot combine @Slot and @Hero filters", + filters[1].span.or(filters[0].span), + )); + } + let Some(annotation) = filters.first() else { + return Ok(EventTarget::All); + }; + let argument = annotation.args.first().ok_or_else(|| { + self.unsupported( + format!("@{} requires one filter value", annotation.name), + annotation.span, + ) + })?; + if annotation.args.len() != 1 { + return Err(self.unsupported( + format!("@{} requires exactly one filter value", annotation.name), + annotation.span, + )); + } + let spelling = if annotation.name == "Slot" { + match argument.text.as_str() { + value if value.parse::().is_ok() => { + format!("Slot {}", value.parse::().unwrap_or_default()) + } + value => value.to_string(), + } + } else { + argument.text.clone() + }; + let domain = if annotation.name == "Slot" { + "EventPlayer" + } else { + "Hero" + }; + let locale = Locale::new("en-US"); + let catalog_spelling = match (domain, spelling.as_str()) { + ("Hero", "mccree") => "CASSIDY", + ("Hero", "hammond") => "WRECKING_BALL", + ("Hero", "soldier") => "SOLDIER_76", + ("Hero", "domina") => "JINYU", + ("Hero", "dmon") => "D_MON", + _ => spelling.as_str(), + }; + let member = self + .compiler + .catalog + .resolve_enum_member(domain, &locale, catalog_spelling) + .map(|(_, member)| member) + .or_else(|| { + (domain == "Hero") + .then(|| { + self.compiler + .catalog + .enum_domain(domain) + .and_then(|domain| { + domain + .members + .iter() + .find(|member| { + member.member.eq_ignore_ascii_case(catalog_spelling) + || member.spellings(&locale).iter().any(|candidate| { + candidate.eq_ignore_ascii_case(catalog_spelling) + }) + || member + .member + .chars() + .filter(|c| c.is_ascii_alphanumeric()) + .collect::() + .eq_ignore_ascii_case( + &catalog_spelling + .chars() + .filter(|c| c.is_ascii_alphanumeric()) + .collect::(), + ) + }) + .map(|member| member.member.clone()) + }) + }) + .flatten() + }) + .ok_or_else(|| { + self.unsupported( + format!("unknown {domain} filter '{spelling}'"), + argument.span.or(annotation.span), + ) + })?; + if domain == "EventPlayer" { + if member == "ALL" { + Ok(EventTarget::All) + } else if let Some(slot) = member.strip_prefix("SLOT_") { + let slot = slot.parse::().map_err(|_| { + self.unsupported( + format!("catalog EventPlayer member '{member}' is not a slot"), + argument.span.or(annotation.span), + ) + })?; + Ok(EventTarget::Slot(slot)) + } else { + Err(self.unsupported( + format!( + "catalog EventPlayer member '{member}' is not supported by canonical WIR" + ), + argument.span.or(annotation.span), + )) + } + } else { + Ok(EventTarget::Hero(member)) + } + } + + fn lower_actions( + &mut self, + statements: &[Stmt], + break_target: Option, + ) -> Result, IntegrationError> { + self.visible_labels.push( + statements + .iter() + .filter_map(|statement| match statement { + Stmt::Label { name, .. } => Some(name.clone()), + _ => None, + }) + .collect(), + ); + let mut actions = Vec::new(); + let mut labels = HashMap::new(); + let mut gotos = Vec::new(); + let mut index = 0; + while index < statements.len() { + let statement = &statements[index]; + let optimization = self.optimization_state_at(statement.span()); + if optimization.enabled + && optimization.for_size + && optimization.for_size_aggressive + && index + 1 == statements.len() + && let Stmt::If { + branches, + r#else: None, + span, + } = statement + && branches.len() == 1 + { + let branch = &branches[0]; + let is_not_condition = matches!( + &*branch.condition, + Expr::Unary { op, .. } if op == "not" + ); + let is_comparison = matches!( + &*branch.condition, + Expr::Binary { op, .. } + if matches!(op.as_str(), "==" | "!=" | "<" | "<=" | ">" | ">=") + ); + if (is_not_condition || is_comparison) && !branch.body.is_empty() { + let body = self.lower_actions(&branch.body, break_target)?; + if !body.is_empty() && (is_not_condition || branch.body.len() == 1) { + let condition = if is_not_condition { + let Expr::Unary { operand, .. } = &*branch.condition else { + unreachable!() + }; + self.lower_value(operand)? + } else { + let condition = self.lower_value(&branch.condition)?; + self.push_call("not", vec![condition]) + }; + let distance = self.canonical_action_width(&body, *span)?; + let distance = self.push_number(distance as f64); + let skip = self.push_call_action("skipIf", &[condition, distance]); + self.mark_action_origins(std::slice::from_ref(&skip), *span); + actions.push(skip); + actions.extend(body); + index += 1; + continue; + } + } + } + if let Some((condition, label, span)) = direct_conditional_goto(statement) + && self + .visible_labels + .iter() + .any(|labels| labels.iter().any(|candidate| candidate == label)) + { + let condition = self.lower_value(condition)?; + let placeholder = self.push_number(0.0); + let skip = self.push_call_action("skipIf", &[condition, placeholder]); + self.mark_action_origins(std::slice::from_ref(&skip), span); + actions.push(skip); + self.deferred_gotos.push((skip, label.to_string(), span, 1)); + index += 1; + continue; + } + if let Some((condition, offset, span)) = direct_conditional_dynamic_goto(statement) { + let condition = self.lower_value(condition)?; + let offset = self.lower_value(offset)?; + let skip = self.push_call_action("skipIf", &[condition, offset]); + self.mark_action_origins(std::slice::from_ref(&skip), span); + actions.push(skip); + let goto = self.push_call_action("skip", &[offset]); + self.mark_action_origins(std::slice::from_ref(&goto), span); + actions.push(goto); + actions.push(self.push_call_action("disabledAbort", &[])); + index += 1; + continue; + } + match statement { + Stmt::Label { name, .. } => { + self.resolve_deferred_gotos(&actions, name, actions.len())?; + labels.insert(name.clone(), actions.len()); + } + Stmt::Goto { + label, + offset, + rule_start, + span, + } => { + if *rule_start { + let loop_action = self.push_call_action("loop", &[]); + self.mark_action_origins(std::slice::from_ref(&loop_action), *span); + actions.push(loop_action); + index += 1; + continue; + } + let placeholder = self.push_number(0.0); + let action = self.push_call_action("skip", &[placeholder]); + self.mark_action_origins(std::slice::from_ref(&action), *span); + let position = actions.len(); + actions.push(action); + gotos.push(( + action, + position, + label.clone(), + offset.clone(), + span.map(Into::into), + )); + } + _ => actions.extend(self.lower_action(statement, break_target)?), + } + index += 1; + } + for (action, position, label, offset, span) in gotos { + let distance = if let Some(offset) = offset { + self.lower_value(&offset)? + } else { + let Some(label) = label else { + return Err(self.unsupported("goto is missing a label or offset", span)); + }; + let Some(&target) = labels.get(&label) else { + if self + .visible_labels + .iter() + .any(|labels| labels.contains(&label)) + { + self.deferred_gotos.push((action, label, span, 0)); + continue; + } + return Err(self.unsupported(format!("unknown goto label '{label}'"), span)); + }; + if target < position { + return Err(self + .unsupported("backward goto is not representable in canonical WIR", span)); + } + let width = self.canonical_action_width(&actions[position + 1..target], span)?; + self.push_number(width as f64) + }; + let Some(Action::Call { args, .. }) = self.actions.get_mut(action) else { + unreachable!("goto placeholder must be a call action") + }; + args[0] = distance; + } + if self.visible_labels.len() == 1 && !self.deferred_gotos.is_empty() { + let (_, label, span, _) = self.deferred_gotos.remove(0); + return Err(self.unsupported(format!("unknown goto label '{label}'"), span)); + } + self.visible_labels.pop(); + Ok(actions) + } + + /// `if condition: return` and `if condition: loop()` lower to a single + /// conditional action; with optimization a constant condition removes the + /// condition entirely. + fn lower_terminal_if( + &mut self, + branch: &hir::types::IfBranch, + span: Option, + ) -> Result>, IntegrationError> { + let [child] = branch.body.as_slice() else { + return Ok(None); + }; + let is_loop_call = matches!( + child, + Stmt::Expr { expr, .. } + if matches!(expr.as_ref(), Expr::Call { name, args, .. } if name == "loop" && args.is_empty()) + ); + let (unconditional, conditional, on_true, on_false) = match child { + Stmt::Return { .. } => ( + "abort", + "abortIf", + "__abortIfConditionIsTrue__", + "__abortIfConditionIsFalse__", + ), + Stmt::Goto { + rule_start: true, .. + } => ( + "loop", + "loopIf", + "loopIfConditionIsTrue", + "__loopIfConditionIsFalse__", + ), + _ if is_loop_call => ( + "loop", + "loopIf", + "loopIfConditionIsTrue", + "__loopIfConditionIsFalse__", + ), + _ => return Ok(None), + }; + let is_rule_condition = + |expr: &Expr| matches!(expr, Expr::Call { name, .. } if name == "ruleCondition"); + let rule_condition = match branch.condition.as_ref() { + condition if is_rule_condition(condition) => Some(on_true), + Expr::Unary { op, operand, .. } + if op == "not" && is_rule_condition(operand.as_ref()) => + { + Some(on_false) + } + _ => None, + }; + if let Some(name) = rule_condition { + return Ok(Some(vec![self.push_call_action(name, &[])])); + } + let optimization = self.optimization_state_at(span.as_ref()); + if !optimization.enabled { + return Ok(None); + } + let condition = self.lower_value(&branch.condition)?; + let materialized = self.materialize_value(condition); + let operators = OperatorOptimizer::new(self.compiler, optimization.strict); + let action = match operators.constant_truth(&materialized) { + Some(false) => return Ok(Some(Vec::new())), + Some(true) => self.push_call_action(unconditional, &[]), + None => self.push_call_action(conditional, &[condition]), + }; + self.mark_action_origins(std::slice::from_ref(&action), span); + Ok(Some(vec![action])) + } + + fn resolve_deferred_gotos( + &mut self, + actions: &[ActionId], + label: &str, + target: usize, + ) -> Result<(), IntegrationError> { + let deferred = std::mem::take(&mut self.deferred_gotos); + let mut remaining = Vec::new(); + for (action, deferred_label, span, argument) in deferred { + if deferred_label != label { + remaining.push((action, deferred_label, span, argument)); + continue; + } + let Some(position) = actions.iter().position(|candidate| *candidate == action) else { + remaining.push((action, deferred_label, span, argument)); + continue; + }; + if target < position { + return Err( + self.unsupported("backward goto is not representable in canonical WIR", span) + ); + } + // A deferred goto may sit inside a structured action, so this + // slice is not necessarily a standalone valid action sequence. + // The flat lowering stream has one action id per native action, + // including the structural markers that the jump must cross. + // Instructions dropped from the output must not widen the jump. + let width = self.useful_actions(&actions[position + 1..target]).len(); + let distance = self.push_number(width as f64); + let Some(Action::Call { args, .. }) = self.actions.get_mut(action) else { + unreachable!("deferred goto placeholder must be a call action") + }; + args[argument] = distance; + } + self.deferred_gotos = remaining; + Ok(()) + } + + fn lower_action( + &mut self, + stmt: &Stmt, + break_target: Option, + ) -> Result, IntegrationError> { + let result = match stmt { + Stmt::Pass { .. } => Ok(Vec::new()), + Stmt::Assign { + target, + value, + span, + } => self.lower_assign(target, value, *span).map(|action| vec![action]), + Stmt::If { + branches, + r#else, + span, + } => { + if let ([branch], None) = (branches.as_slice(), r#else) + && let Some(actions) = self.lower_terminal_if(branch, *span)? + { + return Ok(actions); + } + let branches = branches + .iter() + .map(|branch| { + Ok(( + self.lower_value(&branch.condition)?, + self.lower_actions(&branch.body, break_target)?, + )) + }) + .collect::, IntegrationError>>()?; + let else_body = r#else + .as_ref() + .map(|body| self.lower_actions(body, break_target)) + .transpose()?; + Ok(self.push_if_actions(branches, else_body)) + } + Stmt::For { + variable, + iterable, + body, + span: _, + } => { + let (start, stop, step) = self.lower_range(iterable)?; + let body = self.lower_loop_body(body)?; + match variable.as_ref() { + Expr::GlobalVar { + name, + span: target_span, + } => { + let variable_id = *self.globals.get(name).ok_or_else(|| { + self.unsupported( + format!("unknown global variable '{name}'"), + *target_span, + ) + })?; + Ok(self.push_for_global_actions(variable_id, start, stop, step, body)) + } + Expr::PlayerVar { + player, + name, + span: target_span, + .. + } => { + let variable_id = *self.players.get(name).ok_or_else(|| { + self.unsupported( + format!("unknown player variable '{name}'"), + *target_span, + ) + })?; + let player = self.lower_value(player)?; + Ok(self.push_for_player_actions( + player, variable_id, start, stop, step, body, + )) + } + _ => Err(self.unsupported( + "range loops require a global- or player-variable binder in canonical WIR", + variable.span().copied(), + )), + } + } + Stmt::While { + condition, + body, + span: _, + } => { + let condition = self.lower_value(condition)?; + let body = self.lower_loop_body(body)?; + Ok(self.push_while_actions(condition, body)) + } + Stmt::DoWhile { + condition, + body, + span: _, + } => { + let body = self.lower_do_while_body(body)?; + let condition = self.lower_value(condition)?; + let loop_if = self.push_call_action("loopIf", &[condition]); + // OverPy's pinned lowering expands do/while into its body + // followed by the canonical Loop If action. + let mut actions = body; + actions.push(loop_if); + Ok(actions) + } + Stmt::Switch { + value, + arms, + span, + } => self.lower_switch(value, arms, *span, break_target, None, false), + Stmt::Delete { target, span } => self.lower_delete(target, *span).map(|action| vec![action]), + Stmt::Continue { span } => Err(self.unsupported( + "continue statements are only lowered while constructing a loop body", + *span, + )), + Stmt::Goto { + label, + offset, + rule_start, + span, + } => { + if *rule_start { + Ok(vec![self.push_call_action("loop", &[])]) + } else if label.is_none() { + let offset = offset.as_ref().ok_or_else(|| { + self.unsupported("goto is missing a label or offset", *span) + })?; + let offset = self.lower_value(offset)?; + Ok(vec![self.push_call_action("skip", &[offset])]) + } else { + Err(self.unsupported( + "goto statements are not representable in canonical WIR", + *span, + )) + } + } + Stmt::Label { span, .. } => Err(self.unsupported( + "labels are not representable in canonical WIR", + *span, + )), + Stmt::Break { span } => match break_target { + Some(BreakTarget::Loop) => Ok(vec![self.push_call_action("break", &[])]), + Some(BreakTarget::DoWhile) => Err(self.unsupported( + "break inside a do-while must be a direct statement or a single conditional break", + *span, + )), + Some(BreakTarget::Switch) => Ok(vec![self.push_action(Action::Else)]), + None => Err(self.unsupported( + "break has no enclosing canonical loop or switch", + *span, + )), + }, + Stmt::Return { span: _ } => Ok(vec![self.push_call_action("abort", &[])]), + Stmt::Expr { expr, span } => match expr.as_ref() { + Expr::Call { + name, + args, + debug_source, + .. + } => { + if name == "disableInspector" && args.is_empty() { + Ok(vec![self.push_call_action("disableInspector", &[])]) + } else if name == "pass" && args.is_empty() { + Ok(Vec::new()) + } else if name == "debug" && args.len() == 1 { + Ok(vec![self.lower_debug(&args[0], debug_source.as_deref())?]) + } else if name == "print" && args.len() == 1 { + Ok(vec![self.lower_print(&args[0], *span)?]) + } else if name == "createCasedProgressBarIwt" { + self.lower_cased_progress_bar(args, *span) + } else { + self.lower_action_call(name, args, *span).map(|action| vec![action]) + } + } + Expr::ReceiverCall { + receiver, + name, + args, + span: call_span, + } => self + .lower_receiver_action_call(receiver, name, args, *call_span) + .map(|action| vec![action]), + _ => Err(self.unsupported( + "only action calls are currently representable as expression statements in canonical WIR", + *span, + )), + }, + Stmt::CallSubroutine { name, span } => { + let subroutine = *self.subroutines.get(name).ok_or_else(|| { + self.unsupported(format!("unknown subroutine '{name}'"), *span) + })?; + Ok(vec![self.push_action(Action::CallSubroutine { + subroutine: self.subroutine_names[subroutine].clone(), + })]) + } + }; + if let Ok(actions) = &result { + self.mark_action_origins(actions, stmt.span().copied()); + self.mark_statement_argument_origins(stmt, actions); + } + result + } + + fn mark_statement_argument_origins(&mut self, statement: &Stmt, actions: &[ActionId]) { + match statement { + Stmt::Assign { target, value, .. } => { + let (target_span, index_span) = match &**target { + Expr::Index { array, index, .. } => { + (array.span().copied(), index.span().copied()) + } + _ => (target.span().copied(), None), + }; + let value_span = value.span().copied(); + let modified_value_span = match &**value { + Expr::Binary { right, .. } => right.span().copied(), + _ => value_span, + }; + for action in actions { + let spans = match self.actions.get(*action) { + Some(Action::SetGlobalVariable { .. }) => vec![value_span], + Some(Action::ModifyGlobalVariable { .. }) => vec![modified_value_span], + Some(Action::SetPlayerVariable { .. }) => { + let player_span = match &**target { + Expr::PlayerVar { player, .. } => player.span().copied(), + _ => None, + }; + vec![player_span, value_span] + } + Some(Action::ModifyPlayerVariable { .. }) => { + let player_span = match &**target { + Expr::PlayerVar { player, .. } => player.span().copied(), + _ => None, + }; + vec![player_span, modified_value_span] + } + Some(Action::Call { name, .. }) + if name == "setGlobalVariableAtIndex" + || name == "setPlayerVariableAtIndex" => + { + vec![target_span, index_span, value_span] + } + Some(Action::Call { name, .. }) + if name == "modifyGlobalVariableAtIndex" + || name == "modifyPlayerVariableAtIndex" => + { + vec![target_span, index_span, None, modified_value_span] + } + _ => continue, + }; + self.mark_action_argument_origins(*action, spans); + } + } + Stmt::If { branches, .. } => { + let mut depth = 0usize; + let mut branch = 0usize; + for action in actions { + match self.actions.get(*action) { + Some(Action::If { .. }) => { + if depth == 0 { + self.mark_action_argument_origins( + *action, + [branches + .first() + .and_then(|branch| branch.condition.span().copied())], + ); + } + depth += 1; + } + Some(Action::ElseIf { .. }) if depth == 1 => { + branch += 1; + self.mark_action_argument_origins( + *action, + [branches + .get(branch) + .and_then(|branch| branch.condition.span().copied())], + ); + } + Some(Action::End) => depth = depth.saturating_sub(1), + _ => {} + } + } + } + Stmt::While { condition, .. } => { + if let Some(action) = actions.first() { + if matches!(self.actions.get(*action), Some(Action::While { .. })) { + self.mark_action_argument_origins(*action, [condition.span().copied()]); + } + } + } + Stmt::For { + variable, iterable, .. + } => { + let range_spans = match &**iterable { + Expr::Call { args, .. } => match args.as_slice() { + [stop] => vec![None, stop.span().copied(), None], + [start, stop] => { + vec![start.span().copied(), stop.span().copied(), None] + } + [start, stop, step] => vec![ + start.span().copied(), + stop.span().copied(), + step.span().copied(), + ], + _ => return, + }, + _ => return, + }; + let spans = match &**variable { + Expr::PlayerVar { player, .. } => std::iter::once(player.span().copied()) + .chain(range_spans) + .collect::>(), + _ => range_spans, + }; + if let Some(action) = actions.first() { + if matches!( + self.actions.get(*action), + Some(Action::ForGlobalVariable { .. }) + | Some(Action::ForPlayerVariable { .. }) + ) { + self.mark_action_argument_origins(*action, spans); + } + } + } + Stmt::DoWhile { condition, .. } => { + if let Some(action) = actions.last() { + if matches!( + self.actions.get(*action), + Some(Action::Call { name, .. }) if name == "loopIf" + ) { + self.mark_action_argument_origins(*action, [condition.span().copied()]); + } + } + } + Stmt::Delete { target, .. } => { + let mut indices = Vec::new(); + let _ = indexed_target_parts(target, &mut indices); + indices.reverse(); + for action in actions { + let spans = match self.actions.get(*action) { + Some(Action::SetGlobalVariable { .. }) + | Some(Action::ModifyGlobalVariable { .. }) => { + vec![target.span().copied()] + } + Some(Action::SetPlayerVariable { .. }) + | Some(Action::ModifyPlayerVariable { .. }) => { + vec![None, target.span().copied()] + } + Some(Action::Call { name, .. }) + if name == "setGlobalVariableAtIndex" + || name == "setPlayerVariableAtIndex" => + { + vec![ + target.span().copied(), + indices.first().and_then(|index| index.span().copied()), + target.span().copied(), + ] + } + Some(Action::Call { name, .. }) + if name == "modifyGlobalVariableAtIndex" + || name == "modifyPlayerVariableAtIndex" => + { + vec![ + target.span().copied(), + indices.first().and_then(|index| index.span().copied()), + None, + indices.last().and_then(|index| index.span().copied()), + ] + } + _ => continue, + }; + self.mark_action_argument_origins(*action, spans); + } + } + _ => {} + } + } + + fn lower_loop_body(&mut self, statements: &[Stmt]) -> Result, IntegrationError> { + self.lower_loop_sequence_with_break_target(statements, &[], 0, BreakTarget::Loop) + } + + fn lower_loop_sequence_with_break_target( + &mut self, + statements: &[Stmt], + after: &[ActionId], + structural_after: usize, + break_target: BreakTarget, + ) -> Result, IntegrationError> { + let mut actions = Vec::new(); + let mut index = 0; + while index < statements.len() { + let statement = &statements[index]; + let tail = &statements[index + 1..]; + if let Some(conditions) = pure_continue_conditions(statement) { + let tail = self.lower_loop_sequence_with_break_target( + tail, + after, + structural_after, + break_target, + )?; + let distance = self.canonical_action_width(&tail, statement.span().copied())? + + structural_after + + self.canonical_action_width(after, statement.span().copied())?; + if distance > 0 { + let mut args = Vec::with_capacity(conditions.len() + 1); + if let Some((first, rest)) = conditions.split_first() { + let mut condition = self.lower_value(first)?; + for expression in rest { + let right = self.lower_value(expression)?; + condition = self.push_call("and", vec![condition, right]); + } + args.push(condition); + } + let distance = self.push_number(distance as f64); + args.push(distance); + let skip = self.push_call_action( + if conditions.is_empty() { + "skip" + } else { + "skipIf" + }, + &args, + ); + self.mark_action_origins( + std::slice::from_ref(&skip), + statement.span().copied(), + ); + actions.push(skip); + } + actions.extend(tail); + return Ok(actions); + } + if contains_loop_continue(statement) { + let tail = self.lower_loop_sequence_with_break_target( + tail, + after, + structural_after, + break_target, + )?; + let mut continuation_after = tail.clone(); + continuation_after.extend_from_slice(after); + let lowered = if let Stmt::Switch { value, arms, span } = statement { + self.lower_switch( + value, + arms, + *span, + Some(break_target), + Some((&continuation_after, structural_after)), + false, + )? + } else { + self.lower_if_with_loop_continue( + statement, + &continuation_after, + structural_after, + break_target, + )? + }; + self.mark_action_origins(&lowered, statement.span().copied()); + actions.extend(lowered); + actions.extend(tail); + return Ok(actions); + } + if let Some((conditions, label)) = pure_goto_conditions(statement) { + if let Some(target) = statements[index + 1..] + .iter() + .position( + |candidate| matches!(candidate, Stmt::Label { name, .. } if name == label), + ) + .map(|offset| index + 1 + offset) + { + let middle = self.lower_loop_sequence_with_break_target( + &statements[index + 1..target], + after, + structural_after, + break_target, + )?; + let suffix = self.lower_loop_sequence_with_break_target( + &statements[target + 1..], + after, + structural_after, + break_target, + )?; + let distance = + self.canonical_action_width(&middle, statement.span().copied())?; + let mut args = Vec::with_capacity(conditions.len() + 1); + if let Some((first, rest)) = conditions.split_first() { + let mut condition = self.lower_value(first)?; + for expression in rest { + let right = self.lower_value(expression)?; + condition = self.push_call("and", vec![condition, right]); + } + args.push(condition); + } + args.push(self.push_number(distance as f64)); + let skip = self.push_call_action( + if conditions.is_empty() { + "skip" + } else { + "skipIf" + }, + &args, + ); + self.mark_action_origins( + std::slice::from_ref(&skip), + statement.span().copied(), + ); + actions.push(skip); + actions.extend(middle); + actions.extend(suffix); + return Ok(actions); + } + } + if let Some((condition, label, span)) = direct_conditional_goto(statement) + && self + .visible_labels + .iter() + .any(|labels| labels.iter().any(|candidate| candidate == label)) + { + let condition = self.lower_value(condition)?; + let placeholder = self.push_number(0.0); + let skip = self.push_call_action("skipIf", &[condition, placeholder]); + self.mark_action_origins(std::slice::from_ref(&skip), span); + actions.push(skip); + self.deferred_gotos.push((skip, label.to_string(), span, 1)); + index += 1; + continue; + } + if matches!(statement, Stmt::Label { .. }) { + index += 1; + continue; + } + actions.extend(self.lower_action(statement, Some(break_target))?); + index += 1; + } + Ok(actions) + } + + fn lower_if_with_loop_continue( + &mut self, + statement: &Stmt, + after: &[ActionId], + structural_after: usize, + break_target: BreakTarget, + ) -> Result, IntegrationError> { + let Stmt::If { + branches, + r#else, + span: _, + } = statement + else { + unreachable!("continue-containing loop statement must be an if") + }; + let mut lowered_branches = Vec::with_capacity(branches.len()); + let mut suffix = after.to_vec(); + let mut suffix_structural = structural_after + 1; + let mut lowered_else = None; + if let Some(body) = r#else { + let body = self.lower_loop_sequence_with_break_target( + body, + after, + suffix_structural, + break_target, + )?; + suffix.splice(0..0, body.iter().copied()); + suffix_structural += 1; + lowered_else = Some(body); + } + for index in (0..branches.len()).rev() { + let body = self.lower_loop_sequence_with_break_target( + &branches[index].body, + &suffix, + suffix_structural, + break_target, + )?; + suffix_structural += 1; + suffix.splice(0..0, body.iter().copied()); + lowered_branches.push(body); + } + lowered_branches.reverse(); + let mut branch_actions = Vec::with_capacity(branches.len()); + for (branch, body) in branches.iter().zip(lowered_branches) { + branch_actions.push((self.lower_value(&branch.condition)?, body)); + } + Ok(self.push_if_actions(branch_actions, lowered_else)) + } + + fn lower_do_while_body( + &mut self, + statements: &[Stmt], + ) -> Result, IntegrationError> { + let mut actions = Vec::new(); + for (index, statement) in statements.iter().enumerate() { + if let Some(conditions) = pure_continue_conditions(statement) { + let tail = self.lower_do_while_body(&statements[index + 1..])?; + let mut condition = None; + for expression in conditions { + let value = self.lower_value(expression)?; + condition = Some(match condition { + Some(left) => self.push_call("and", vec![left, value]), + None => value, + }); + } + let action = if let Some(condition) = condition { + self.push_call_action("loopIf", &[condition]) + } else { + self.push_call_action("loop", &[]) + }; + self.mark_action_origins(std::slice::from_ref(&action), statement.span().copied()); + actions.push(action); + actions.extend(tail); + return Ok(actions); + } + if contains_loop_continue(statement) { + if let Stmt::Switch { value, arms, span } = statement { + let lowered = self.lower_switch( + value, + arms, + *span, + Some(BreakTarget::DoWhile), + None, + true, + )?; + self.mark_action_origins(&lowered, statement.span().copied()); + actions.extend(lowered); + continue; + } + let Stmt::If { + branches, r#else, .. + } = statement + else { + unreachable!("continue-containing do-while statement must be an if") + }; + let branches = branches + .iter() + .map(|branch| { + Ok(( + self.lower_value(&branch.condition)?, + self.lower_do_while_body(&branch.body)?, + )) + }) + .collect::, IntegrationError>>()?; + let else_body = r#else + .as_ref() + .map(|body| self.lower_do_while_body(body)) + .transpose()?; + let lowered = self.push_if_actions(branches, else_body); + self.mark_action_origins(&lowered, statement.span().copied()); + actions.extend(lowered); + continue; + } + let direct_break = matches!(statement, Stmt::Break { .. }); + let conditional_break = match statement { + Stmt::If { + branches, + r#else: None, + .. + } if branches.len() == 1 => { + matches!(branches[0].body.as_slice(), [Stmt::Break { .. }]) + } + _ => false, + }; + + if direct_break || conditional_break { + let tail = self.lower_do_while_body(&statements[index + 1..])?; + let distance = self.canonical_action_width(&tail, statement.span().copied())? + 1; + let (name, args, _span) = if let Stmt::Break { span } = statement { + ("skip", Vec::new(), *span) + } else if let Stmt::If { branches, span, .. } = statement { + ( + "skipIf", + vec![self.lower_value(&branches[0].condition)?], + *span, + ) + } else { + unreachable!("break shape was checked above") + }; + let distance = self.push_number(distance as f64); + let mut args = args; + args.push(distance); + let skip = self.push_call_action(name, &args); + self.mark_action_origins(std::slice::from_ref(&skip), statement.span().copied()); + actions.push(skip); + actions.extend(tail); + return Ok(actions); + } + + actions.extend(self.lower_action(statement, Some(BreakTarget::DoWhile))?); + } + Ok(actions) + } + + fn lower_range( + &mut self, + iterable: &Expr, + ) -> Result<(ValueId, ValueId, ValueId), IntegrationError> { + let Expr::Call { name, args, .. } = iterable else { + return Err(self.unsupported( + "range loop iterable must be a range(...) call", + iterable.span().copied(), + )); + }; + if name != "range" || !(1..=3).contains(&args.len()) { + return Err(self.unsupported( + "range loop requires one to three arguments", + iterable.span().copied(), + )); + } + let number = |this: &mut Self, value: f64| this.push_number(value); + match args.as_slice() { + [stop] => Ok(( + number(self, 0.0), + self.lower_value(stop)?, + number(self, 1.0), + )), + [start, stop] => Ok(( + self.lower_value(start)?, + self.lower_value(stop)?, + number(self, 1.0), + )), + [start, stop, step] => Ok(( + self.lower_value(start)?, + self.lower_value(stop)?, + self.lower_value(step)?, + )), + _ => unreachable!("range arity checked above"), + } + } + + fn lower_switch( + &mut self, + value: &Expr, + arms: &[SwitchArm], + span: Option, + break_target: Option, + loop_continue: Option<(&[ActionId], usize)>, + do_while_continue: bool, + ) -> Result, IntegrationError> { + if break_target.is_none() + && arms.iter().all(|arm| match arm { + SwitchArm::Case { body, .. } | SwitchArm::Default { body, .. } => { + switch_body_is_noop(body) + } + }) + { + return Ok(Vec::new()); + } + let selector = self.lower_value(value)?; + let mut case_values = Vec::new(); + let mut lowered_arms = Vec::with_capacity(arms.len()); + let mut has_default = false; + let mut legacy_case_offsets = Vec::new(); + let mut legacy_offset = 0; + let mut legacy_default_offset = None; + + let mut reverse_bodies = (loop_continue.is_some() && !do_while_continue).then(|| { + (0..arms.len()) + .map(|_| None) + .collect::>>() + }); + if let Some((outer_after, structural_after)) = loop_continue { + let mut future = Vec::new(); + for index in (0..arms.len()).rev() { + let body = match &arms[index] { + SwitchArm::Case { body, .. } | SwitchArm::Default { body, .. } => body, + }; + let mut after = future.clone(); + after.extend_from_slice(outer_after); + let lowered = self.lower_switch_body( + body, + Some((&after, structural_after)), + do_while_continue, + )?; + let mut next_future = lowered.0.clone(); + next_future.extend_from_slice(&future); + future = next_future; + reverse_bodies.as_mut().unwrap()[index] = Some(lowered); + } + } + for (index, arm) in arms.iter().enumerate() { + let (value, (body, break_at)) = match arm { + SwitchArm::Case { value, body, .. } => { + case_values.push(self.lower_value(value)?); + let lowered = if let Some(bodies) = reverse_bodies.as_mut() { + bodies[index].take().unwrap() + } else { + self.lower_switch_body(body, loop_continue, do_while_continue)? + }; + (Some(value), lowered) + } + SwitchArm::Default { body, span } => { + if has_default { + return Err( + self.unsupported("a switch may contain at most one default arm", *span) + ); + } + has_default = true; + legacy_default_offset = Some(legacy_offset); + let lowered = if let Some(bodies) = reverse_bodies.as_mut() { + bodies[index].take().unwrap() + } else { + self.lower_switch_body(body, loop_continue, do_while_continue)? + }; + (None, lowered) + } + }; + if value.is_some() { + legacy_case_offsets.push(legacy_offset); + } + legacy_offset += + self.canonical_action_width(&body, span)? + usize::from(break_at.is_some()); + lowered_arms.push((value.map(Box::as_ref), body, break_at)); + } + + let break_arms: Vec<_> = lowered_arms + .iter() + .enumerate() + .filter_map(|(index, (_, _, break_at))| break_at.map(|break_at| (index, break_at))) + .collect(); + let first_break = break_arms.first().copied(); + let has_later_reachable_actions = + first_break.is_some_and(|(break_index, (break_at, _))| { + lowered_arms[break_index].1.len() > break_at + || lowered_arms + .iter() + .skip(break_index + 1) + .any(|(_, body, _)| !body.is_empty()) + }); + let use_shared_exit = break_arms.len() > 1 && has_later_reachable_actions; + + let case_values = self.lower_array(case_values); + if !use_shared_exit { + let default_offset = legacy_default_offset.unwrap_or(legacy_offset); + let offset_values = std::iter::once(default_offset) + .chain(legacy_case_offsets) + .map(|value| self.push_number(value as f64)) + .collect(); + let offsets = self.lower_array(offset_values); + let skip = self.lower_switch_selector(selector, case_values, offsets); + let true_value = self.push_value(Value::Bool(true)); + let mut branch_body = vec![skip]; + let else_body = if let Some((break_index, (break_at, _))) = first_break { + for (index, (_, body, _)) in lowered_arms.iter().enumerate() { + if index < break_index { + branch_body.extend(body.iter().copied()); + } else if index == break_index { + branch_body.extend(body[..break_at].iter().copied()); + } + } + let mut tail = Vec::new(); + tail.extend(lowered_arms[break_index].1[break_at..].iter().copied()); + for (_, body, _) in lowered_arms.iter().skip(break_index + 1) { + tail.extend(body.iter().copied()); + } + Some(tail) + } else { + for (_, body, _) in &lowered_arms { + branch_body.extend(body.iter().copied()); + } + None + }; + let result = self.push_if_actions(vec![(true_value, branch_body)], else_body); + return Ok(result); + } + + let offsets = self.push_value(Value::Array(Vec::new())); + let skip = self.lower_switch_selector(selector, case_values, offsets); + let mut arm_offsets = vec![None; lowered_arms.len()]; + let (switch, switch_end) = + self.lower_switch_level(&lowered_arms, 0, Some(skip), 0, &mut arm_offsets, span)?; + + let default_offset = lowered_arms + .iter() + .enumerate() + .find_map(|(index, (value, _, _))| value.is_none().then(|| arm_offsets[index].unwrap())) + .unwrap_or(switch_end); + let offset_values = std::iter::once(default_offset) + .chain( + lowered_arms + .iter() + .enumerate() + .filter(|(_, (value, _, _))| value.is_some()) + .map(|(index, _)| arm_offsets[index].unwrap()), + ) + .map(|value| self.push_number(value as f64)) + .collect(); + let offset_values = self.lower_array(offset_values); + let offset_value = self.value(offset_values).clone(); + let Some(node) = self.values.get_mut(offsets) else { + unreachable!("switch offset placeholder must exist") + }; + *node = offset_value; + + let Some(Action::Call { args, .. }) = self.actions.get_mut(skip) else { + unreachable!("switch selector must be a call action") + }; + let Some(selector_id) = args.first().copied() else { + unreachable!("switch selector condition must be a value call") + }; + let Some(Value::Call { args, .. }) = self.values.get_mut(selector_id) else { + unreachable!("switch selector condition must be a value call") + }; + args[0] = offsets; + + Ok(switch) + } + + fn lower_switch_selector( + &mut self, + selector: ValueId, + case_values: ValueId, + offsets: ValueId, + ) -> ActionId { + let one = self.push_number(1.0); + let index = self.push_call("indexOfArrayValue", vec![case_values, selector]); + let case_offset = self.push_call("add", vec![one, index]); + let skip_condition = self.push_call("valueInArray", vec![offsets, case_offset]); + self.push_call_action("skip", &[skip_condition]) + } + + fn lower_switch_level( + &mut self, + arms: &[LoweredSwitchArm<'_>], + start: usize, + selector_skip: Option, + level_offset: usize, + arm_offsets: &mut [Option], + span: Option, + ) -> Result<(Vec, usize), IntegrationError> { + let break_index = (start..arms.len()) + .find(|index| arms[*index].2.is_some()) + .expect("switch level must contain a break"); + let mut branch_body = Vec::new(); + if let Some(selector_skip) = selector_skip { + branch_body.push(selector_skip); + } + let mut branch_offset = 0; + for index in start..=break_index { + arm_offsets[index] = Some(if selector_skip.is_some() { + level_offset + branch_offset + } else if index == start { + level_offset + } else { + level_offset + 1 + branch_offset + }); + let (_, body, break_at) = &arms[index]; + let body = if index == break_index { + &body[..break_at.as_ref().unwrap().0] + } else { + body.as_slice() + }; + branch_offset += self.canonical_action_width(body, span)?; + branch_body.extend(body.iter().copied()); + } + let branch_width = self.canonical_action_width(&branch_body, span)?; + let (_, break_body, Some((break_at, _))) = &arms[break_index] else { + unreachable!("break index must point to a switch break") + }; + let mut else_body = break_body[*break_at..].to_vec(); + let tail_width = self.canonical_action_width(&else_body, span)?; + let else_content_start = if selector_skip.is_some() { + level_offset + branch_width + tail_width + } else { + level_offset + branch_width + tail_width + 2 + }; + let has_next_break = (break_index + 1..arms.len()).any(|index| arms[index].2.is_some()); + let end_offset = if has_next_break { + let (child, child_end) = self.lower_switch_level( + arms, + break_index + 1, + None, + else_content_start, + arm_offsets, + span, + )?; + else_body.extend(child); + child_end + } else { + let mut offset = else_content_start; + for index in break_index + 1..arms.len() { + arm_offsets[index] = Some(offset); + let (_, body, _) = &arms[index]; + offset += self.canonical_action_width(body, span)?; + else_body.extend(body.iter().copied()); + } + offset + }; + let true_value = self.push_value(Value::Bool(true)); + let switch = self.push_if_actions(vec![(true_value, branch_body)], Some(else_body)); + Ok((switch, end_offset)) + } + + fn lower_switch_body( + &mut self, + statements: &[Stmt], + loop_continue: Option<(&[ActionId], usize)>, + do_while_continue: bool, + ) -> Result { + let mut actions = Vec::new(); + let break_index = statements + .iter() + .position(|statement| matches!(statement, Stmt::Break { .. })); + let body_end = break_index.unwrap_or(statements.len()); + if do_while_continue { + actions.extend(self.lower_do_while_body(&statements[..body_end])?); + } else if let Some((after, structural_after)) = loop_continue { + actions.extend(self.lower_loop_sequence_with_break_target( + &statements[..body_end], + after, + structural_after + 1, + BreakTarget::Switch, + )?); + } else { + for statement in &statements[..body_end] { + actions.extend(self.lower_action(statement, Some(BreakTarget::Switch))?); + } + } + let mut break_at = None; + if let Some(index) = break_index { + let Stmt::Break { span } = &statements[index] else { + unreachable!("switch break index must point to a break") + }; + if statements[index + 1..] + .iter() + .any(|statement| matches!(statement, Stmt::Break { .. })) + { + return Err(self.unsupported( + "multiple switch breaks in one arm require canonical switch targets", + *span, + )); + } + break_at = Some(( + actions.len(), + span.ok_or_else(|| { + self.unsupported("switch break is missing source provenance", None) + })?, + )); + for statement in statements[index + 1..].iter() { + actions.extend(self.lower_action(statement, Some(BreakTarget::Switch))?); + } + } + Ok((actions, break_at)) + } + + fn canonical_action_width( + &self, + actions: &[ActionId], + fallback_span: Option, + ) -> Result { + let public_actions = self.public_actions(actions); + let mut program = self.program.clone(); + program.settings = None; + program.rules.push(rule_from_parts( + "action layout".to_string(), + false, + workshop_rs::Event::Global, + Vec::new(), + public_actions.clone(), + )); + workshop_rs::emitter::action_width( + &program, + self.compiler.catalog, + &Locale::new("en-US"), + &public_actions, + ) + .map(|layout| layout.width) + .map_err(|error| { + let span = fallback_span; + IntegrationError::new("workshop-action-layout", error.to_string(), span) + }) + } +} diff --git a/crates/opy-rs/src/compiler/lowering/values.rs b/crates/opy-rs/src/compiler/lowering/values.rs new file mode 100644 index 00000000..344d145b --- /dev/null +++ b/crates/opy-rs/src/compiler/lowering/values.rs @@ -0,0 +1,1836 @@ +use super::*; + +impl<'a> Lowering<'a> { + pub(super) fn lower_values<'expr>( + &mut self, + expressions: impl IntoIterator, + ) -> Result, IntegrationError> { + expressions + .into_iter() + .map(|expr| self.lower_value(expr)) + .collect() + } + + pub(super) fn lower_value(&mut self, expr: &Expr) -> Result { + let value_id = self.lower_value_unoptimized(expr)?; + let optimization = self.optimization_state_at(expr.span()); + if optimization.enabled { + self.optimized_nodes + .entry(value_id) + .or_insert(optimization.strict); + } + Ok(value_id) + } + + fn lower_value_unoptimized(&mut self, expr: &Expr) -> Result { + let span = expr.span().copied(); + let optimization = self.optimization_state_at(span.as_ref()); + if optimization.enabled + && !optimization.strict + && (matches!(expr, Expr::Binary { .. } | Expr::Unary { .. }) + || matches!(expr, Expr::Call { name, .. } if matches!(name.as_str(), "len" | "countOf"))) + { + let bindings = HashMap::new(); + let mut stack = Vec::new(); + if let Some(value) = + crate::compile_time::evaluate(expr, &self.constants, &bindings, &mut stack) + { + match value { + crate::compile_time::Value::Number(value) if value.is_finite() => { + return Ok(self.push_number(value)); + } + crate::compile_time::Value::String(value) => { + return Ok(self.lower_custom_string(value)); + } + crate::compile_time::Value::Bool(value) => { + return Ok(self.push_value(Value::Bool(value))); + } + crate::compile_time::Value::Array(_) + | crate::compile_time::Value::Object(_) + | crate::compile_time::Value::Number(_) => {} + } + } + } + let value = match expr { + Expr::Number { value, .. } => Value::Number(*value), + Expr::String { value, .. } => { + return Ok(self.lower_custom_string(value.clone())); + } + Expr::Bool { value, .. } => Value::Bool(*value), + Expr::Null { .. } => Value::Null, + Expr::Local { name, .. } => { + let binding = self.array_bindings.iter().rev().find(|binding| { + binding.element == *name || binding.index.as_deref() == Some(name) + }); + match binding { + Some(binding) if binding.element == *name => { + return Ok(self.push_call("currentArrayElement", Vec::new())); + } + Some(_) => return Ok(self.push_call("currentArrayIndex", Vec::new())), + None => { + return Err(self.unsupported( + format!("local '{name}' is not inside a supported array callback"), + span, + )); + } + } + } + Expr::Type { .. } => { + return Err(self.unsupported( + "type expressions are only valid as createWorkshopSetting type arguments", + span, + )); + } + Expr::GlobalVar { name, .. } => { + let id = *self.globals.get(name).ok_or_else(|| { + self.unsupported(format!("unknown global variable '{name}'"), span) + })?; + Value::GlobalVariable(self.global_names[id].clone()) + } + Expr::PlayerVar { player, name, .. } => { + let player = self.lower_value(player)?; + let id = *self.players.get(name).ok_or_else(|| { + self.unsupported(format!("unknown player variable '{name}'"), span) + })?; + Value::PlayerVariable { + player, + variable: self.player_names[id].clone(), + } + } + Expr::EventPlayer { .. } => Value::EventPlayer, + Expr::HostPlayer { .. } => Value::Call { + name: "hostPlayer".to_string(), + args: Vec::new(), + }, + Expr::Enum { + value_type, value, .. + } => { + let value = match (value_type.as_str(), value.as_str()) { + ("Clipping", "NONE") => "DO_NOT_CLIP", + ("Clipping", "SURFACES") => "CLIP_AGAINST_SURFACES", + _ => value, + }; + if self + .compiler + .catalog + .enum_spelling(value_type, &Locale::new("en-US"), value) + .is_none() + { + return Err(self.unsupported( + format!("unknown catalog enum member '{value_type}.{value}'"), + span, + )); + } + let member = Value::Enum { + value_type: value_type.clone(), + value: value.to_string(), + }; + if value_type == "Gamemode" { + let member = self.push_value(member); + Value::Call { + name: "gameMode".to_string(), + args: vec![member], + } + } else { + member + } + } + Expr::Array { elements, .. } => { + let elements = self.lower_values(elements)?; + return Ok(self.lower_array(elements)); + } + Expr::Vector { x, y, z, .. } => { + let x = self.lower_value(x)?; + let y = self.lower_value(y)?; + let z = self.lower_value(z)?; + if let Some(member) = self.canonical_vector_member(x, y, z) { + Value::Enum { + value_type: "Vector".to_string(), + value: member.to_string(), + } + } else { + Value::Call { + name: "vector".to_string(), + args: self.value_args(&[x, y, z]), + } + } + } + Expr::Constant { name, .. } => { + let const_expr = *self + .constants + .get(name) + .ok_or_else(|| self.unsupported(format!("unknown constant '{name}'"), span))?; + return self.lower_value(const_expr); + } + Expr::Index { array, index, .. } => { + if let Expr::Dict { entries, .. } = array.as_ref() + && is_literal_key(index) + && entries.iter().all(|entry| is_literal_key(&entry.key)) + { + if let Some(value) = entries + .iter() + .find(|entry| literal_key_matches(&entry.key, index)) + .map(|entry| &entry.value) + { + return self.lower_value(value); + } + return Ok(self.push_value(Value::Null)); + } + // The pinned OverPy oracle lowers a literal zero-index read + // (`arr[0]`, `arr[0.0]`) to `firstOf(arr)`; non-zero indexes + // and indexed writes keep the indexed forms. + if matches!(index.as_ref(), Expr::Number { value, .. } if *value == 0.0) { + let array = self.lower_value(array)?; + Value::Call { + name: "firstOf".to_string(), + args: self.value_args(&[array]), + } + } else { + let array = self.lower_value(array)?; + let index = self.lower_value(index)?; + Value::Call { + name: "valueInArray".to_string(), + args: self.value_args(&[array, index]), + } + } + } + Expr::Format { text, args, .. } => { + let (format_text, dynamic_args) = self.fold_format_constants(text, args); + if dynamic_args.is_empty() { + let value = format_text; + return Ok(self.lower_custom_string(value)); + } + if dynamic_args.len() <= 3 { + let text_node = self.push_value(Value::String(format_text)); + let mut call_args = vec![text_node]; + for arg in dynamic_args { + let arg = self.lower_value(arg)?; + call_args.push(arg); + } + Value::Call { + name: "customString".to_string(), + args: call_args, + } + } else { + let chunks = split_format_chunks(&format_text, dynamic_args.len()).ok_or_else(|| { + self.unsupported( + "format strings with more than three replacements require sequential placeholders", + span, + ) + })?; + let lowered_args = dynamic_args.iter().copied(); + let lowered_args = self.lower_values(lowered_args)?; + let mut parts = Vec::with_capacity(chunks.len()); + for (chunk, indices) in chunks { + let text = self.push_value(Value::String(chunk)); + let mut call_args = vec![text]; + call_args.extend(indices.into_iter().map(|index| lowered_args[index])); + let call_args = + self.normalize_contextual_arguments("customString", call_args); + parts.push(self.push_value(Value::Call { + name: "customString".to_string(), + args: call_args, + })); + } + let separator = self.push_value(Value::String("{0}{1}".to_string())); + let mut value = parts[0]; + for part in parts.into_iter().skip(1) { + value = self.push_call("customString", vec![separator, value, part]); + } + return Ok(value); + } + } + Expr::Conditional { + then_value, + condition, + else_value, + .. + } => Value::Call { + name: "ifThenElse".to_string(), + args: { + let condition = self.lower_value(condition)?; + let then_value = self.lower_value(then_value)?; + let else_value = self.lower_value(else_value)?; + self.value_args(&[condition, then_value, else_value]) + }, + }, + Expr::Binary { + op, left, right, .. + } => { + if self.optimization_state_at(span.as_ref()).enabled + && matches!(op.as_str(), "in" | "not in") + { + if let Expr::Array { elements, .. } = right.as_ref() { + if elements + .iter() + .any(|elem| literal_key_matches(elem, left.as_ref())) + { + return Ok(self.push_value(Value::Bool(op == "in"))); + } + let strict = self.strict_optimization_active(expr); + if is_membership_literal(left.as_ref(), strict) + && elements + .iter() + .all(|elem| is_membership_literal(elem, strict)) + { + return Ok(self.push_value(Value::Bool(op == "not in"))); + } + } + } + if op == "==" && self.optimization_state_at(span.as_ref()).enabled { + if let Some(value) = self.lower_current_map_equality(left, right) { + return Ok(value); + } + } + let left = self.lower_value(left)?; + let right = self.lower_value(right)?; + if self.optimization_state_at(span.as_ref()).enabled + && let Some(value) = self.fold_numeric_binary(op, left, right) + { + Value::Number(value) + } else { + if op == "in" { + Value::Call { + name: "arrayContains".to_string(), + args: self.value_args(&[right, left]), + } + } else if op == "not in" { + let contains = self.push_call("arrayContains", vec![right, left]); + Value::Call { + name: "not".to_string(), + args: self.value_args(&[contains]), + } + } else { + let name = match op.as_str() { + "==" | "!=" | "<" | "<=" | ">" | ">=" | "and" | "or" => op, + "+" => "add", + "-" => "subtract", + "*" => "multiply", + "/" => "divide", + "%" => "modulo", + "**" => "raiseToPower", + _ => { + return Err(self.unsupported( + format!( + "binary operator '{op}' is not currently representable in canonical WIR" + ), + span, + )); + } + }; + Value::Call { + name: name.to_string(), + args: self.value_args(&[left, right]), + } + } + } + } + Expr::Unary { op, operand, .. } => match op.as_str() { + "not" => { + // The pinned OverPy 9.7.10 oracle lowers `not (a == b)` + // to the negated comparison (`a != b`), flipping every + // ordering comparison; `in` membership stays wrapped in + // `not`. Mirror that observable lowering. + if let Expr::Binary { + op: comparison, + left, + right, + .. + } = operand.as_ref() + { + if let Some(negated) = negated_comparison(comparison) { + let left = self.lower_value(left)?; + let right = self.lower_value(right)?; + Value::Call { + name: negated.to_string(), + args: self.value_args(&[left, right]), + } + } else { + let operand = self.lower_value(operand)?; + Value::Call { + name: "not".to_string(), + args: self.value_args(&[operand]), + } + } + } else { + let operand = self.lower_value(operand)?; + Value::Call { + name: "not".to_string(), + args: self.value_args(&[operand]), + } + } + } + "-" => { + let operand = self.lower_value(operand)?; + if let Value::Number(number) = self.value(operand) { + Value::Number(-number) + } else { + Value::Call { + name: "-".to_string(), + args: self.value_args(&[operand]), + } + } + } + "+" => return self.lower_value(operand), + _ => { + return Err(self.unsupported( + format!( + "unary operator '{op}' is not currently representable in canonical WIR" + ), + span, + )); + } + }, + Expr::Call { name, args, .. } => { + if matches!(name.as_str(), "_" | "__" | "___") { + return self.lower_translation(name, args, span); + } + if name == "createWorkshopSetting" { + return self.lower_workshop_setting(args, span); + } + if name == "buttonToString" { + let [button] = args.as_slice() else { + return Err(self.unsupported("buttonToString requires one button", span)); + }; + let button = self.lower_value(button)?; + return Ok(self.push_call("inputBindingString", vec![button])); + } + if matches!( + name.as_str(), + "getRealClosestPlayer" + | "getRealClosestPlayers" + | "getRealFarthestPlayer" + | "getRealFarthestPlayers" + ) { + let [center, team] = args.as_slice() else { + return Err( + self.unsupported(format!("{name} requires center and team"), span) + ); + }; + let center = self.lower_value(center)?; + let team = self.lower_value(team)?; + let players = self.push_call("getLivingPlayers", vec![team]); + let current = self.push_call("currentArrayElement", Vec::new()); + let spawned = self.push_call("hasSpawned", vec![current]); + let players = self.push_call("filteredArray", vec![players, spawned]); + let distance = self.push_call("distance", vec![current, center]); + let key = if matches!( + name.as_str(), + "getRealFarthestPlayer" | "getRealFarthestPlayers" + ) { + let negative_one = self.push_number(-1.0); + self.push_call("multiply", vec![negative_one, distance]) + } else { + distance + }; + let sorted = self.push_call("sortedArray", vec![players, key]); + return if matches!( + name.as_str(), + "getRealClosestPlayer" | "getRealFarthestPlayer" + ) { + Ok(self.push_call("firstOf", vec![sorted])) + } else { + Ok(sorted) + }; + } + if name == "getRealPlayersInRadius" { + let lowered = self.lower_values(args)?; + let players = self.push_call("getPlayersInRadius", lowered); + let current = self.push_call("currentArrayElement", Vec::new()); + let alive = self.push_call("isAlive", vec![current]); + let spawned = self.push_call("hasSpawned", vec![current]); + let condition = self.push_call("and", vec![alive, spawned]); + return Ok(self.push_call("filteredArray", vec![players, condition])); + } + if name == "lineIntersectsSphere" { + let [line_start, line_direction, sphere_center, sphere_radius] = + args.as_slice() + else { + return Err( + self.unsupported("lineIntersectsSphere requires four arguments", span) + ); + }; + let line_start = self.lower_value(line_start)?; + let line_direction = self.lower_value(line_direction)?; + let sphere_center = self.lower_value(sphere_center)?; + let sphere_radius = self.lower_value(sphere_radius)?; + let center_direction = + self.push_call("subtract", vec![sphere_center, line_start]); + let angle = self.push_call( + "angleBetweenVectors", + vec![line_direction, center_direction], + ); + let distance = self.push_call("distance", vec![line_start, sphere_center]); + let ratio = self.push_call("divide", vec![sphere_radius, distance]); + let limit = self.push_call("asinDeg", vec![ratio]); + return Ok(self.push_call("<=", vec![angle, limit])); + } + if name == "arrayToString" { + let (array, max_length) = match args.as_slice() { + [array] => (array, 12), + [array, Expr::Number { value, .. }] => { + if !value.is_finite() || *value < 0.0 || value.fract() != 0.0 { + return Err(self.unsupported( + "arrayToString maxLength must be a non-negative integer literal", + span, + )); + } + (array, (*value).min(1000.0) as usize) + } + _ => return Err(self.unsupported("arrayToString requires an array", span)), + }; + let array = self.lower_value(array)?; + return Ok(self.lower_debug_array_text(array, max_length)); + } + if matches!(name.as_str(), "decompressNumbers" | "decompressVectors") { + let [text] = args.as_slice() else { + return Err(self.unsupported(format!("{name} requires one string"), span)); + }; + return self.lower_decompression(text, name == "decompressVectors"); + } + if name == "strVisualLength" { + let [Expr::String { value, .. }] = args.as_slice() else { + return Err( + self.unsupported("strVisualLength requires one literal string", span) + ); + }; + let width = value.chars().map(blizzard_global::width).sum::(); + return Ok(self.push_number(width as f64)); + } + if name == "spacesForLength" { + let [Expr::Number { value, .. }] = args.as_slice() else { + return Err( + self.unsupported("spacesForLength requires one literal number", span) + ); + }; + if !value.is_finite() || *value < 0.0 || value.fract() != 0.0 { + return Err(self.unsupported( + "spacesForLength requires a non-negative integer literal", + span, + )); + } + return Ok(self.lower_custom_string(blizzard_global::spaces(*value as i32))); + } + if name == "spacesForString" { + let [Expr::String { value, .. }] = args.as_slice() else { + if let [ + Expr::Call { + name: translation, + args: translation_args, + .. + }, + ] = args.as_slice() + && matches!(translation.as_str(), "_" | "__" | "___") + && let Some(text) = translation_args.last() + && let Expr::String { + value, + span: text_span, + } = text + { + let replacement = Expr::String { + value: blizzard_global::spaces( + value.chars().map(blizzard_global::width).sum(), + ), + span: *text_span, + }; + let mut translated_args = translation_args.clone(); + *translated_args.last_mut().expect("translation text exists") = + replacement; + return self.lower_value(&Expr::Call { + name: translation.clone(), + args: translated_args, + debug_source: None, + span, + }); + } + return Err( + self.unsupported("spacesForString requires one literal string", span) + ); + }; + return Ok(self.lower_custom_string(blizzard_global::spaces( + value.chars().map(blizzard_global::width).sum(), + ))); + } + if name == "hsl" { + let (hue, saturation, lightness, alpha) = match args.as_slice() { + [hue, saturation, lightness] => (hue, saturation, lightness, None), + [hue, saturation, lightness, alpha] => { + (hue, saturation, lightness, Some(alpha)) + } + _ => { + return Err( + self.unsupported("hsl requires three or four arguments", span) + ); + } + }; + let hue = self.lower_value(hue)?; + let saturation = self.lower_value(saturation)?; + let lightness = self.lower_value(lightness)?; + let alpha = match alpha { + Some(alpha) => self.lower_value(alpha)?, + None => self.push_number(255.0), + }; + let one = self.push_number(1.0); + let thirty = self.push_number(30.0); + let hue_thirtieths = self.push_call("divide", vec![hue, thirty]); + let lightness_complement = self.push_call("subtract", vec![one, lightness]); + let lightness_limit = + self.push_call("min", vec![lightness, lightness_complement]); + let channel = |this: &mut Self, offset: f64| { + let offset = this.push_number(offset); + let phase = this.push_call("add", vec![offset, hue_thirtieths]); + let twelve = this.push_number(12.0); + let phase = this.push_call("modulo", vec![phase, twelve]); + let three = this.push_number(3.0); + let lower = this.push_call("subtract", vec![phase, three]); + let nine = this.push_number(9.0); + let upper = this.push_call("subtract", vec![nine, phase]); + let clamped = this.push_call("min", vec![lower, upper]); + let negative_one = this.push_number(-1.0); + let clamped = this.push_call("max", vec![clamped, negative_one]); + let saturation_limit = + this.push_call("multiply", vec![saturation, lightness_limit]); + let adjustment = + this.push_call("multiply", vec![saturation_limit, clamped]); + let value = this.push_call("subtract", vec![lightness, adjustment]); + let scale = this.push_number(255.0); + this.push_call("multiply", vec![scale, value]) + }; + let red = channel(self, 0.0); + let green = channel(self, 8.0); + let blue = channel(self, 4.0); + return Ok(self.push_call("customColor", vec![red, green, blue, alpha])); + } + if name == "timeToString" { + let [time] = args.as_slice() else { + return Err(self.unsupported("timeToString requires one argument", span)); + }; + let time = self.lower_value(time)?; + let three_thousand_six_hundred = self.push_number(3600.0); + let sixty = self.push_number(60.0); + let hour_value = + self.push_call("divide", vec![time, three_thousand_six_hundred]); + let down = self.push_value(Value::Enum { + value_type: "Rounding".to_string(), + value: "DOWN".to_string(), + }); + let hour = self.push_call("roundToInteger", vec![hour_value, down]); + let minute_remainder = + self.push_call("modulo", vec![time, three_thousand_six_hundred]); + let minute_value = self.push_call("divide", vec![minute_remainder, sixty]); + let minute = self.push_call("roundToInteger", vec![minute_value, down]); + let second = self.push_call("modulo", vec![time, sixty]); + let hundred = self.push_number(100.0); + let first_digit = self.push_number(1.0); + let two = self.push_number(2.0); + let minute_with_padding = self.push_call("add", vec![minute, hundred]); + let padding_template = self.push_value(Value::String("{0}".to_string())); + let minute_with_padding = + self.push_call("customString", vec![padding_template, minute_with_padding]); + let minute_text = + self.push_call("stringSlice", vec![minute_with_padding, first_digit, two]); + let second_with_padding = self.push_call("add", vec![second, hundred]); + let second_with_padding = + self.push_call("customString", vec![padding_template, second_with_padding]); + let all_digits = self.push_number(9999.0); + let second_text = self.push_call( + "stringSlice", + vec![second_with_padding, first_digit, all_digits], + ); + let template = self.push_value(Value::String("{0}:{1}:{2}".to_string())); + return Ok(self.push_call( + "customString", + vec![template, hour, minute_text, second_text], + )); + } + if name == "compressed" { + return self.lower_compressed(args, span); + } + if name == "compress" { + return self.lower_compress(args, span); + } + if name == "getSign" { + let [number] = args.as_slice() else { + return Err(self.unsupported("getSign requires one argument", span)); + }; + let number = self.lower_value(number)?; + let zero = self.push_number(0.0); + let positive = self.push_call(">", vec![number, zero]); + let one = self.push_number(1.0); + let negative_one = self.push_number(-1.0); + let sign = self.push_call("ifThenElse", vec![positive, one, negative_one]); + let is_zero = self.push_call("==", vec![number, zero]); + return Ok(self.push_call("ifThenElse", vec![is_zero, zero, sign])); + } + if name == "lerp" { + let [start, end, t] = args.as_slice() else { + return Err(self.unsupported("lerp requires three arguments", span)); + }; + let start = self.lower_value(start)?; + let end = self.lower_value(end)?; + let t = self.lower_value(t)?; + let one = self.push_number(1.0); + let weight = self.push_call("subtract", vec![one, t]); + let start_part = self.push_call("multiply", vec![start, weight]); + let end_part = self.push_call("multiply", vec![end, t]); + return Ok(self.push_call("add", vec![start_part, end_part])); + } + if name == "log" { + let (number, base) = match args.as_slice() { + [number] => (number, None), + [number, base] => (number, Some(base)), + _ => { + return Err(self.unsupported("log requires one or two arguments", span)); + } + }; + let number = self.lower_value(number)?; + let exponent = self.push_number(0.0001); + let powered = self.push_call("raiseToPower", vec![number, exponent]); + let one = self.push_number(1.0); + let delta = self.push_call("subtract", vec![powered, one]); + let scale = self.push_number(10000.0); + let approximation = self.push_call("multiply", vec![scale, delta]); + if let Some(base) = base { + let base = self.lower_value(base)?; + let base_powered = self.push_call("raiseToPower", vec![base, exponent]); + let base_one = self.push_number(1.0); + let base_delta = self.push_call("subtract", vec![base_powered, base_one]); + let base_scale = self.push_number(10000.0); + let base_log = self.push_call("multiply", vec![base_scale, base_delta]); + return Ok(self.push_call("divide", vec![approximation, base_log])); + } + return Ok(approximation); + } + if name == "getCurrentMap" && args.is_empty() && !self.used_maps.is_empty() { + return Ok(self.lower_bugged_current_map()); + } + if matches!(name.as_str(), "attacker" | "victim") && args.is_empty() { + return Ok(self.push_call(name, Vec::new())); + } + if name == "localPlayer" && args.is_empty() { + return Ok(self.push_call(name, Vec::new())); + } + if name == "ruleCondition" { + if !args.is_empty() { + return Err( + self.unsupported("ruleCondition does not accept arguments", span) + ); + } + let conditions = self.current_rule_conditions.clone().ok_or_else(|| { + self.unsupported("ruleCondition is only valid inside a rule", span) + })?; + let Some((first, rest)) = conditions.split_first() else { + return Ok(self.push_value(Value::Bool(true))); + }; + let mut combined = *first; + for condition in rest { + combined = self.push_call("and", vec![combined, *condition]); + } + return Ok(combined); + } + if name == "vect" && args.len() == 3 { + let x = self.lower_value(&args[0])?; + let y = self.lower_value(&args[1])?; + let z = self.lower_value(&args[2])?; + if let Some(member) = self.canonical_vector_member(x, y, z) { + Value::Enum { + value_type: "Vector".to_string(), + value: member.to_string(), + } + } else { + Value::Vector { x, y, z } + } + } else if matches!( + name.as_str(), + "createWorkshopSettingBool" + | "createWorkshopSettingEnum" + | "createWorkshopSettingInt" + | "createWorkshopSettingFloat" + | "createWorkshopSettingHero" + ) { + let mut lowered = self.lower_values(args)?; + // The sort order is the last parameter; OverPy writes 0 when omitted. + let (canonical, arity_without_sort_order) = workshop_setting_call(name); + if lowered.len() == arity_without_sort_order { + lowered.push(self.push_number(0.0)); + } + Value::Call { + name: canonical.to_string(), + args: self.value_args(&lowered), + } + } else if matches!(name.as_str(), "all" | "any") { + let call_name = if name == "all" { + "isTrueForAll" + } else { + "isTrueForAny" + }; + let [array] = args.as_slice() else { + return Err(self.unsupported( + format!("{name} requires exactly one array argument"), + span, + )); + }; + let (array, condition) = match array { + Expr::Comprehension { + element, + variable, + index, + iterable, + .. + } => { + if index.is_some() { + return Err(self.unsupported( + format!("{name} does not support an index binder"), + span, + )); + } + let iterable = self.lower_value(iterable)?; + self.array_bindings.push(ArrayBinding { + element: variable.clone(), + index: None, + }); + let condition = self.lower_value(element); + self.array_bindings.pop(); + (iterable, condition?) + } + array => ( + self.lower_value(array)?, + self.push_call("currentArrayElement", Vec::new()), + ), + }; + Value::Call { + name: call_name.to_string(), + args: self.value_args(&[array, condition]), + } + } else if matches!(name.as_str(), "ceil" | "floor" | "round") { + let [value] = args.as_slice() else { + return Err(self.unsupported( + format!("{name} requires exactly one numeric argument"), + span, + )); + }; + let rounding = match name.as_str() { + "ceil" => "UP", + "floor" => "DOWN", + "round" => "NEAREST", + _ => unreachable!(), + }; + let rounding = self.push_value(Value::Enum { + value_type: "Rounding".to_string(), + value: rounding.to_string(), + }); + let value = self.lower_value(value)?; + Value::Call { + name: "roundToInteger".to_string(), + args: self.value_args(&[value, rounding]), + } + } else if name == "sorted" { + let (array, key) = match args.as_slice() { + [array] => ( + self.lower_value(array)?, + self.push_call("currentArrayElement", Vec::new()), + ), + [ + array, + Expr::Lambda { + params, body, span, .. + }, + ] => { + let array = self.lower_value(array)?; + let key = self.lower_array_callback(params, body, *span)?; + (array, key) + } + _ => { + return Err(self.unsupported( + "sorted requires an array and an optional lambda key", + span, + )); + } + }; + Value::Call { + name: "sortedArray".to_string(), + args: self.value_args(&[array, key]), + } + } else { + let function = self + .compiler + .manifest + .resolve_function(name) + .ok_or_else(|| self.unsupported(format!("unknown value '{name}'"), span))?; + if !matches!(function.kind, FunctionKind::Value) { + return Err( + self.unsupported(format!("'{name}' is not a generic OPY value"), span) + ); + } + let catalog_id = function.catalog_id.as_ref().ok_or_else(|| { + self.unsupported( + format!( + "value '{}' requires a special lowering not in #46", + function.id + ), + span, + ) + })?; + if function.id == "getAllPlayers" { + return Ok(self.lower_all_players()); + } + let lowered_args = self.lower_values(args)?; + Value::Call { + name: catalog_id.clone(), + args: self.value_args(&lowered_args), + } + } + } + Expr::ReceiverCall { + receiver, + name, + args, + .. + } => { + if name == "getOppositeTeam" { + if !args.is_empty() { + return Err(self.unsupported("getOppositeTeam requires no arguments", span)); + } + let receiver = self.lower_value(receiver)?; + let team = self.push_call("teamOf", vec![receiver]); + return Ok(self.push_call("oppositeTeamOf", vec![team])); + } + if name == "toArray" { + if !args.is_empty() { + return Err(self.unsupported("toArray requires no arguments", span)); + } + let Expr::Type { + name: type_name, .. + } = receiver.as_ref() + else { + return Err( + self.unsupported("toArray requires an enum type receiver", span) + ); + }; + let domain_name = match type_name.as_str() { + "Clip" => "Clipping", + _ => type_name.as_str(), + }; + let Some(domain) = self.compiler.catalog.enum_domain(domain_name) else { + return Err( + self.unsupported(format!("unknown enum type '{type_name}'"), span) + ); + }; + let values = domain + .members + .iter() + .map(|member| { + self.push_value(Value::Enum { + value_type: domain_name.to_string(), + value: member.member.clone(), + }) + }) + .collect(); + return Ok(self.push_call("array", values)); + } + if matches!(name.as_str(), "all" | "any") { + let receiver = self.lower_value(receiver)?; + let condition = match args.as_slice() { + [] => self.push_call("currentArrayElement", Vec::new()), + [ + Expr::Lambda { + params, body, span, .. + }, + ] => self.lower_array_callback(params, body, *span)?, + _ => { + return Err(self.unsupported( + format!("{name} requires zero or one lambda argument"), + span, + )); + } + }; + let args = self.value_args(&[receiver, condition]); + return Ok(self.push_value(Value::Call { + name: if name == "all" { + "isTrueForAll" + } else { + "isTrueForAny" + } + .to_string(), + args, + })); + } + let function = self.compiler.manifest.resolve_member(name).ok_or_else(|| { + self.unsupported(format!("unknown member value '{name}'"), span) + })?; + if !matches!(function.kind, FunctionKind::MemberValue) { + return Err(self.unsupported(format!("'{name}' is not a member value"), span)); + } + if function.id == "unique" { + if !args.is_empty() { + return Err(self.unsupported("unique requires no arguments", span)); + } + let receiver = self.lower_value(receiver)?; + let current_element = self.push_call("currentArrayElement", Vec::new()); + let first_index = + self.push_call("indexOfArrayValue", vec![receiver, current_element]); + let current_index = self.push_call("currentArrayIndex", Vec::new()); + let condition = self.push_call("==", vec![first_index, current_index]); + return Ok(self.push_call("filteredArray", vec![receiver, condition])); + } + if function.id == "reverse" { + if !args.is_empty() { + return Err(self.unsupported("reverse requires no arguments", span)); + } + let receiver = self.lower_value(receiver)?; + let index = self.push_call("currentArrayIndex", Vec::new()); + let key = self.push_call("-", vec![index]); + return Ok(self.push_call("sortedArray", vec![receiver, key])); + } + if function.id == "getEffectiveHero" { + if !args.is_empty() { + return Err( + self.unsupported("getEffectiveHero requires no arguments", span) + ); + } + let receiver = self.lower_value(receiver)?; + let duplicated = self.push_call("getHeroOfDuplication", vec![receiver]); + let hero = self.push_call("getHero", vec![receiver]); + let null = self.push_value(Value::Null); + let condition = self.push_call("==", vec![duplicated, null]); + return Ok(self.push_call("ifThenElse", vec![condition, hero, duplicated])); + } + if function.id == "getRealPlayersInViewAngle" { + let [team, view_angle] = args.as_slice() else { + return Err(self.unsupported( + "getRealPlayersInViewAngle requires team and view angle", + span, + )); + }; + let receiver = self.lower_value(receiver)?; + let team = self.lower_value(team)?; + let view_angle = self.lower_value(view_angle)?; + let players = + self.push_call("getPlayersInViewAngle", vec![receiver, team, view_angle]); + let current = self.push_call("currentArrayElement", Vec::new()); + let alive = self.push_call("isAlive", vec![current]); + let spawned = self.push_call("hasSpawned", vec![current]); + let condition = self.push_call("and", vec![alive, spawned]); + return Ok(self.push_call("filteredArray", vec![players, condition])); + } + if matches!( + function.id.as_str(), + "getRealPlayerClosestToReticle" | "getRealPlayersClosestToReticle" + ) { + let [team] = args.as_slice() else { + return Err(self + .unsupported("getRealPlayersClosestToReticle requires a team", span)); + }; + let receiver = self.lower_value(receiver)?; + let team = self.lower_value(team)?; + let players = self.push_call("getLivingPlayers", vec![team]); + let current = self.push_call("currentArrayElement", Vec::new()); + let spawned = self.push_call("hasSpawned", vec![current]); + let not_self = self.push_call("!=", vec![current, receiver]); + let condition = self.push_call("and", vec![spawned, not_self]); + let players = self.push_call("filteredArray", vec![players, condition]); + let facing = self.push_call("getFacingDirection", vec![receiver]); + let eye_position = self.push_call("getEyePosition", vec![receiver]); + let direction = self.push_call("subtract", vec![current, eye_position]); + let angle = self.push_call("angleBetweenVectors", vec![facing, direction]); + let sorted = self.push_call("sortedArray", vec![players, angle]); + return if function.id == "getRealPlayerClosestToReticle" { + Ok(self.push_call("firstOf", vec![sorted])) + } else { + Ok(sorted) + }; + } + if function.id == "map" { + let [ + Expr::Lambda { + params, body, span, .. + }, + ] = args.as_slice() + else { + return Err(self.unsupported("map requires one lambda argument", span)); + }; + let mapped = self.lower_array_callback(params, body, *span)?; + let receiver = self.lower_value(receiver)?; + return Ok(self.push_call("mappedArray", vec![receiver, mapped])); + } + if matches!( + function.id.as_str(), + "getHitPosition" | "getPlayerHit" | "getNormal" + ) { + let member_name = function.id.as_str(); + let Expr::Call { + name: receiver_name, + args: receiver_args, + .. + } = receiver.as_ref() + else { + return Err(self.unsupported( + format!("{member_name} requires a raycast receiver"), + span, + )); + }; + if receiver_name != "raycast" || !args.is_empty() { + return Err(self.unsupported( + format!("{member_name} requires raycast(...) with no member arguments"), + span, + )); + } + let catalog_id = function.catalog_id.clone().ok_or_else(|| { + self.unsupported( + format!("{member_name} has no canonical catalog identity"), + span, + ) + })?; + let lowered_args = self.lower_values(receiver_args)?; + return Ok(self.push_value(Value::Call { + name: catalog_id, + args: self.value_args(&lowered_args), + })); + } + if function.id == "filter" { + let [ + Expr::Lambda { + params, body, span, .. + }, + ] = args.as_slice() + else { + return Err(self.unsupported("filter requires one lambda argument", span)); + }; + let condition = self.lower_array_callback(params, body, *span)?; + let receiver = self.lower_value(receiver)?; + Value::Call { + name: "filteredArray".to_string(), + args: self.value_args(&[receiver, condition]), + } + } else if matches!(function.id.as_str(), "concat" | "exclude") { + let [value] = args.as_slice() else { + return Err(self.unsupported( + format!("{} requires exactly one argument", function.id), + span, + )); + }; + let receiver = self.lower_value(receiver)?; + let value = self.lower_value(value)?; + Value::Call { + name: if function.id == "concat" { + "appendToArray" + } else { + "removeFromArray" + } + .to_string(), + args: self.value_args(&[receiver, value]), + } + } else { + let catalog_id = function.catalog_id.as_ref().ok_or_else(|| { + self.unsupported( + format!( + "member value '{}' has no canonical catalog identity", + function.id + ), + span, + ) + })?; + let mut lowered = Vec::with_capacity(args.len() + 1); + lowered.push(self.lower_value(receiver)?); + lowered.extend(self.lower_values(args)?); + Value::Call { + name: catalog_id.clone(), + args: self.value_args(&lowered), + } + } + } + Expr::Member { + receiver, member, .. + } => { + let receiver = self.lower_value(receiver)?; + if let Some(name) = match member.as_str() { + "x" => Some("__xComponentOf__"), + "y" => Some("__yComponentOf__"), + "z" => Some("__zComponentOf__"), + _ => None, + } { + Value::Call { + name: name.to_string(), + args: self.value_args(&[receiver]), + } + } else { + let member = self.push_value(Value::String(member.clone())); + Value::Call { + name: "memberAccess".to_string(), + args: self.value_args(&[receiver, member]), + } + } + } + Expr::Comprehension { + element, + variable, + index, + iterable, + condition, + span: comprehension_span, + .. + } => { + if condition.is_some() && index.is_some() { + return Err(self.unsupported( + "comprehensions with both a filter and an index binder are not currently representable in canonical WIR", + *comprehension_span, + )); + } + let iterable = self.lower_value(iterable)?; + let iterable = if self.value_is_known_player(iterable) { + self.push_call("array", vec![iterable]) + } else { + iterable + }; + let binding = ArrayBinding { + element: variable.clone(), + index: index.clone(), + }; + self.array_bindings.push(binding); + let predicate = condition + .as_deref() + .map(|condition| self.lower_value(condition)); + let element = self.lower_value(element); + self.array_bindings.pop(); + let element = element?; + let iterable = if let Some(predicate) = predicate { + let predicate = predicate?; + let filtered = self.push_call("filteredArray", vec![iterable, predicate]); + let optimization = self.optimization_state_at(comprehension_span.as_ref()); + if optimization.enabled { + self.optimized_nodes.insert(filtered, optimization.strict); + } + filtered + } else { + iterable + }; + Value::Call { + name: "mappedArray".to_string(), + args: self.value_args(&[iterable, element]), + } + } + Expr::Lambda { span, .. } => { + return Err(self.unsupported( + "lambda expressions are only representable as supported array operation arguments", + *span, + )); + } + Expr::StringModifier { + modifier, + value, + span, + } => { + let value = match modifier.as_str() { + "b" => big_letters(value), + "c" => case_sensitive(value), + "w" => fullwidth(value), + _ => { + return Err(self.unsupported( + format!( + "string modifier '{modifier}' is not currently representable in canonical WIR" + ), + *span, + )); + } + }; + return Ok(self.lower_custom_string(value)); + } + _ => { + return Err(self.unsupported( + format!( + "expression '{}' is not currently representable in canonical WIR", + expr.kind_name() + ), + span, + )); + } + }; + let value_id = self.push_value(value); + let Some(Value::Call { name, args }) = self.values.get(value_id) else { + return Ok(value_id); + }; + let name = name.clone(); + let args = args.clone(); + let mut args = self.normalize_contextual_arguments(&name, args); + self.apply_replacements_to_values(&name, &mut args, span); + if let Some(Value::Call { + args: target_args, .. + }) = self.values.get_mut(value_id) + { + *target_args = args; + } + Ok(value_id) + } + + /// `getCurrentMap() == Map.X`: the maps whose value comparison the + /// Workshop gets wrong are compared as text instead. + fn lower_current_map_equality(&mut self, left: &Expr, right: &Expr) -> Option { + let is_current_map = |expr: &Expr| matches!(expr, Expr::Call { name, args, .. } if name == "getCurrentMap" && args.is_empty()); + let map_of = |expr: &Expr| match expr { + Expr::Enum { + value_type, value, .. + } if value_type == "Map" => Some(value.clone()), + _ => None, + }; + let map = match (map_of(left), map_of(right)) { + (Some(map), None) if is_current_map(right) => map, + (None, Some(map)) if is_current_map(left) => map, + _ => return None, + }; + let current = self.push_call("currentMap", Vec::new()); + let map_value = self.push_value(Value::Enum { + value_type: "Map".to_string(), + value: map.clone(), + }); + if !TEXT_COMPARED_MAPS.contains(&map.as_str()) { + return Some(self.push_call("==", vec![current, map_value])); + } + let format = self.push_value(Value::String("{0}".to_string())); + let current_text = self.push_call("customString", vec![format, current]); + let format = self.push_value(Value::String("{0}".to_string())); + let map_text = self.push_call("customString", vec![format, map_value]); + Some(self.push_call("==", vec![current_text, map_text])) + } + + /// A bare `getCurrentMap()` selects the used map from the bugged ones by + /// its text, since comparing the map values themselves fails for them. + fn lower_bugged_current_map(&mut self) -> ValueId { + let mut maps: Vec = self + .used_maps + .clone() + .into_iter() + .map(|map| { + self.push_value(Value::Enum { + value_type: "Map".to_string(), + value: map.to_string(), + }) + }) + .collect(); + maps.push(self.push_call("currentMap", Vec::new())); + let candidates = self.push_call("array", maps); + let current = self.push_call("currentMap", Vec::new()); + let format = self.push_value(Value::String("{0}".to_string())); + let current_text = self.push_call("customString", vec![format, current]); + let element = self.push_call("currentArrayElement", Vec::new()); + let empty = self.push_call("emptyArray", Vec::new()); + let element_text = self.push_call("stringSplit", vec![element, empty]); + let matches = self.push_call("==", vec![current_text, element_text]); + let filtered = self.push_call("filteredArray", vec![candidates, matches]); + self.push_call("firstOf", vec![filtered]) + } + + pub(super) fn apply_replacements_to_values( + &mut self, + call_id: &str, + args: &mut [ValueId], + span: Option, + ) { + for value in args { + *value = self.apply_replacement(*value, call_id, span); + } + } + + fn apply_replacement( + &mut self, + value_id: ValueId, + call_id: &str, + span: Option, + ) -> ValueId { + let optimization = self.optimization_state_at(span.as_ref()); + if !optimization.enabled + || !optimization.for_size + || matches!( + call_id, + "workshopSettingToggle" + | "workshopSettingCombo" + | "workshopSettingInteger" + | "workshopSettingFloat" + ) + { + return value_id; + } + let replacement = |name: &str, hir: &hir::Program| { + hir.preprocessing + .replacements + .iter() + .find(|value| value.value == name) + .is_some() + }; + match self.value(value_id).clone() { + Value::Number(0.0) => { + let name = [ + "getCapturePercentage", + "getPayloadProgressPercentage", + "isMatchComplete", + ] + .into_iter() + .find(|name| replacement(name, self.hir)); + name.map_or(value_id, |name| self.push_call(name, Vec::new())) + } + Value::Number(1.0) => { + if replacement("getMatchRound", self.hir) { + self.push_call("getMatchRound", Vec::new()) + } else { + value_id + } + } + Value::Enum { value_type, value } if value_type == "Team" && value == "TEAM_1" => { + if replacement("getControlScoringTeam", self.hir) { + self.push_call("getControlScoringTeam", Vec::new()) + } else { + value_id + } + } + Value::String(value) if value.is_empty() => { + if replacement("emptyArray", self.hir) { + self.push_call("emptyArray", Vec::new()) + } else if replacement("variable", self.hir) { + self.push_value(Value::GlobalVariable(EMPTY_STRING_NAME.to_string())) + } else { + value_id + } + } + Value::Call { name, args } + if name == "customString" + && args.len() == 1 + && self.value_is_empty_string(args[0]) => + { + if replacement("emptyArray", self.hir) { + self.push_call("emptyArray", Vec::new()) + } else if replacement("variable", self.hir) { + self.push_value(Value::GlobalVariable(EMPTY_STRING_NAME.to_string())) + } else { + value_id + } + } + _ => value_id, + } + } + + fn lower_compressed( + &mut self, + args: &[Expr], + span: Option, + ) -> Result { + self.lower_compressed_mode(args, span, true) + } + + fn lower_decompression( + &mut self, + text: &Expr, + is_vector: bool, + ) -> Result { + let text = self.lower_value(text)?; + let null = self.push_value(Value::Null); + let separator = self.push_call("firstOf", vec![null]); + let split = self.push_call("stringSplit", vec![text, separator]); + let alphabet = if has_directive(self.hir, "useVariableForCompressionAlphabet") { + let variable = *self + .globals + .get(COMPRESSION_ALPHABET_NAME) + .expect("compression alphabet variable is created"); + self.push_value(Value::GlobalVariable(self.global_names[variable].clone())) + } else { + self.lower_custom_string(compression_alphabet()) + }; + let decoded = if has_directive(self.hir, "useVariableForCompressionAlphabet") { + split + } else { + let current = self.push_call("currentArrayElement", Vec::new()); + let alphabet = self.push_call("appendToArray", vec![current, alphabet]); + self.push_call("mappedArray", vec![split, alphabet]) + }; + let width = if is_vector { 3 } else { 4 }; + let min_decimal_place = if is_vector { -2.0 } else { -3.0 }; + let offset = if is_vector { 5000.0 } else { 50000.0 }; + let component = |this: &mut Self, component_offset: usize| { + let current = this.push_call("currentArrayElement", Vec::new()); + let mut terms = Vec::with_capacity(width); + for index in 0..width { + let position = this.push_number((index + component_offset) as f64); + let character = this.push_call("charAt", vec![current, position]); + let formula_alphabet = + if has_directive(this.hir, "useVariableForCompressionAlphabet") { + alphabet + } else { + this.push_call("lastOf", vec![current]) + }; + let digit = this.push_call("strIndex", vec![formula_alphabet, character]); + let power = 100_f64.powf(index as f64 + min_decimal_place / 2.0); + let power = this.push_number(power); + terms.push(this.push_call("multiply", vec![power, digit])); + } + let mut value = terms + .first() + .copied() + .unwrap_or_else(|| this.push_number(0.0)); + for term in terms.into_iter().skip(1) { + value = this.push_call("add", vec![value, term]); + } + let offset = this.push_number(offset); + this.push_call("subtract", vec![value, offset]) + }; + if is_vector { + let x = component(self, 0); + let y = component(self, width * 2); + let z = component(self, width); + let vector = self.push_call("vector", vec![x, y, z]); + Ok(self.push_call("mappedArray", vec![decoded, vector])) + } else { + let number = component(self, 0); + Ok(self.push_call("mappedArray", vec![decoded, number])) + } + } + + fn lower_compressed_mode( + &mut self, + args: &[Expr], + span: Option, + decode: bool, + ) -> Result { + let [Expr::Array { elements, .. }] = args else { + return Err(self.unsupported( + "compressed requires one literal array of numbers or vectors", + span, + )); + }; + if elements.is_empty() { + return Err(self.unsupported("cannot compress an empty array", span)); + } + + let Some(numbers) = elements + .iter() + .map(|element| match element { + Expr::Null { .. } => Some(vec![0.0]), + Expr::Number { value, .. } => Some(vec![*value]), + Expr::Unary { op, operand, .. } if matches!(op.as_str(), "+" | "-") => { + hir::visit::literal_number(operand) + .map(|value| vec![if op == "-" { -value } else { value }]) + } + Expr::Vector { x, y, z, .. } => Some(vec![ + hir::visit::literal_number(x)?, + hir::visit::literal_number(y)?, + hir::visit::literal_number(z)?, + ]), + _ => None, + }) + .collect::>>() + else { + return Err(self.unsupported("compressed requires literal numbers or vectors", span)); + }; + let is_vector = numbers.first().is_some_and(|value| value.len() == 3); + if numbers.iter().any(|value| (value.len() == 3) != is_vector) { + return Err(self.unsupported("compressed cannot mix numbers and vectors", span)); + } + let flattened = numbers.iter().flatten().copied().collect::>(); + let limit = if is_vector { 4999.0 } else { 49999.0 }; + if flattened.iter().any(|value| value.abs() >= limit) { + return Err(self.unsupported("compressed values exceed the supported magnitude", span)); + } + + let max_decimals = if is_vector { 2 } else { 3 }; + let compression_offset = if decode { + flattened.iter().copied().fold(0.0_f64, f64::min).min(0.0) + } else if is_vector { + -5000.0 + } else { + -50000.0 + }; + let adjusted = flattened + .iter() + .map(|value| value - compression_offset) + .collect::>(); + let mut strings = adjusted + .iter() + .map(|value| { + format!("{value:.precision$}", precision = max_decimals) + .replace('.', "") + .chars() + .rev() + .collect::() + }) + .collect::>(); + let mut min_decimal_place = -(max_decimals as i32); + if decode { + while strings.iter().all(|value| value.starts_with('0')) { + for value in &mut strings { + value.remove(0); + } + min_decimal_place += 1; + } + } else { + min_decimal_place = if is_vector { -2 } else { -3 }; + } + let max_decimal_place = if decode { + min_decimal_place + strings.iter().map(String::len).max().unwrap_or_default() as i32 + } else if is_vector { + 4 + } else { + 5 + }; + for value in &mut strings { + let trimmed = value.trim_end_matches('0'); + *value = if trimmed.is_empty() { + "0".to_string() + } else { + trimmed.to_string() + }; + } + + let alphabet = compression_alphabet_chars(); + let encode = |value: &str| -> Option { + let mut encoded = String::new(); + let chars = value.as_bytes(); + for pair in chars.chunks(2) { + let number = if pair.len() == 1 { + u16::from(pair[0] - b'0') + } else { + u16::from(pair[1] - b'0') * 10 + u16::from(pair[0] - b'0') + }; + encoded.push(*alphabet.get(number as usize)?); + } + Some(encoded) + }; + let compressed = if is_vector { + let width = (((max_decimal_place - min_decimal_place + 1) / 2) * 2) as usize; + strings + .chunks(3) + .map(|values| { + let mut grouped = String::new(); + for index in [0, 2, 1] { + let mut value = values[index].clone(); + if index != 1 { + value.push_str(&"0".repeat(width.saturating_sub(value.len()))); + } else { + value = value.trim_end_matches('0').to_string(); + if value.is_empty() { + value.push('0'); + } + } + grouped.push_str(&value); + } + encode(&grouped) + }) + .collect::>>() + .ok_or_else(|| self.unsupported("compressed value cannot be encoded", span))? + .join("0") + } else { + strings + .iter() + .map(|value| encode(value)) + .collect::>>() + .ok_or_else(|| self.unsupported("compressed value cannot be encoded", span))? + .join("0") + }; + if !decode { + return Ok(self.lower_custom_string(compressed)); + } + let compressed_string = self.lower_custom_string(compressed); + let null = self.push_value(Value::Null); + let separator = self.push_call("firstOf", vec![null]); + let split = self.push_call("stringSplit", vec![compressed_string, separator]); + let alphabet_value = if has_directive(self.hir, "useVariableForCompressionAlphabet") { + let variable = *self + .globals + .get(COMPRESSION_ALPHABET_NAME) + .expect("compression alphabet variable is created"); + self.push_value(Value::GlobalVariable(self.global_names[variable].clone())) + } else { + self.lower_custom_string(compression_alphabet()) + }; + let decoded = if has_directive(self.hir, "useVariableForCompressionAlphabet") { + split + } else { + let current = self.push_call("currentArrayElement", Vec::new()); + let alphabet = self.push_call("appendToArray", vec![current, alphabet_value]); + self.push_call("mappedArray", vec![split, alphabet]) + }; + let width = ((max_decimal_place - min_decimal_place + 1) / 2) as usize; + let optimization = self.optimization_state_at(span.as_ref()); + let component = |this: &mut Self, component_offset: usize| { + let current = this.push_call("currentArrayElement", Vec::new()); + let mut terms = Vec::with_capacity(width); + for index in 0..width { + let position = this.push_number((index + component_offset) as f64); + let character = this.push_call("charAt", vec![current, position]); + let formula_alphabet = + if has_directive(this.hir, "useVariableForCompressionAlphabet") { + alphabet_value + } else { + this.push_call("lastOf", vec![current]) + }; + let digit = this.push_call("strIndex", vec![formula_alphabet, character]); + let power = 100_f64.powf(index as f64 + f64::from(min_decimal_place) / 2.0); + let power = this.push_number(power); + let weighted = this.push_call("multiply", vec![power, digit]); + if optimization.enabled { + this.optimized_nodes.insert(weighted, optimization.strict); + } + terms.push(weighted); + } + let mut value = terms + .first() + .copied() + .unwrap_or_else(|| this.push_number(0.0)); + for term in terms.into_iter().skip(1) { + value = this.push_call("add", vec![value, term]); + } + if is_vector || compression_offset == 0.0 { + value + } else { + let offset = this.push_number(compression_offset); + this.push_call("add", vec![value, offset]) + } + }; + let value = if is_vector { + let x = component(self, 0); + let y = component(self, width * 2); + let z = component(self, width); + let vector = self.push_call("vector", vec![x, y, z]); + let value = if compression_offset == 0.0 { + vector + } else { + let offset = self.push_number(-compression_offset); + let offset = self.push_call("vector", vec![offset, offset, offset]); + self.push_call("subtract", vec![vector, offset]) + }; + self.push_call("mappedArray", vec![decoded, value]) + } else { + let number = component(self, 0); + self.push_call("mappedArray", vec![decoded, number]) + }; + Ok(value) + } + + fn lower_compress( + &mut self, + args: &[Expr], + span: Option, + ) -> Result { + self.lower_compressed_mode(args, span, false) + } + + fn strict_optimization_active(&self, expr: &Expr) -> bool { + self.optimization_state_at(expr.span()).strict + } + + pub(super) fn optimization_state_at(&self, span: Option<&HirSpan>) -> OptimizationState { + let Some(span) = span else { + return self.hir.preprocessing.optimization.clone(); + }; + let mut active = None; + for directive in &self.hir.preprocessing.directives { + let Some(directive_span) = directive.span else { + continue; + }; + if directive_span.file != span.file { + continue; + } + if directive_span.start.line < span.start.line + || (directive_span.start.line == span.start.line + && directive_span.start.col <= span.start.col) + { + active = Some(directive.state.optimization.clone()); + } else { + break; + } + } + active + .or_else(|| { + self.hir + .preprocessing + .source_file_initial_optimization + .get(&span.file) + .cloned() + }) + .unwrap_or_else(|| self.hir.preprocessing.optimization.clone()) + } + + fn lower_array_callback( + &mut self, + params: &[String], + body: &Expr, + span: Option, + ) -> Result { + if !(1..=2).contains(¶ms.len()) { + return Err(self.unsupported( + "array callbacks require one element parameter and at most one index parameter", + span, + )); + } + if params.windows(2).any(|pair| pair[0] == pair[1]) { + return Err( + self.unsupported("array callback parameters must have distinct names", span) + ); + } + self.array_bindings.push(ArrayBinding { + element: params[0].clone(), + index: params.get(1).cloned(), + }); + let result = self.lower_value(body); + self.array_bindings.pop(); + result + } + + fn lower_workshop_setting( + &mut self, + args: &[Expr], + span: Option, + ) -> Result { + let [ + Expr::Type { + name: setting_type, + args: type_args, + span: type_span, + }, + category, + setting_name, + default, + sort_order, + ] = args + else { + return Err(self.unsupported( + "createWorkshopSetting requires a type and four value arguments", + span, + )); + }; + + let catalog_name = match (setting_type.as_str(), type_args.as_slice()) { + ("bool", []) => "createWorkshopSettingBool", + ("int", [_, _]) => "createWorkshopSettingInt", + ("float", [_, _]) => "createWorkshopSettingFloat", + ("int", []) | ("float", []) => { + return Err(self.unsupported( + format!("createWorkshopSetting type '{setting_type}' requires a numeric range"), + type_span.or(span), + )); + } + _ => { + return Err(self.unsupported( + format!("unsupported createWorkshopSetting type '{setting_type}'"), + type_span.or(span), + )); + } + }; + + // OverPy uses an ideographic space for an empty setting category so + // the generated Workshop setting has a non-empty category value. + let category = match category { + Expr::String { value, .. } if value.is_empty() => { + self.push_value(Value::String("\u{3000}".to_string())) + } + _ => self.lower_value(category)?, + }; + let mut lowered = vec![ + category, + self.lower_value(setting_name)?, + self.lower_value(default)?, + ]; + if let [minimum, maximum] = type_args.as_slice() { + lowered.push(self.lower_value(minimum)?); + lowered.push(self.lower_value(maximum)?); + } + lowered.push(self.lower_value(sort_order)?); + Ok(self.push_call(workshop_setting_call(catalog_name).0, lowered)) + } +} From 6662011c65f9be4a8bf142b51ffd9d47c8000fb2 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 00:20:02 +0800 Subject: [PATCH 06/67] refactor(opy-provider): share LSP position conversion --- crates/opy-provider/src/main.rs | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/crates/opy-provider/src/main.rs b/crates/opy-provider/src/main.rs index 7bac535b..9ae63014 100644 --- a/crates/opy-provider/src/main.rs +++ b/crates/opy-provider/src/main.rs @@ -1070,17 +1070,7 @@ fn document_lsp_position( .map(|document| document.text.clone()) .or_else(|| std::fs::read_to_string(&resolved).ok()) .unwrap_or_default(); - let character = source - .lines() - .nth(line.saturating_sub(1) as usize) - .map(|text| { - text.chars() - .take(col.saturating_sub(1) as usize) - .map(char::len_utf16) - .sum::() as u32 - }) - .unwrap_or_else(|| col.saturating_sub(1)); - json!({ "line": line.saturating_sub(1), "character": character }) + lsp_position(&source, line, col) } fn check_result(project: &LoadedProject, outcome: &CheckOutcome) -> Value { @@ -1209,17 +1199,21 @@ fn diagnostic_range(project: &LoadedProject, location: Option<&SourceLocation>) }); }; json!({ - "start": lsp_position(project, &location.path, location.start.line, location.start.col), - "end": lsp_position(project, &location.path, location.end.line, location.end.col), + "start": project_lsp_position(project, &location.path, location.start.line, location.start.col), + "end": project_lsp_position(project, &location.path, location.end.line, location.end.col), }) } -fn lsp_position(project: &LoadedProject, path: &str, line: u32, col: u32) -> Value { +fn project_lsp_position(project: &LoadedProject, path: &str, line: u32, col: u32) -> Value { let source = if path == path_string(project.filesystem.main_path()) { project.filesystem.source().to_owned() } else { std::fs::read_to_string(resolved_project_path(project, path)).unwrap_or_default() }; + lsp_position(&source, line, col) +} + +fn lsp_position(source: &str, line: u32, col: u32) -> Value { let character = source .lines() .nth(line.saturating_sub(1) as usize) From 1f680a8eaf5cc72c6b8c77fd4eb3cf51f117a3a1 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 00:28:42 +0800 Subject: [PATCH 07/67] refactor(opy-rs): unify settings scalar parsing --- crates/opy-rs/src/settings.rs | 157 ++++++++++++++-------------------- 1 file changed, 65 insertions(+), 92 deletions(-) diff --git a/crates/opy-rs/src/settings.rs b/crates/opy-rs/src/settings.rs index b36d648e..91847748 100644 --- a/crates/opy-rs/src/settings.rs +++ b/crates/opy-rs/src/settings.rs @@ -595,6 +595,12 @@ struct Jsonc<'a> { file: u32, } +enum ScalarValue { + String(String), + Bool(bool), + Number(f64), +} + impl Jsonc<'_> { fn here(&self) -> Position { Position::new(self.line, self.col) @@ -759,127 +765,94 @@ impl Jsonc<'_> { fn parse_value(&mut self) -> OpyResult<(cst::SettingsNode, Position)> { let start = self.here(); let ch = self.peek(); - let node = match ch { + let saved = (self.pos, self.line, self.col); + let scalar = match ch { Some('"') | Some('\'') => { - let saved = (self.pos, self.line, self.col); let value = self.parse_string_expression().ok_or_else(|| { self.error( "settings-invalid", "unterminated string in settings value".to_string(), ) })?; - self.skip_inline_whitespace(); - if self.is_expression_continuation() { - self.pos = saved.0; - self.line = saved.1; - self.col = saved.2; - cst::SettingsNode::Raw { - name: String::new(), - value: self.parse_expression_value(), - span: Span::new(self.file, start, self.here()), - } - } else { - cst::SettingsNode::String { - name: String::new(), - value, - span: Span::new(self.file, start, self.here()), - } - } + Some(ScalarValue::String(value)) } Some('t') => { - let saved = (self.pos, self.line, self.col); self.expect_word("true")?; - self.skip_inline_whitespace(); - if self.is_expression_continuation() { - self.pos = saved.0; - self.line = saved.1; - self.col = saved.2; - cst::SettingsNode::Raw { + Some(ScalarValue::Bool(true)) + } + Some('f') => { + self.expect_word("false")?; + Some(ScalarValue::Bool(false)) + } + Some(c) if c.is_ascii_digit() || c == '-' => { + Some(ScalarValue::Number(self.parse_number()?)) + } + _ => None, + }; + let node = if let Some(scalar) = scalar { + self.skip_inline_whitespace(); + if self.is_expression_continuation() { + self.pos = saved.0; + self.line = saved.1; + self.col = saved.2; + cst::SettingsNode::Raw { + name: String::new(), + value: self.parse_expression_value(), + span: Span::new(self.file, start, self.here()), + } + } else { + let span = Span::new(self.file, start, self.here()); + match scalar { + ScalarValue::String(value) => cst::SettingsNode::String { name: String::new(), - value: self.parse_expression_value(), - span: Span::new(self.file, start, self.here()), - } - } else { - cst::SettingsNode::Bool { + value, + span, + }, + ScalarValue::Bool(value) => cst::SettingsNode::Bool { name: String::new(), - value: true, - span: Span::new(self.file, start, self.here()), - } + value, + span, + }, + ScalarValue::Number(value) => cst::SettingsNode::Number { + name: String::new(), + value, + span, + }, } } - Some('f') => { - let saved = (self.pos, self.line, self.col); - self.expect_word("false")?; - self.skip_inline_whitespace(); - if self.is_expression_continuation() { - self.pos = saved.0; - self.line = saved.1; - self.col = saved.2; - cst::SettingsNode::Raw { + } else { + match ch { + Some('[') => { + let elements = self.parse_list()?; + cst::SettingsNode::List { name: String::new(), - value: self.parse_expression_value(), + elements, span: Span::new(self.file, start, self.here()), } - } else { - cst::SettingsNode::Bool { + } + Some('{') => { + let (children, _) = self.parse_object()?; + cst::SettingsNode::Group { name: String::new(), - value: false, + children, span: Span::new(self.file, start, self.here()), } } - } - Some(c) if c.is_ascii_digit() || c == '-' => { - let saved = (self.pos, self.line, self.col); - let value = self.parse_number()?; - self.skip_inline_whitespace(); - if self.is_expression_continuation() { - self.pos = saved.0; - self.line = saved.1; - self.col = saved.2; + _ => { let value = self.parse_expression_value(); - cst::SettingsNode::Raw { - name: String::new(), - value, - span: Span::new(self.file, start, self.here()), + if value.trim().is_empty() { + return Err(self.error( + "settings-invalid", + "expected a value in settings block".to_string(), + )); } - } else { - cst::SettingsNode::Number { + cst::SettingsNode::Raw { name: String::new(), value, span: Span::new(self.file, start, self.here()), } } } - Some('[') => { - let elements = self.parse_list()?; - cst::SettingsNode::List { - name: String::new(), - elements, - span: Span::new(self.file, start, self.here()), - } - } - Some('{') => { - let (children, _) = self.parse_object()?; - cst::SettingsNode::Group { - name: String::new(), - children, - span: Span::new(self.file, start, self.here()), - } - } - _ => { - let value = self.parse_expression_value(); - if value.trim().is_empty() { - return Err(self.error( - "settings-invalid", - "expected a value in settings block".to_string(), - )); - } - cst::SettingsNode::Raw { - name: String::new(), - value, - span: Span::new(self.file, start, self.here()), - } - } }; let end = self.here(); Ok((node, end)) From 7dd5756d31dec4ed3f8f1c88d020c03b1a7983d3 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 00:32:15 +0800 Subject: [PATCH 08/67] refactor(opy-rs): remove HIR validation wrappers --- crates/opy-rs/src/hir/validate.rs | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/crates/opy-rs/src/hir/validate.rs b/crates/opy-rs/src/hir/validate.rs index 10968c07..4c31724b 100644 --- a/crates/opy-rs/src/hir/validate.rs +++ b/crates/opy-rs/src/hir/validate.rs @@ -189,7 +189,7 @@ pub(crate) fn validate_program(program: &Program) -> Result<(), HirError> { check_unique(name, "global variable", *span, &mut tables.globals)?; tables.globals.push(name); if let Some(initializer) = initializer { - validate_expr(initializer, program, &tables)?; + validate_exprs(std::iter::once(initializer.as_ref()), program, &tables)?; } } Declaration::PlayerVariable { @@ -202,7 +202,7 @@ pub(crate) fn validate_program(program: &Program) -> Result<(), HirError> { check_unique(name, "player variable", *span, &mut tables.players)?; tables.players.push(name); if let Some(initializer) = initializer { - validate_expr(initializer, program, &tables)?; + validate_exprs(std::iter::once(initializer.as_ref()), program, &tables)?; } } Declaration::Subroutine { name, span, .. } => { @@ -214,7 +214,7 @@ pub(crate) fn validate_program(program: &Program) -> Result<(), HirError> { check_name(name, "constant", *span)?; check_unique(name, "constant", *span, &mut tables.constants)?; tables.constants.push(name); - validate_expr(value, program, &tables)?; + validate_exprs(std::iter::once(value.as_ref()), program, &tables)?; } Declaration::Macro { name, @@ -360,20 +360,12 @@ fn validate_rule(rule: &Rule, program: &Program, tables: &NameTables<'_>) -> Res rule.event.span, )); } - validate_expr_vec(&rule.event.args, program, tables)?; + validate_exprs(&rule.event.args, program, tables)?; check_span(rule.event.span, program.files.len())?; - validate_expr_vec(&rule.conditions, program, tables)?; + validate_exprs(&rule.conditions, program, tables)?; validate_stmts(&rule.actions, program, tables) } -fn validate_expr_vec( - expressions: &[Expr], - program: &Program, - tables: &NameTables<'_>, -) -> Result<(), HirError> { - validate_exprs(expressions, program, tables) -} - fn validate_stmts( statements: &[Stmt], program: &Program, @@ -399,10 +391,6 @@ fn validate_stmts( .map_or(Ok(()), Err) } -fn validate_expr(expr: &Expr, program: &Program, tables: &NameTables<'_>) -> Result<(), HirError> { - validate_exprs(std::iter::once(expr), program, tables) -} - fn validate_exprs<'expr>( expressions: impl IntoIterator, program: &Program, From b84eab02f7ee665d4ebd35441e52981e6123473e Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 00:36:10 +0800 Subject: [PATCH 09/67] refactor(opy-rs): reuse compile-time value display --- crates/opy-rs/src/compile_time.rs | 10 +++++----- crates/opy-rs/src/compiler/lowering.rs | 15 +-------------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/crates/opy-rs/src/compile_time.rs b/crates/opy-rs/src/compile_time.rs index 6349dcc5..32c37811 100644 --- a/crates/opy-rs/src/compile_time.rs +++ b/crates/opy-rs/src/compile_time.rs @@ -64,7 +64,7 @@ pub(crate) fn evaluate( .map(|arg| evaluate(arg, constants, bindings, stack)) .collect::>>()?; let mut result = text.clone(); - for (index, value) in values.iter().enumerate() { + for (index, value) in values.into_iter().enumerate() { result = result.replace(&format!("{{{index}}}"), &display(value)?); } Some(Value::String(result)) @@ -197,10 +197,10 @@ fn evaluate_index(collection: Value, index: Value) -> Option { } } -fn display(value: &Value) -> Option { +pub(crate) fn display(value: Value) -> Option { match value { - Value::Number(value) if value.is_finite() => Some(workshop_number_text(*value)), - Value::String(value) => Some(value.clone()), + Value::Number(value) if value.is_finite() => Some(workshop_number_text(value)), + Value::String(value) => Some(value), Value::Bool(value) => Some(value.to_string()), _ => None, } @@ -219,7 +219,7 @@ pub(crate) fn round_half_up(value: f64) -> f64 { } } -pub(crate) fn workshop_number_text(value: f64) -> String { +fn workshop_number_text(value: f64) -> String { let rounded = (value * 100.0).round() / 100.0; if rounded.fract() == 0.0 { format!("{}", rounded as i64) diff --git a/crates/opy-rs/src/compiler/lowering.rs b/crates/opy-rs/src/compiler/lowering.rs index 4de9ad0e..8561331e 100644 --- a/crates/opy-rs/src/compiler/lowering.rs +++ b/crates/opy-rs/src/compiler/lowering.rs @@ -1226,7 +1226,7 @@ impl<'a> Lowering<'a> { .map(|arg| { let mut stack = Vec::new(); crate::compile_time::evaluate(arg, &self.constants, &HashMap::new(), &mut stack) - .and_then(compile_time_value_text) + .and_then(crate::compile_time::display) }) .collect::>(); let dynamic_indexes = values @@ -2245,19 +2245,6 @@ fn split_format_chunks(text: &str, arg_count: usize) -> Option Option { - match value { - crate::compile_time::Value::Number(value) if value.is_finite() => { - Some(crate::compile_time::workshop_number_text(value)) - } - crate::compile_time::Value::Number(_) - | crate::compile_time::Value::Array(_) - | crate::compile_time::Value::Object(_) => None, - crate::compile_time::Value::String(value) => Some(value), - crate::compile_time::Value::Bool(value) => Some(value.to_string()), - } -} - fn debug_expr_text(expr: &Expr) -> String { match expr { Expr::Number { text, .. } => text.clone(), From d125a1b30e3e1a5b8305f8ee8dcbc225d6f39d07 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 00:53:29 +0800 Subject: [PATCH 10/67] refactor(opy-rs): share loop condition lowering --- crates/opy-rs/src/compiler/lowering/rules.rs | 39 +++++++++----------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/crates/opy-rs/src/compiler/lowering/rules.rs b/crates/opy-rs/src/compiler/lowering/rules.rs index fe569bab..0f7d52c8 100644 --- a/crates/opy-rs/src/compiler/lowering/rules.rs +++ b/crates/opy-rs/src/compiler/lowering/rules.rs @@ -1128,6 +1128,21 @@ impl<'a> Lowering<'a> { self.lower_loop_sequence_with_break_target(statements, &[], 0, BreakTarget::Loop) } + fn lower_condition_chain( + &mut self, + conditions: &[&Expr], + ) -> Result, IntegrationError> { + let mut lowered = None; + for expression in conditions { + let value = self.lower_value(expression)?; + lowered = Some(match lowered { + Some(left) => self.push_call("and", vec![left, value]), + None => value, + }); + } + Ok(lowered) + } + fn lower_loop_sequence_with_break_target( &mut self, statements: &[Stmt], @@ -1152,12 +1167,7 @@ impl<'a> Lowering<'a> { + self.canonical_action_width(after, statement.span().copied())?; if distance > 0 { let mut args = Vec::with_capacity(conditions.len() + 1); - if let Some((first, rest)) = conditions.split_first() { - let mut condition = self.lower_value(first)?; - for expression in rest { - let right = self.lower_value(expression)?; - condition = self.push_call("and", vec![condition, right]); - } + if let Some(condition) = self.lower_condition_chain(&conditions)? { args.push(condition); } let distance = self.push_number(distance as f64); @@ -1233,12 +1243,7 @@ impl<'a> Lowering<'a> { let distance = self.canonical_action_width(&middle, statement.span().copied())?; let mut args = Vec::with_capacity(conditions.len() + 1); - if let Some((first, rest)) = conditions.split_first() { - let mut condition = self.lower_value(first)?; - for expression in rest { - let right = self.lower_value(expression)?; - condition = self.push_call("and", vec![condition, right]); - } + if let Some(condition) = self.lower_condition_chain(&conditions)? { args.push(condition); } args.push(self.push_number(distance as f64)); @@ -1342,15 +1347,7 @@ impl<'a> Lowering<'a> { for (index, statement) in statements.iter().enumerate() { if let Some(conditions) = pure_continue_conditions(statement) { let tail = self.lower_do_while_body(&statements[index + 1..])?; - let mut condition = None; - for expression in conditions { - let value = self.lower_value(expression)?; - condition = Some(match condition { - Some(left) => self.push_call("and", vec![left, value]), - None => value, - }); - } - let action = if let Some(condition) = condition { + let action = if let Some(condition) = self.lower_condition_chain(&conditions)? { self.push_call_action("loopIf", &[condition]) } else { self.push_call_action("loop", &[]) From c2caa80bb4ce2f0ff1cfbcb6e300ae5c967f036e Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 01:00:41 +0800 Subject: [PATCH 11/67] refactor(opy-rs): centralize condition folding --- crates/opy-rs/src/compiler/lowering.rs | 12 ++++++++++++ crates/opy-rs/src/compiler/lowering/rules.rs | 11 ++--------- crates/opy-rs/src/compiler/lowering/values.rs | 11 +++-------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/crates/opy-rs/src/compiler/lowering.rs b/crates/opy-rs/src/compiler/lowering.rs index 8561331e..da77d649 100644 --- a/crates/opy-rs/src/compiler/lowering.rs +++ b/crates/opy-rs/src/compiler/lowering.rs @@ -1170,6 +1170,18 @@ impl<'a> Lowering<'a> { }) } + fn combine_conditions( + &mut self, + conditions: impl IntoIterator, + ) -> Option { + let mut conditions = conditions.into_iter(); + let mut combined = conditions.next()?; + for condition in conditions { + combined = self.push_call("and", vec![combined, condition]); + } + Some(combined) + } + fn normalize_contextual_argument( &mut self, call_id: &str, diff --git a/crates/opy-rs/src/compiler/lowering/rules.rs b/crates/opy-rs/src/compiler/lowering/rules.rs index 0f7d52c8..e0dec0f3 100644 --- a/crates/opy-rs/src/compiler/lowering/rules.rs +++ b/crates/opy-rs/src/compiler/lowering/rules.rs @@ -1132,15 +1132,8 @@ impl<'a> Lowering<'a> { &mut self, conditions: &[&Expr], ) -> Result, IntegrationError> { - let mut lowered = None; - for expression in conditions { - let value = self.lower_value(expression)?; - lowered = Some(match lowered { - Some(left) => self.push_call("and", vec![left, value]), - None => value, - }); - } - Ok(lowered) + let values = self.lower_values(conditions.iter().copied())?; + Ok(self.combine_conditions(values)) } fn lower_loop_sequence_with_break_target( diff --git a/crates/opy-rs/src/compiler/lowering/values.rs b/crates/opy-rs/src/compiler/lowering/values.rs index 344d145b..9692888f 100644 --- a/crates/opy-rs/src/compiler/lowering/values.rs +++ b/crates/opy-rs/src/compiler/lowering/values.rs @@ -719,14 +719,9 @@ impl<'a> Lowering<'a> { let conditions = self.current_rule_conditions.clone().ok_or_else(|| { self.unsupported("ruleCondition is only valid inside a rule", span) })?; - let Some((first, rest)) = conditions.split_first() else { - return Ok(self.push_value(Value::Bool(true))); - }; - let mut combined = *first; - for condition in rest { - combined = self.push_call("and", vec![combined, *condition]); - } - return Ok(combined); + return Ok(self + .combine_conditions(conditions) + .unwrap_or_else(|| self.push_value(Value::Bool(true)))); } if name == "vect" && args.len() == 3 { let x = self.lower_value(&args[0])?; From 9286f4db809da77fad467b1fef34a4660c8bf7db Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 01:10:42 +0800 Subject: [PATCH 12/67] refactor(opy-rs): share settings source cursor --- crates/opy-rs/src/settings.rs | 148 +++++++++++++++------------------- 1 file changed, 67 insertions(+), 81 deletions(-) diff --git a/crates/opy-rs/src/settings.rs b/crates/opy-rs/src/settings.rs index 91847748..65878294 100644 --- a/crates/opy-rs/src/settings.rs +++ b/crates/opy-rs/src/settings.rs @@ -52,19 +52,20 @@ pub struct SettingsBlock { /// matching respects `"`/`'` strings, `\` escapes, and nesting; an /// unterminated block is `settings-invalid`. pub fn find_blocks(text: &str, file_id: u32) -> OpyResult> { - let mut scanner = Scanner { + let mut scanner = Cursor { text, pos: 0, char_pos: 0, line: 1, col: 1, + file: file_id, }; let mut blocks = Vec::new(); let mut in_block_comment = false; let mut string_quote = None; let mut escaped = false; let mut seen_first_construct = false; - while let Some(ch) = scanner.peek(0) { + while let Some(ch) = scanner.peek() { if let Some(quote) = string_quote { if escaped { escaped = false; @@ -73,33 +74,33 @@ pub fn find_blocks(text: &str, file_id: u32) -> OpyResult> { } else if ch == quote { string_quote = None; } - scanner.advance(1); + scanner.advance_by(1); continue; } if in_block_comment { - if ch == '*' && scanner.peek(1) == Some('/') { + if ch == '*' && scanner.peek_at(1) == Some('/') { in_block_comment = false; - scanner.advance(2); + scanner.advance_by(2); } else { - scanner.advance(1); + scanner.advance_by(1); } continue; } if matches!(ch, '"' | '\'') { string_quote = Some(ch); - scanner.advance(1); + scanner.advance_by(1); continue; } if ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' { - scanner.advance(1); + scanner.advance_by(1); continue; } if ch == '#' { scanner.skip_to_eol(); continue; } - if ch == '/' && scanner.peek(1) == Some('*') { - scanner.advance(2); + if ch == '/' && scanner.peek_at(1) == Some('*') { + scanner.advance_by(2); in_block_comment = true; continue; } @@ -126,7 +127,7 @@ pub fn find_blocks(text: &str, file_id: u32) -> OpyResult> { continue; } seen_first_construct = true; - scanner.advance(1); + scanner.advance_by(1); } Ok(blocks) } @@ -134,32 +135,32 @@ pub fn find_blocks(text: &str, file_id: u32) -> OpyResult> { /// Match the braces of one `settings { ... }` block, returning the extracted /// block. `scanner` is positioned just past the `settings` keyword. fn match_block( - scanner: &mut Scanner<'_>, + scanner: &mut Cursor<'_>, keyword_start: Position, keyword_offset: usize, keyword_span: Span, ) -> OpyResult { - scanner.skip_whitespace(); - if scanner.peek(0) != Some('{') { - let Some(quote) = scanner.peek(0).filter(|ch| matches!(ch, '"' | '\'')) else { + scanner.skip_horizontal_whitespace(); + if scanner.peek() != Some('{') { + let Some(quote) = scanner.peek().filter(|ch| matches!(ch, '"' | '\'')) else { return Err(OpyError::at( "settings-invalid", "settings block must be a `settings { ... }` block or `settings \"file\"`", keyword_span, )); }; - scanner.advance(1); + scanner.advance_by(1); let mut path = String::new(); let mut escaped = false; loop { - let Some(ch) = scanner.peek(0) else { + let Some(ch) = scanner.peek() else { return Err(OpyError::at( "settings-invalid", "unterminated external settings path", keyword_span, )); }; - scanner.advance(1); + scanner.advance_by(1); if escaped { path.push(match ch { 'n' => '\n', @@ -182,11 +183,11 @@ fn match_block( path.push(ch); } } - scanner.skip_whitespace(); - if scanner.peek(0) == Some('#') { + scanner.skip_horizontal_whitespace(); + if scanner.peek() == Some('#') { scanner.skip_to_eol(); } - if !matches!(scanner.peek(0), None | Some('\n')) { + if !matches!(scanner.peek(), None | Some('\n')) { return Err(OpyError::at( "settings-invalid", "external settings declaration has unexpected trailing content", @@ -211,7 +212,7 @@ fn match_block( let mut text_start_offset = None; let mut text_start = None; loop { - let Some(ch) = scanner.peek(0) else { + let Some(ch) = scanner.peek() else { return Err(OpyError::at( "settings-invalid", "unterminated settings block (missing closing brace)".to_string(), @@ -226,7 +227,7 @@ fn match_block( } else if ch == quote { string_quote = None; } - scanner.advance(1); + scanner.advance_by(1); continue; } match ch { @@ -265,7 +266,7 @@ fn match_block( } _ => {} } - scanner.advance(1); + scanner.advance_by(1); } } @@ -290,9 +291,10 @@ pub fn sanitize_for_lex(text: &str, block: &SettingsBlock) -> String { /// in objects and arrays. Rejections (`settings-invalid`): duplicate keys, /// non-object root, missing `gamemodes` group, malformed values. pub fn parse_block(block: &SettingsBlock) -> OpyResult { - let mut parser = Jsonc { + let mut parser = Cursor { text: &block.text, pos: 0, + char_pos: 0, line: block.text_start.line, col: block.text_start.col, file: block.content_file, @@ -514,16 +516,21 @@ fn display_value(value: &crate::compile_time::Value) -> Result { _ => Err("settings list can only contain primitive values".to_string()), } } -struct Scanner<'a> { +struct Cursor<'a> { text: &'a str, pos: usize, char_pos: usize, line: u32, col: u32, + file: u32, } -impl Scanner<'_> { - fn peek(&self, ahead: usize) -> Option { +impl Cursor<'_> { + fn peek(&self) -> Option { + self.peek_at(0) + } + + fn peek_at(&self, ahead: usize) -> Option { self.text[self.pos..].chars().nth(ahead) } @@ -545,83 +552,60 @@ impl Scanner<'_> { Position::new(line, col) } - fn advance(&mut self, n: usize) { + fn advance(&mut self) -> Option { + let ch = self.peek()?; + if ch == '\n' { + self.line += 1; + self.col = 1; + } else { + self.col += 1; + } + self.pos += ch.len_utf8(); + self.char_pos += 1; + Some(ch) + } + + fn advance_by(&mut self, n: usize) { for _ in 0..n { - let Some(ch) = self.peek(0) else { + if self.advance().is_none() { return; - }; - if ch == '\n' { - self.line += 1; - self.col = 1; - } else { - self.col += 1; } - self.pos += ch.len_utf8(); - self.char_pos += 1; } } fn skip_to_eol(&mut self) { - while self.peek(0).is_some_and(|ch| ch != '\n') { - self.advance(1); + while self.peek().is_some_and(|ch| ch != '\n') { + self.advance_by(1); } } - fn skip_whitespace(&mut self) { - while matches!(self.peek(0), Some(' ' | '\t' | '\r')) { - self.advance(1); + fn skip_horizontal_whitespace(&mut self) { + while matches!(self.peek(), Some(' ' | '\t' | '\r')) { + self.advance_by(1); } } fn read_word(&mut self) -> String { let mut word = String::new(); - while let Some(ch) = self.peek(0) { + while let Some(ch) = self.peek() { if !is_ident_continue(ch) { break; } word.push(ch); - self.advance(1); + self.advance_by(1); } word } } -/// A JSONC parser over the block text. -struct Jsonc<'a> { - text: &'a str, - pos: usize, - line: u32, - col: u32, - file: u32, -} - +/// Primitive JSONC values before classification as typed settings nodes. enum ScalarValue { String(String), Bool(bool), Number(f64), } -impl Jsonc<'_> { - fn here(&self) -> Position { - Position::new(self.line, self.col) - } - - fn peek(&self) -> Option { - self.text[self.pos..].chars().next() - } - - fn advance(&mut self) -> Option { - let ch = self.peek()?; - self.pos += ch.len_utf8(); - if ch == '\n' { - self.line += 1; - self.col = 1; - } else { - self.col += 1; - } - Some(ch) - } - +impl Cursor<'_> { fn skip_whitespace(&mut self) { loop { while let Some(ch) = self.peek() { @@ -765,7 +749,7 @@ impl Jsonc<'_> { fn parse_value(&mut self) -> OpyResult<(cst::SettingsNode, Position)> { let start = self.here(); let ch = self.peek(); - let saved = (self.pos, self.line, self.col); + let saved = (self.pos, self.char_pos, self.line, self.col); let scalar = match ch { Some('"') | Some('\'') => { let value = self.parse_string_expression().ok_or_else(|| { @@ -793,8 +777,9 @@ impl Jsonc<'_> { self.skip_inline_whitespace(); if self.is_expression_continuation() { self.pos = saved.0; - self.line = saved.1; - self.col = saved.2; + self.char_pos = saved.1; + self.line = saved.2; + self.col = saved.3; cst::SettingsNode::Raw { name: String::new(), value: self.parse_expression_value(), @@ -1028,12 +1013,13 @@ impl Jsonc<'_> { fn parse_string_expression(&mut self) -> Option { let mut value = self.parse_string_value()?; loop { - let saved = (self.pos, self.line, self.col); + let saved = (self.pos, self.char_pos, self.line, self.col); self.skip_whitespace(); if self.peek() != Some('"') && self.peek() != Some('\'') { self.pos = saved.0; - self.line = saved.1; - self.col = saved.2; + self.char_pos = saved.1; + self.line = saved.2; + self.col = saved.3; break; } let next = self.parse_string_value()?; From aa04f4e1bba5cb4dddb5c23b39385fcac0101113 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 01:15:29 +0800 Subject: [PATCH 13/67] refactor(opy-rs): remove dead debug count branch --- crates/opy-rs/src/compiler/mod.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/opy-rs/src/compiler/mod.rs b/crates/opy-rs/src/compiler/mod.rs index 7736bea5..a141ff32 100644 --- a/crates/opy-rs/src/compiler/mod.rs +++ b/crates/opy-rs/src/compiler/mod.rs @@ -162,12 +162,7 @@ fn emit_debug_element_counts( (report.rules.get(rule_index), rule_counts.get(rule_index)) { if *rule_count > 1 { - let suffix = if *rule_count == 1 { - "element" - } else { - "elements" - }; - annotated.push_str(&format!("//{} {suffix}\n", rule_count)); + annotated.push_str(&format!("//{rule_count} elements\n")); } conditions.clear(); collect_element_nodes( From 3eba1fd3423f5d414e5ae3bfa16eb5b02211b896 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 01:19:03 +0800 Subject: [PATCH 14/67] refactor(opy-rs): move reserved index set --- crates/opy-rs/src/compiler/lowering.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/opy-rs/src/compiler/lowering.rs b/crates/opy-rs/src/compiler/lowering.rs index da77d649..a14793d4 100644 --- a/crates/opy-rs/src/compiler/lowering.rs +++ b/crates/opy-rs/src/compiler/lowering.rs @@ -557,7 +557,7 @@ impl<'a> Lowering<'a> { } })); let translation_helper_index = self.translation_helper_index(&helper_reserved)?; - let mut global_reserved = implicit_reserved.clone(); + let mut global_reserved = implicit_reserved; if let Some(index) = translation_helper_index { helper_reserved.insert(index); global_reserved.insert(index); From 812fae6e9c21a3b7464d808531553e4edb915b78 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 01:28:05 +0800 Subject: [PATCH 15/67] refactor(opy-rs): centralize lowerer initialization --- crates/opy-rs/src/lower.rs | 70 +++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/crates/opy-rs/src/lower.rs b/crates/opy-rs/src/lower.rs index f248ea3a..e9472558 100644 --- a/crates/opy-rs/src/lower.rs +++ b/crates/opy-rs/src/lower.rs @@ -82,6 +82,29 @@ struct Lowerer { errors: Vec, } +impl Lowerer { + fn new(manifest: &'static Manifest, catalog: Catalog) -> Self { + Self { + global_declarations: HashMap::new(), + player_declarations: HashMap::new(), + subroutine_declarations: HashMap::new(), + subroutine_definitions: Vec::new(), + constant_declarations: HashMap::new(), + macro_declarations: HashMap::new(), + enums: HashMap::new(), + enum_declarations: HashMap::new(), + locals: Vec::new(), + current_order: 0, + allow_dict_literal: false, + manifest, + catalog, + texture_used: false, + setup_tags: false, + errors: Vec::new(), + } + } +} + mod declarations; mod expressions; pub(crate) mod policy; @@ -122,27 +145,11 @@ pub fn lower_with_preprocessing( )); } }; - let mut lowerer = Lowerer { - global_declarations: HashMap::new(), - player_declarations: HashMap::new(), - subroutine_declarations: HashMap::new(), - subroutine_definitions: Vec::new(), - constant_declarations: HashMap::new(), - macro_declarations: HashMap::new(), - enums: HashMap::new(), - enum_declarations: HashMap::new(), - locals: Vec::new(), - current_order: 0, - allow_dict_literal: false, - manifest, - catalog, - texture_used: false, - setup_tags: preprocessing - .directives - .iter() - .any(|directive| matches!(directive.name.as_str(), "setupTags" | "setupTx")), - errors: Vec::new(), - }; + let mut lowerer = Lowerer::new(manifest, catalog); + lowerer.setup_tags = preprocessing + .directives + .iter() + .any(|directive| matches!(directive.name.as_str(), "setupTags" | "setupTx")); lowerer.collect_symbols(program); let mut declarations = Vec::new(); @@ -452,24 +459,9 @@ pub(crate) fn lower_settings_expression( format!("cannot load the Workshop catalog: {error}"), ) })?; - let mut lowerer = Lowerer { - global_declarations: HashMap::new(), - player_declarations: HashMap::new(), - subroutine_declarations: HashMap::new(), - subroutine_definitions: Vec::new(), - constant_declarations: HashMap::new(), - macro_declarations: HashMap::new(), - enums: HashMap::new(), - enum_declarations: HashMap::new(), - locals: Vec::new(), - current_order: program.top_level.len(), - allow_dict_literal: true, - manifest, - catalog, - texture_used: false, - setup_tags: false, - errors: Vec::new(), - }; + let mut lowerer = Lowerer::new(manifest, catalog); + lowerer.current_order = program.top_level.len(); + lowerer.allow_dict_literal = true; lowerer.collect_symbols(program); let lowered = lowerer.lower_expr(&expression, &[], CallPosition::Value); lowerer.errors.into_iter().next().map_or(Ok(lowered), Err) From d3e30671b881bca57b68db19df1beaf48b204d57 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 01:31:16 +0800 Subject: [PATCH 16/67] refactor(opy-rs): centralize parser initialization --- crates/opy-rs/src/parser.rs | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/crates/opy-rs/src/parser.rs b/crates/opy-rs/src/parser.rs index 1fb9a254..f9ea2e05 100644 --- a/crates/opy-rs/src/parser.rs +++ b/crates/opy-rs/src/parser.rs @@ -30,15 +30,7 @@ pub fn parse(tokens: &[Token]) -> ParseOutput { /// Parse with the global redeclaration policy observed by the pinned oracle. pub fn parse_with_options(tokens: &[Token], allow_macro_redeclaration: bool) -> ParseOutput { - let mut parser = Parser { - tokens, - pos: 0, - errors: Vec::new(), - allow_macro_redeclaration, - last_statement_continued: false, - last_colon_body_continued: false, - open_if_indents: Vec::new(), - }; + let mut parser = Parser::new(tokens, allow_macro_redeclaration); let program = parser.parse_program(); if parser.errors.is_empty() { ParseOutput { @@ -69,6 +61,20 @@ struct Parser<'a> { open_if_indents: Vec, } +impl<'a> Parser<'a> { + fn new(tokens: &'a [Token], allow_macro_redeclaration: bool) -> Self { + Self { + tokens, + pos: 0, + errors: Vec::new(), + allow_macro_redeclaration, + last_statement_continued: false, + last_colon_body_continued: false, + open_if_indents: Vec::new(), + } + } +} + fn is_identifier(text: &str) -> bool { !text.is_empty() && text.chars().enumerate().all(|(index, ch)| { @@ -338,15 +344,7 @@ pub(crate) fn parse_expression_fragment( for token in &mut tokens { token.span = shift_span(token.span, origin); } - let mut parser = Parser { - tokens: &tokens, - pos: 0, - allow_macro_redeclaration: false, - errors: Vec::new(), - last_statement_continued: false, - last_colon_body_continued: false, - open_if_indents: Vec::new(), - }; + let mut parser = Parser::new(&tokens, false); let expression = parser.parse_expr().map_err(|()| { parser.errors.first().cloned().unwrap_or_else(|| { OpyError::at( From 7e17575b31ee4f4114e3d3259fc5d36ef4aa8a1d Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 01:35:30 +0800 Subject: [PATCH 17/67] refactor(opy-rs): centralize settings cursor creation --- crates/opy-rs/src/settings.rs | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/crates/opy-rs/src/settings.rs b/crates/opy-rs/src/settings.rs index 65878294..e6fc8692 100644 --- a/crates/opy-rs/src/settings.rs +++ b/crates/opy-rs/src/settings.rs @@ -52,14 +52,7 @@ pub struct SettingsBlock { /// matching respects `"`/`'` strings, `\` escapes, and nesting; an /// unterminated block is `settings-invalid`. pub fn find_blocks(text: &str, file_id: u32) -> OpyResult> { - let mut scanner = Cursor { - text, - pos: 0, - char_pos: 0, - line: 1, - col: 1, - file: file_id, - }; + let mut scanner = Cursor::new(text, file_id, Position::new(1, 1)); let mut blocks = Vec::new(); let mut in_block_comment = false; let mut string_quote = None; @@ -291,14 +284,7 @@ pub fn sanitize_for_lex(text: &str, block: &SettingsBlock) -> String { /// in objects and arrays. Rejections (`settings-invalid`): duplicate keys, /// non-object root, missing `gamemodes` group, malformed values. pub fn parse_block(block: &SettingsBlock) -> OpyResult { - let mut parser = Cursor { - text: &block.text, - pos: 0, - char_pos: 0, - line: block.text_start.line, - col: block.text_start.col, - file: block.content_file, - }; + let mut parser = Cursor::new(&block.text, block.content_file, block.text_start); parser.skip_whitespace(); let children = if block.external_path.is_some() { let (children, _) = parser.parse_object()?; @@ -526,6 +512,17 @@ struct Cursor<'a> { } impl Cursor<'_> { + fn new(text: &str, file: u32, origin: Position) -> Cursor<'_> { + Cursor { + text, + pos: 0, + char_pos: 0, + line: origin.line, + col: origin.col, + file, + } + } + fn peek(&self) -> Option { self.peek_at(0) } From 7740e80fbd11fa66c7aa8a222551d88e0f6cd61f Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 01:35:35 +0800 Subject: [PATCH 18/67] refactor(opy-rs): centralize check failure state --- crates/opy-rs/src/tooling.rs | 44 ++++++++++++------------------------ 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/crates/opy-rs/src/tooling.rs b/crates/opy-rs/src/tooling.rs index a7c1c843..91f67624 100644 --- a/crates/opy-rs/src/tooling.rs +++ b/crates/opy-rs/src/tooling.rs @@ -73,6 +73,15 @@ pub struct CheckOutcome { } impl CheckOutcome { + fn failure(diagnostics: Vec, files: Vec) -> Self { + Self { + diagnostics, + model: None, + files, + post_compile_hook: None, + } + } + /// Whether the project checked clean. pub fn is_clean(&self) -> bool { self.diagnostics @@ -109,12 +118,7 @@ pub fn check_with_overlay( .map(|warning| Diagnostic::from_warning(warning, &files)) .collect::>(); diagnostics.push(Diagnostic::from_error(error, &files)); - return CheckOutcome { - diagnostics, - model: None, - files, - post_compile_hook: None, - }; + return CheckOutcome::failure(diagnostics, files); } }; let parsed = crate::parser::parse_with_options( @@ -133,12 +137,7 @@ pub fn check_with_overlay( .iter() .map(|error| Diagnostic::from_error(error.clone(), &files)), ); - return CheckOutcome { - diagnostics, - model: None, - files, - post_compile_hook: None, - }; + return CheckOutcome::failure(diagnostics, files); }; // Parse the extracted settings block into the CST; expression values are // resolved after ordinary CST-to-HIR lowering so they use the shared OPY @@ -151,12 +150,7 @@ pub fn check_with_overlay( .map(|warning| Diagnostic::from_warning(warning, &files)) .collect::>(); diagnostics.push(Diagnostic::from_error(error, &files)); - return CheckOutcome { - diagnostics, - model: None, - files, - post_compile_hook: None, - }; + return CheckOutcome::failure(diagnostics, files); } } } @@ -191,12 +185,7 @@ pub fn check_with_overlay( if let Err(error) = crate::settings::resolve_hir_settings(&mut hir, &program) { let mut diagnostics = frontend_warnings; diagnostics.push(Diagnostic::from_error(error, &files)); - return CheckOutcome { - diagnostics, - model: None, - files, - post_compile_hook: None, - }; + return CheckOutcome::failure(diagnostics, files); } CheckOutcome { diagnostics: frontend_warnings, @@ -214,12 +203,7 @@ pub fn check_with_overlay( .map(|warning| Diagnostic::from_warning(warning, &files)) .collect::>(); diagnostics.push(Diagnostic::from_error(error, &files)); - CheckOutcome { - diagnostics, - model: None, - files, - post_compile_hook: None, - } + CheckOutcome::failure(diagnostics, files) } } } From 89424e885fb9db3b2819c4b649db3454cdb00ea9 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 01:40:20 +0800 Subject: [PATCH 19/67] refactor(opy-rs): centralize preprocessing error state --- crates/opy-rs/src/preprocess.rs | 148 ++++++++++---------------------- 1 file changed, 44 insertions(+), 104 deletions(-) diff --git a/crates/opy-rs/src/preprocess.rs b/crates/opy-rs/src/preprocess.rs index 45f71609..017065cf 100644 --- a/crates/opy-rs/src/preprocess.rs +++ b/crates/opy-rs/src/preprocess.rs @@ -156,6 +156,16 @@ pub struct PreprocessOutcome { pub warnings: Vec, } +impl PreprocessOutcome { + fn failure(error: OpyError, pre: Preprocessor) -> Self { + Self { + result: Err(error), + files: pre.files, + warnings: pre.warnings, + } + } +} + /// Preprocess with open-document overlays while retaining the file registry /// registered so far on failure. pub fn preprocess_with_overlay_outcome( @@ -199,15 +209,14 @@ pub fn preprocess_with_overlay_outcome( crate::diag::Position::new(1, 1), crate::diag::Position::new(1, first_line.chars().count() as u32 + 1), ); - return PreprocessOutcome { - result: Err(OpyError::at( + return PreprocessOutcome::failure( + OpyError::at( "main-file-invalid", "`#!mainFile` expects one quoted path on the first line", span, - )), - files: pre.files, - warnings: pre.warnings, - }; + ), + pre, + ); } if let Some((main_file, span)) = first_main_file_directive(main_text) { let candidate = pre.root.join(&main_file); @@ -230,28 +239,26 @@ pub fn preprocess_with_overlay_outcome( } None => { let Some(canonical) = canonical else { - return PreprocessOutcome { - result: Err(OpyError::at( + return PreprocessOutcome::failure( + OpyError::at( "main-file-not-found", format!("cannot find main file '{main_file}'"), span, - )), - files: pre.files, - warnings: pre.warnings, - }; + ), + pre, + ); }; let text = match std::fs::read_to_string(&canonical) { Ok(text) => text, Err(error) => { - return PreprocessOutcome { - result: Err(OpyError::at( + return PreprocessOutcome::failure( + OpyError::at( "main-file-not-found", format!("cannot read main file '{main_file}': {error}"), span, - )), - files: pre.files, - warnings: pre.warnings, - }; + ), + pre, + ); } }; let new_root = canonical @@ -287,24 +294,12 @@ pub fn preprocess_with_overlay_outcome( // the owning file's lexed text, so the lexer never sees its braces (#86). let settings = match crate::settings::find_blocks(source_text, source_file_id) { Ok(mut blocks) => blocks.pop(), - Err(error) => { - return PreprocessOutcome { - result: Err(error), - files: pre.files, - warnings: pre.warnings, - }; - } + Err(error) => return PreprocessOutcome::failure(error, pre), }; let settings = match settings { Some(block) => match pre.resolve_settings_source(block) { Ok(block) => Some(block), - Err(error) => { - return PreprocessOutcome { - result: Err(error), - files: pre.files, - warnings: pre.warnings, - }; - } + Err(error) => return PreprocessOutcome::failure(error, pre), }, None => None, }; @@ -323,27 +318,13 @@ pub fn preprocess_with_overlay_outcome( }; let mut tokens = match tokens { Ok(tokens) => tokens, - Err(error) => { - return PreprocessOutcome { - result: Err(error), - files: pre.files, - warnings: pre.warnings, - }; - } + Err(error) => return PreprocessOutcome::failure(error, pre), }; if let Err(error) = pre.process_directives(&mut tokens, false, settings) { - return PreprocessOutcome { - result: Err(error), - files: pre.files, - warnings: pre.warnings, - }; + return PreprocessOutcome::failure(error, pre); } if let Err(error) = pre.load_translation_catalog() { - return PreprocessOutcome { - result: Err(error), - files: pre.files, - warnings: pre.warnings, - }; + return PreprocessOutcome::failure(error, pre); } let result = Ok(( Preprocessed { @@ -559,6 +540,11 @@ impl Preprocessor { } fn parse_po(text: &str, language: &str, span: Option) -> OpyResult> { + let span = span.unwrap_or(Span::new( + 0, + crate::diag::Position::new(1, 1), + crate::diag::Position::new(1, 1), + )); let mut header_language = None; let mut entries = Vec::new(); let mut current: Option<(Option, String, String)> = None; @@ -588,26 +574,14 @@ fn parse_po(text: &str, language: &str, span: Option) -> OpyResult) -> OpyResult) -> OpyResult(value).map_err(|error| { OpyError::at( "translations-invalid", format!("malformed PO string: {error}"), - span.unwrap_or_else(|| { - Span::new( - 0, - crate::diag::Position::new(1, 1), - crate::diag::Position::new(1, 1), - ) - }), + span, ) })?; match name { @@ -692,13 +644,7 @@ fn parse_po(text: &str, language: &str, span: Option) -> OpyResult) -> OpyResult Date: Mon, 28 Sep 2026 02:36:50 +0800 Subject: [PATCH 20/67] refactor(opy-rs): consolidate parser and lowering paths --- crates/opy-cli/src/main.rs | 14 +- crates/opy-cli/src/present.rs | 11 ++ crates/opy-rs/src/compiler/lowering.rs | 121 +++++++++-------- .../src/compiler/lowering/action_calls.rs | 22 +-- .../src/compiler/lowering/assignments.rs | 32 ++--- crates/opy-rs/src/compiler/lowering/rules.rs | 128 ++++++++---------- crates/opy-rs/src/compiler/lowering/values.rs | 8 +- crates/opy-rs/src/compiler/mod.rs | 71 +++++----- crates/opy-rs/src/compiler/reconstruct.rs | 61 +++++---- crates/opy-rs/src/hir/dump.rs | 110 +++++++-------- crates/opy-rs/src/lexer.rs | 124 +++++------------ crates/opy-rs/src/manifest/mod.rs | 19 ++- crates/opy-rs/src/parser.rs | 79 +++++------ crates/opy-rs/src/parser/definitions.rs | 49 ++----- crates/opy-rs/src/parser/expressions.rs | 114 +++++----------- crates/opy-rs/src/settings.rs | 44 +++--- 16 files changed, 419 insertions(+), 588 deletions(-) diff --git a/crates/opy-cli/src/main.rs b/crates/opy-cli/src/main.rs index d2fa5fd1..595c7994 100644 --- a/crates/opy-cli/src/main.rs +++ b/crates/opy-cli/src/main.rs @@ -20,9 +20,7 @@ use opy_rs::{FilesystemProject, LANGUAGE_NAME, LANGUAGE_VERSION}; use serde::Serialize; use crate::cli::{CheckArgs, Cli, Command, CompileArgs, FileArgs, OutputFormatArg}; -use crate::present::{ - CheckView, DiagnosticSeverity, DiagnosticView, PositionView, Presentation, SpanView, -}; +use crate::present::{CheckView, DiagnosticView, PositionView, Presentation, SpanView}; fn main() -> ExitCode { let cli = match Cli::try_parse() { @@ -234,10 +232,7 @@ fn check_view(outcome: &CheckOutcome) -> CheckView { fn diagnostic_view(diagnostic: &OpyDiagnostic) -> DiagnosticView { DiagnosticView { - severity: match diagnostic.severity { - opy_rs::tooling::DiagnosticSeverity::Error => DiagnosticSeverity::Error, - opy_rs::tooling::DiagnosticSeverity::Warning => DiagnosticSeverity::Warning, - }, + severity: diagnostic.severity.into(), code: diagnostic.code.clone(), message: diagnostic.message.clone(), span: diagnostic.span.as_ref().map(diagnostic_span_view), @@ -246,10 +241,7 @@ fn diagnostic_view(diagnostic: &OpyDiagnostic) -> DiagnosticView { fn compile_diagnostic_view(diagnostic: &CompileDiagnostic) -> DiagnosticView { DiagnosticView { - severity: match diagnostic.severity { - opy_rs::tooling::DiagnosticSeverity::Error => DiagnosticSeverity::Error, - opy_rs::tooling::DiagnosticSeverity::Warning => DiagnosticSeverity::Warning, - }, + severity: diagnostic.severity.into(), code: diagnostic.code.clone(), message: diagnostic.message.clone(), span: diagnostic.span.as_ref().map(diagnostic_span_view), diff --git a/crates/opy-cli/src/present.rs b/crates/opy-cli/src/present.rs index 42468441..0fc52492 100644 --- a/crates/opy-cli/src/present.rs +++ b/crates/opy-cli/src/present.rs @@ -1,5 +1,7 @@ use std::io::{IsTerminal, Write}; +use opy_rs::tooling::DiagnosticSeverity as OpyDiagnosticSeverity; + use crate::cli::{ColorArg, RendererArg}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -8,6 +10,15 @@ pub(crate) enum DiagnosticSeverity { Warning, } +impl From for DiagnosticSeverity { + fn from(value: OpyDiagnosticSeverity) -> Self { + match value { + OpyDiagnosticSeverity::Error => Self::Error, + OpyDiagnosticSeverity::Warning => Self::Warning, + } + } +} + impl DiagnosticSeverity { fn as_str(self) -> &'static str { match self { diff --git a/crates/opy-rs/src/compiler/lowering.rs b/crates/opy-rs/src/compiler/lowering.rs index a14793d4..540bac72 100644 --- a/crates/opy-rs/src/compiler/lowering.rs +++ b/crates/opy-rs/src/compiler/lowering.rs @@ -241,43 +241,32 @@ type LoweredSwitchBody = (Vec, Option); type LoweredSwitchArm<'a> = (Option<&'a Expr>, Vec, Option); fn pure_continue_conditions(statement: &Stmt) -> Option> { - match statement { - Stmt::Continue { .. } => Some(Vec::new()), - Stmt::If { - branches, - r#else: None, - .. - } if branches.len() == 1 && branches[0].body.len() == 1 => { - let mut conditions = pure_continue_conditions(&branches[0].body[0])?; - conditions.insert(0, &branches[0].condition); - Some(conditions) - } - _ => None, + if matches!(statement, Stmt::Continue { .. }) { + return Some(Vec::new()); } + let (condition, body, _) = single_if_statement(statement)?; + let mut conditions = pure_continue_conditions(body)?; + conditions.insert(0, condition); + Some(conditions) } fn pure_goto_conditions(statement: &Stmt) -> Option<(Vec<&Expr>, &str)> { - match statement { - Stmt::Goto { - label: Some(label), - offset: None, - rule_start: false, - .. - } => Some((Vec::new(), label.as_str())), - Stmt::If { - branches, - r#else: None, - .. - } if branches.len() == 1 && branches[0].body.len() == 1 => { - let (mut conditions, label) = pure_goto_conditions(&branches[0].body[0])?; - conditions.insert(0, &branches[0].condition); - Some((conditions, label)) - } - _ => None, + if let Stmt::Goto { + label: Some(label), + offset: None, + rule_start: false, + .. + } = statement + { + return Some((Vec::new(), label.as_str())); } + let (condition, body, _) = single_if_statement(statement)?; + let (mut conditions, label) = pure_goto_conditions(body)?; + conditions.insert(0, condition); + Some((conditions, label)) } -fn direct_conditional_goto(statement: &Stmt) -> Option<(&Expr, &str, Option)> { +fn single_if_statement(statement: &Stmt) -> Option<(&Expr, &Stmt, Option)> { let Stmt::If { branches, r#else: None, @@ -289,44 +278,38 @@ fn direct_conditional_goto(statement: &Stmt) -> Option<(&Expr, &str, Option Option<(&Expr, &Expr, Option)> { - let Stmt::If { - branches, - r#else: None, - span, - } = statement +fn direct_conditional_goto(statement: &Stmt) -> Option<(&Expr, &str, Option)> { + let (condition, body, span) = single_if_statement(statement)?; + let Stmt::Goto { + label: Some(label), + offset: None, + rule_start: false, + .. + } = body else { return None; }; - let [branch] = branches.as_slice() else { - return None; - }; - let [ - Stmt::Goto { - label: None, - offset: Some(offset), - rule_start: false, - .. - }, - ] = branch.body.as_slice() + Some((condition, label.as_str(), span)) +} + +fn direct_conditional_dynamic_goto(statement: &Stmt) -> Option<(&Expr, &Expr, Option)> { + let (condition, body, span) = single_if_statement(statement)?; + let Stmt::Goto { + label: None, + offset: Some(offset), + rule_start: false, + .. + } = body else { return None; }; - Some((&branch.condition, offset, *span)) + Some((condition, offset, span)) } fn contains_loop_continue(statement: &Stmt) -> bool { @@ -1792,6 +1775,28 @@ impl<'a> Lowering<'a> { fn unsupported(&self, message: impl Into, span: Option) -> IntegrationError { IntegrationError::new("unsupported-integration-surface", message, span) } + + fn global_variable_id( + &self, + name: &str, + span: Option, + ) -> Result { + self.globals + .get(name) + .copied() + .ok_or_else(|| self.unsupported(format!("unknown global variable '{name}'"), span)) + } + + fn player_variable_id( + &self, + name: &str, + span: Option, + ) -> Result { + self.players + .get(name) + .copied() + .ok_or_else(|| self.unsupported(format!("unknown player variable '{name}'"), span)) + } } /// Collect the pinned OverPy implicit default global and player variables. diff --git a/crates/opy-rs/src/compiler/lowering/action_calls.rs b/crates/opy-rs/src/compiler/lowering/action_calls.rs index 24dc6043..360d82f6 100644 --- a/crates/opy-rs/src/compiler/lowering/action_calls.rs +++ b/crates/opy-rs/src/compiler/lowering/action_calls.rs @@ -383,9 +383,7 @@ impl<'a> Lowering<'a> { name, span: target_span, } => { - let variable = *self.globals.get(name).ok_or_else(|| { - self.unsupported(format!("unknown global variable '{name}'"), *target_span) - })?; + let variable = self.global_variable_id(name, *target_span)?; let action = self.push_action(Action::ModifyGlobalVariable { variable: self.global_names[variable].clone(), op, @@ -400,9 +398,7 @@ impl<'a> Lowering<'a> { span: target_span, .. } => { - let variable = *self.players.get(name).ok_or_else(|| { - self.unsupported(format!("unknown player variable '{name}'"), *target_span) - })?; + let variable = self.player_variable_id(name, *target_span)?; let player_span = player.span().copied(); let player = self.lower_value(player)?; let action = self.push_action(Action::ModifyPlayerVariable { @@ -429,12 +425,7 @@ impl<'a> Lowering<'a> { name, span: array_span, } => { - let variable = *self.globals.get(name).ok_or_else(|| { - self.unsupported( - format!("unknown global variable '{name}'"), - *array_span, - ) - })?; + let variable = self.global_variable_id(name, *array_span)?; let variable = self.push_value(Value::GlobalVariable( self.global_names[variable].clone(), )); @@ -456,12 +447,7 @@ impl<'a> Lowering<'a> { span: array_span, .. } => { - let variable = *self.players.get(name).ok_or_else(|| { - self.unsupported( - format!("unknown player variable '{name}'"), - *array_span, - ) - })?; + let variable = self.player_variable_id(name, *array_span)?; let player = self.lower_value(player)?; let variable = self.push_value(Value::PlayerVariable { player, diff --git a/crates/opy-rs/src/compiler/lowering/assignments.rs b/crates/opy-rs/src/compiler/lowering/assignments.rs index 0b2f7ffb..88819edc 100644 --- a/crates/opy-rs/src/compiler/lowering/assignments.rs +++ b/crates/opy-rs/src/compiler/lowering/assignments.rs @@ -28,9 +28,7 @@ impl<'a> Lowering<'a> { name, span: target_span, } => { - let variable = *self.globals.get(name).ok_or_else(|| { - self.unsupported(format!("unknown global variable '{name}'"), *target_span) - })?; + let variable = self.global_variable_id(name, *target_span)?; let root_value = self.push_value(Value::GlobalVariable(self.global_names[variable].clone())); (root_value, "modifyGlobalVariableAtIndex") @@ -41,9 +39,7 @@ impl<'a> Lowering<'a> { span: target_span, .. } => { - let variable = *self.players.get(name).ok_or_else(|| { - self.unsupported(format!("unknown player variable '{name}'"), *target_span) - })?; + let variable = self.player_variable_id(name, *target_span)?; let player = self.lower_value(player)?; let value = self.push_value(Value::PlayerVariable { player, @@ -160,9 +156,7 @@ impl<'a> Lowering<'a> { name, span: target_span, } => { - let variable = *self.globals.get(name).ok_or_else(|| { - self.unsupported(format!("unknown global variable '{name}'"), *target_span) - })?; + let variable = self.global_variable_id(name, *target_span)?; if let Expr::Binary { op, left, right, .. } = value @@ -188,9 +182,7 @@ impl<'a> Lowering<'a> { span: target_span, .. } => { - let variable = *self.players.get(name).ok_or_else(|| { - self.unsupported(format!("unknown player variable '{name}'"), *target_span) - })?; + let variable = self.player_variable_id(name, *target_span)?; let player_val = self.lower_value(player)?; if let Expr::Binary { op, left, right, .. @@ -222,9 +214,7 @@ impl<'a> Lowering<'a> { name, span: arr_span, } => { - let variable = *self.globals.get(name).ok_or_else(|| { - self.unsupported(format!("unknown global variable '{name}'"), *arr_span) - })?; + let variable = self.global_variable_id(name, *arr_span)?; let var_node = self.push_value(Value::GlobalVariable( self.global_names[variable].clone(), )); @@ -244,9 +234,7 @@ impl<'a> Lowering<'a> { .. } => { let player_val = self.lower_value(player)?; - let variable = *self.players.get(name).ok_or_else(|| { - self.unsupported(format!("unknown player variable '{name}'"), *arr_span) - })?; + let variable = self.player_variable_id(name, *arr_span)?; let var_node = self.push_value(Value::PlayerVariable { player: player_val, variable: self.player_names[variable].clone(), @@ -323,9 +311,7 @@ impl<'a> Lowering<'a> { name, span: target_span, } => { - let variable = *self.globals.get(name).ok_or_else(|| { - self.unsupported(format!("unknown global variable '{name}'"), *target_span) - })?; + let variable = self.global_variable_id(name, *target_span)?; let root_value = self.push_value(Value::GlobalVariable(self.global_names[variable].clone())); ("setGlobalVariableAtIndex", root_value) @@ -337,9 +323,7 @@ impl<'a> Lowering<'a> { .. } => { let player_value = self.lower_value(player)?; - let variable = *self.players.get(name).ok_or_else(|| { - self.unsupported(format!("unknown player variable '{name}'"), *target_span) - })?; + let variable = self.player_variable_id(name, *target_span)?; let root_value = self.push_value(Value::PlayerVariable { player: player_value, variable: self.player_names[variable].clone(), diff --git a/crates/opy-rs/src/compiler/lowering/rules.rs b/crates/opy-rs/src/compiler/lowering/rules.rs index e0dec0f3..c126c6c0 100644 --- a/crates/opy-rs/src/compiler/lowering/rules.rs +++ b/crates/opy-rs/src/compiler/lowering/rules.rs @@ -540,18 +540,7 @@ impl<'a> Lowering<'a> { } } } - if let Some((condition, label, span)) = direct_conditional_goto(statement) - && self - .visible_labels - .iter() - .any(|labels| labels.iter().any(|candidate| candidate == label)) - { - let condition = self.lower_value(condition)?; - let placeholder = self.push_number(0.0); - let skip = self.push_call_action("skipIf", &[condition, placeholder]); - self.mark_action_origins(std::slice::from_ref(&skip), span); - actions.push(skip); - self.deferred_gotos.push((skip, label.to_string(), span, 1)); + if self.lower_conditional_label_jump(statement, &mut actions)? { index += 1; continue; } @@ -798,12 +787,7 @@ impl<'a> Lowering<'a> { name, span: target_span, } => { - let variable_id = *self.globals.get(name).ok_or_else(|| { - self.unsupported( - format!("unknown global variable '{name}'"), - *target_span, - ) - })?; + let variable_id = self.global_variable_id(name, *target_span)?; Ok(self.push_for_global_actions(variable_id, start, stop, step, body)) } Expr::PlayerVar { @@ -812,12 +796,7 @@ impl<'a> Lowering<'a> { span: target_span, .. } => { - let variable_id = *self.players.get(name).ok_or_else(|| { - self.unsupported( - format!("unknown player variable '{name}'"), - *target_span, - ) - })?; + let variable_id = self.player_variable_id(name, *target_span)?; let player = self.lower_value(player)?; Ok(self.push_for_player_actions( player, variable_id, start, stop, step, body, @@ -1124,6 +1103,30 @@ impl<'a> Lowering<'a> { } } + fn lower_conditional_label_jump( + &mut self, + statement: &Stmt, + actions: &mut Vec, + ) -> Result { + let Some((condition, label, span)) = direct_conditional_goto(statement) else { + return Ok(false); + }; + if !self + .visible_labels + .iter() + .any(|labels| labels.iter().any(|candidate| candidate == label)) + { + return Ok(false); + } + let condition = self.lower_value(condition)?; + let placeholder = self.push_number(0.0); + let skip = self.push_call_action("skipIf", &[condition, placeholder]); + self.mark_action_origins(std::slice::from_ref(&skip), span); + actions.push(skip); + self.deferred_gotos.push((skip, label.to_string(), span, 1)); + Ok(true) + } + fn lower_loop_body(&mut self, statements: &[Stmt]) -> Result, IntegrationError> { self.lower_loop_sequence_with_break_target(statements, &[], 0, BreakTarget::Loop) } @@ -1136,6 +1139,29 @@ impl<'a> Lowering<'a> { Ok(self.combine_conditions(values)) } + fn lower_skip_with_distance( + &mut self, + conditions: &[&Expr], + distance: usize, + span: Option, + ) -> Result { + let mut args = Vec::with_capacity(conditions.len() + 1); + if let Some(condition) = self.lower_condition_chain(conditions)? { + args.push(condition); + } + args.push(self.push_number(distance as f64)); + let skip = self.push_call_action( + if conditions.is_empty() { + "skip" + } else { + "skipIf" + }, + &args, + ); + self.mark_action_origins(std::slice::from_ref(&skip), span); + Ok(skip) + } + fn lower_loop_sequence_with_break_target( &mut self, statements: &[Stmt], @@ -1159,24 +1185,11 @@ impl<'a> Lowering<'a> { + structural_after + self.canonical_action_width(after, statement.span().copied())?; if distance > 0 { - let mut args = Vec::with_capacity(conditions.len() + 1); - if let Some(condition) = self.lower_condition_chain(&conditions)? { - args.push(condition); - } - let distance = self.push_number(distance as f64); - args.push(distance); - let skip = self.push_call_action( - if conditions.is_empty() { - "skip" - } else { - "skipIf" - }, - &args, - ); - self.mark_action_origins( - std::slice::from_ref(&skip), + let skip = self.lower_skip_with_distance( + &conditions, + distance, statement.span().copied(), - ); + )?; actions.push(skip); } actions.extend(tail); @@ -1235,41 +1248,18 @@ impl<'a> Lowering<'a> { )?; let distance = self.canonical_action_width(&middle, statement.span().copied())?; - let mut args = Vec::with_capacity(conditions.len() + 1); - if let Some(condition) = self.lower_condition_chain(&conditions)? { - args.push(condition); - } - args.push(self.push_number(distance as f64)); - let skip = self.push_call_action( - if conditions.is_empty() { - "skip" - } else { - "skipIf" - }, - &args, - ); - self.mark_action_origins( - std::slice::from_ref(&skip), + let skip = self.lower_skip_with_distance( + &conditions, + distance, statement.span().copied(), - ); + )?; actions.push(skip); actions.extend(middle); actions.extend(suffix); return Ok(actions); } } - if let Some((condition, label, span)) = direct_conditional_goto(statement) - && self - .visible_labels - .iter() - .any(|labels| labels.iter().any(|candidate| candidate == label)) - { - let condition = self.lower_value(condition)?; - let placeholder = self.push_number(0.0); - let skip = self.push_call_action("skipIf", &[condition, placeholder]); - self.mark_action_origins(std::slice::from_ref(&skip), span); - actions.push(skip); - self.deferred_gotos.push((skip, label.to_string(), span, 1)); + if self.lower_conditional_label_jump(statement, &mut actions)? { index += 1; continue; } diff --git a/crates/opy-rs/src/compiler/lowering/values.rs b/crates/opy-rs/src/compiler/lowering/values.rs index 9692888f..caa9e755 100644 --- a/crates/opy-rs/src/compiler/lowering/values.rs +++ b/crates/opy-rs/src/compiler/lowering/values.rs @@ -82,16 +82,12 @@ impl<'a> Lowering<'a> { )); } Expr::GlobalVar { name, .. } => { - let id = *self.globals.get(name).ok_or_else(|| { - self.unsupported(format!("unknown global variable '{name}'"), span) - })?; + let id = self.global_variable_id(name, span)?; Value::GlobalVariable(self.global_names[id].clone()) } Expr::PlayerVar { player, name, .. } => { let player = self.lower_value(player)?; - let id = *self.players.get(name).ok_or_else(|| { - self.unsupported(format!("unknown player variable '{name}'"), span) - })?; + let id = self.player_variable_id(name, span)?; Value::PlayerVariable { player, variable: self.player_names[id].clone(), diff --git a/crates/opy-rs/src/compiler/mod.rs b/crates/opy-rs/src/compiler/mod.rs index a141ff32..b37a748b 100644 --- a/crates/opy-rs/src/compiler/mod.rs +++ b/crates/opy-rs/src/compiler/mod.rs @@ -711,14 +711,13 @@ impl Compiler { match self.mapped_text(&artifact, &hir, &locale.to_string()) { Ok(text) => Some(text), Err(error) => { - let mut diagnostics = frontend_diagnostics; - diagnostics.push(compile_diagnostic(error, &hir.files)); return ( - CompileReport::failure( + integration_failure_report( compiler, catalog, - CompileFailureClass::Integration, - diagnostics, + frontend_diagnostics, + error, + &hir.files, ), None, ); @@ -732,34 +731,28 @@ impl Compiler { CompileReport::success(compiler, catalog, artifact, frontend_diagnostics), mapped, ), - Err(error) => { - let mut diagnostics = frontend_diagnostics; - diagnostics.push(compile_diagnostic(error, &hir.files)); - ( - CompileReport::failure( - compiler, - catalog, - CompileFailureClass::Integration, - diagnostics, - ), - None, - ) - } - } - } - Err(error) => { - let mut diagnostics = frontend_diagnostics; - diagnostics.push(compile_diagnostic(error, &hir.files)); - ( - CompileReport::failure( - compiler, - catalog, - CompileFailureClass::Integration, - diagnostics, + Err(error) => ( + integration_failure_report( + compiler, + catalog, + frontend_diagnostics, + error, + &hir.files, + ), + None, ), - None, - ) + } } + Err(error) => ( + integration_failure_report( + compiler, + catalog, + frontend_diagnostics, + error, + &hir.files, + ), + None, + ), } } @@ -911,6 +904,22 @@ fn compile_diagnostic(error: IntegrationError, files: &[hir::SourceFile]) -> Com } } +fn integration_failure_report( + compiler: CompilerIdentity, + catalog: CatalogIdentity, + mut diagnostics: Vec, + error: IntegrationError, + files: &[hir::SourceFile], +) -> CompileReport { + diagnostics.push(compile_diagnostic(error, files)); + CompileReport::failure( + compiler, + catalog, + CompileFailureClass::Integration, + diagnostics, + ) +} + fn compile_frontend_diagnostic(diagnostic: &crate::tooling::Diagnostic) -> CompileDiagnostic { CompileDiagnostic { severity: diagnostic.severity, diff --git a/crates/opy-rs/src/compiler/reconstruct.rs b/crates/opy-rs/src/compiler/reconstruct.rs index a5b0e8ba..7fab5abe 100644 --- a/crates/opy-rs/src/compiler/reconstruct.rs +++ b/crates/opy-rs/src/compiler/reconstruct.rs @@ -690,19 +690,13 @@ impl<'a> Emitter<'a> { ); continue; }; - if let Some(previous_position) = previous { - if variable_position <= previous_position { - self.issue( - "unsupported-init-rule", - format!( - "initializer rule Sets '{variable}' out of global table order; \ - the frontend synthesizes initializers in declaration order" - ), - span, - ); - } - } - previous = Some(variable_position); + self.record_initializer_position( + &mut previous, + variable_position, + variable, + "global", + span, + ); initializers.insert( variable_position, ( @@ -754,19 +748,13 @@ impl<'a> Emitter<'a> { ); continue; }; - if let Some(previous_position) = previous { - if variable_position <= previous_position { - self.issue( - "unsupported-init-rule", - format!( - "initializer rule Sets '{variable}' out of player table order; \ - the frontend synthesizes initializers in declaration order" - ), - span, - ); - } - } - previous = Some(variable_position); + self.record_initializer_position( + &mut previous, + variable_position, + variable, + "player", + span, + ); initializers.insert( variable_position, ( @@ -779,6 +767,27 @@ impl<'a> Emitter<'a> { initializers } + fn record_initializer_position( + &mut self, + previous: &mut Option, + position: usize, + variable: &str, + table: &str, + span: Option, + ) { + if previous.is_some_and(|previous| position <= previous) { + self.issue( + "unsupported-init-rule", + format!( + "initializer rule Sets '{variable}' out of {table} table order; \ + the frontend synthesizes initializers in declaration order" + ), + span, + ); + } + *previous = Some(position); + } + /// A declaration initializer: same value emission, but zero literals are /// spelled `0.0` because the frontend drops integer-`0` initializers /// (matching the reference adapter). diff --git a/crates/opy-rs/src/hir/dump.rs b/crates/opy-rs/src/hir/dump.rs index 6927bc36..59203f1c 100644 --- a/crates/opy-rs/src/hir/dump.rs +++ b/crates/opy-rs/src/hir/dump.rs @@ -91,51 +91,33 @@ fn dump_declaration(declaration: &Declaration, out: &mut String, level: usize) { span, initializer, .. - } => { - out.push_str(&format!( - "{}globalVariable {} (index {}){}", - indent(level), - name, - index.map_or("-".to_string(), |i| i.to_string()), - span_suffix(span.as_ref()), - )); - if let Some(initializer) = initializer { - out.push_str(" = "); - render_expr(initializer, out); - } - out.push('\n'); - } + } => dump_indexed_declaration( + "globalVariable", + name, + *index, + span.as_ref(), + initializer.as_deref(), + out, + level, + ), Declaration::PlayerVariable { name, index, span, initializer, .. - } => { - out.push_str(&format!( - "{}playerVariable {} (index {}){}", - indent(level), - name, - index.map_or("-".to_string(), |i| i.to_string()), - span_suffix(span.as_ref()), - )); - if let Some(initializer) = initializer { - out.push_str(" = "); - render_expr(initializer, out); - } - out.push('\n'); - } + } => dump_indexed_declaration( + "playerVariable", + name, + *index, + span.as_ref(), + initializer.as_deref(), + out, + level, + ), Declaration::Subroutine { name, index, span, .. - } => { - out.push_str(&format!( - "{}subroutine {} (index {}){}\n", - indent(level), - name, - index.map_or("-".to_string(), |i| i.to_string()), - span_suffix(span.as_ref()), - )); - } + } => dump_indexed_declaration("subroutine", name, *index, span.as_ref(), None, out, level), Declaration::Constant { name, span, value } => { out.push_str(&format!( "{}constant {}{} = ", @@ -168,6 +150,30 @@ fn dump_declaration(declaration: &Declaration, out: &mut String, level: usize) { } } +fn dump_indexed_declaration( + kind: &str, + name: &str, + index: Option, + span: Option<&Span>, + initializer: Option<&Expr>, + out: &mut String, + level: usize, +) { + out.push_str(&format!( + "{}{} {} (index {}){}", + indent(level), + kind, + name, + index.map_or("-".to_string(), |index| index.to_string()), + span_suffix(span), + )); + if let Some(initializer) = initializer { + out.push_str(" = "); + render_expr(initializer, out); + } + out.push('\n'); +} + fn dump_rule(rule: &Rule, out: &mut String, level: usize) { let state = if rule.disabled { " (disabled)" } else { "" }; out.push_str(&format!( @@ -563,12 +569,7 @@ fn render_expr(expr: &Expr, out: &mut String) { Expr::Call { name, args, .. } => { out.push_str(name); out.push('('); - for (index, arg) in args.iter().enumerate() { - if index > 0 { - out.push_str(", "); - } - render_expr(arg, out); - } + render_call_args(args, out); out.push(')'); } Expr::ReceiverCall { @@ -581,24 +582,14 @@ fn render_expr(expr: &Expr, out: &mut String) { out.push('.'); out.push_str(name); out.push('('); - for (index, arg) in args.iter().enumerate() { - if index > 0 { - out.push_str(", "); - } - render_expr(arg, out); - } + render_call_args(args, out); out.push(')'); } Expr::MacroCall { name, args, .. } => { out.push('$'); out.push_str(name); out.push('('); - for (index, arg) in args.iter().enumerate() { - if index > 0 { - out.push_str(", "); - } - render_expr(arg, out); - } + render_call_args(args, out); out.push(')'); } Expr::MacroParam { name, .. } => { @@ -671,6 +662,15 @@ fn render_expr(expr: &Expr, out: &mut String) { } } +fn render_call_args(args: &[Expr], out: &mut String) { + for (index, arg) in args.iter().enumerate() { + if index > 0 { + out.push_str(", "); + } + render_expr(arg, out); + } +} + fn format_number(value: f64) -> String { if value == value.trunc() && value.abs() < 1e15 { format!("{value:.0}") diff --git a/crates/opy-rs/src/lexer.rs b/crates/opy-rs/src/lexer.rs index 81ac2bce..c6b19391 100644 --- a/crates/opy-rs/src/lexer.rs +++ b/crates/opy-rs/src/lexer.rs @@ -165,7 +165,7 @@ impl Lexer { ';' => self.single(TokenKind::Semicolon), '.' => self.single(TokenKind::Dot), '@' => self.single(TokenKind::At), - '=' => self.two(TokenKind::Assign, TokenKind::Eq, '='), + '=' => self.lex_two(TokenKind::Assign, TokenKind::Eq, '='), '+' => { if self.peek(1) == Some('+') { self.lex_duplicate(TokenKind::Increment, "++"); @@ -187,12 +187,7 @@ impl Lexer { self.advance(); self.advance(); self.advance(); - let end = self.here(0); - self.tokens.push(Token::new( - TokenKind::DoubleStarAssign, - "**=", - Span::new(self.file_id, start.start, end.start), - )); + self.push_token(TokenKind::DoubleStarAssign, "**=", start); } else { self.advance(); self.single(TokenKind::DoubleStar) @@ -203,9 +198,9 @@ impl Lexer { } '/' => self.lex_two(TokenKind::Slash, TokenKind::SlashAssign, '='), '%' => self.lex_two(TokenKind::Percent, TokenKind::PercentAssign, '='), - '<' => self.two(TokenKind::Lt, TokenKind::Le, '='), - '>' => self.two(TokenKind::Gt, TokenKind::Ge, '='), - '!' => self.two(TokenKind::LexBang, TokenKind::Ne, '='), + '<' => self.lex_two(TokenKind::Lt, TokenKind::Le, '='), + '>' => self.lex_two(TokenKind::Gt, TokenKind::Ge, '='), + '!' => self.lex_two(TokenKind::LexBang, TokenKind::Ne, '='), other => { return Err(OpyError::at( "lex-error", @@ -216,7 +211,7 @@ impl Lexer { } } let here = self.here(0); - self.tokens.push(Token::new(TokenKind::Eof, "", here)); + self.push_token(TokenKind::Eof, "", here); Ok(self.tokens) } @@ -238,12 +233,7 @@ impl Lexer { text.push(self.chars[self.pos]); self.advance(); } - let end = self.here(0); - self.tokens.push(Token::new( - TokenKind::Directive, - text, - Span::new(self.file_id, start.start, end.start), - )); + self.push_token(TokenKind::Directive, text, start); } else { while self.pos < self.chars.len() && self.chars[self.pos] != '\n' { self.advance(); @@ -285,12 +275,7 @@ impl Lexer { let ch = self.chars[self.pos]; if ch == quote { self.advance(); - let end = self.here(0); - let mut token = Token::new( - TokenKind::String, - value, - Span::new(self.file_id, start.start, end.start), - ); + let mut token = self.make_token(TokenKind::String, value, start); token.raw = Some(raw); self.tokens.push(token); return Ok(()); @@ -439,12 +424,7 @@ impl Lexer { Span::new(self.file_id, start.start, self.here(0).start), )); } - let end = self.here(0); - self.tokens.push(Token::new( - TokenKind::Number, - text, - Span::new(self.file_id, start.start, end.start), - )); + self.push_token(TokenKind::Number, text, start); return Ok(()); } while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() { @@ -488,12 +468,7 @@ impl Lexer { } } } - let end = self.here(0); - self.tokens.push(Token::new( - TokenKind::Number, - text, - Span::new(self.file_id, start.start, end.start), - )); + self.push_token(TokenKind::Number, text, start); Ok(()) } @@ -504,86 +479,50 @@ impl Lexer { text.push(self.chars[self.pos]); self.advance(); } - let end = self.here(0); - self.tokens.push(Token::new( - TokenKind::Ident, - text, - Span::new(self.file_id, start.start, end.start), - )); + self.push_token(TokenKind::Ident, text, start); } fn single(&mut self, kind: TokenKind) { let start = self.here(1); let text = self.chars[self.pos].to_string(); self.advance(); - let end = self.here(0); - self.tokens.push(Token::new( - kind, - text, - Span::new(self.file_id, start.start, end.start), - )); + self.push_token(kind, text, start); } /// Two-char operator where the second char may be `=`. fn lex_two(&mut self, plain: TokenKind, assign: TokenKind, second: char) { let start = self.here(1); - if self.peek(1) == Some(second) { + let (kind, text) = if self.peek(1) == Some(second) { self.advance(); let text = format!("{}{}", self.chars[self.pos - 1], second); self.advance(); - let end = self.here(0); - self.tokens.push(Token::new( - assign, - text, - Span::new(self.file_id, start.start, end.start), - )); + (assign, text) } else { let text = self.chars[self.pos].to_string(); self.advance(); - let end = self.here(0); - self.tokens.push(Token::new( - plain, - text, - Span::new(self.file_id, start.start, end.start), - )); - } + (plain, text) + }; + self.push_token(kind, text, start); } fn lex_duplicate(&mut self, kind: TokenKind, text: &str) { let start = self.here(1); self.advance(); self.advance(); - let end = self.here(0); - self.tokens.push(Token::new( + self.push_token(kind, text, start); + } + + fn make_token(&self, kind: TokenKind, text: impl Into, start: Span) -> Token { + Token::new( kind, text, - Span::new(self.file_id, start.start, end.start), - )); + Span::new(self.file_id, start.start, self.here(0).start), + ) } - /// Two-char operator with a fixed second char (e.g. `==`, `<=`). - fn two(&mut self, plain: TokenKind, combined: TokenKind, second: char) { - let start = self.here(1); - let text = self.chars[self.pos].to_string(); - if self.peek(1) == Some(second) { - self.advance(); - let combined_text = format!("{}{}", text, second); - self.advance(); - let end = self.here(0); - self.tokens.push(Token::new( - combined, - combined_text, - Span::new(self.file_id, start.start, end.start), - )); - } else { - self.advance(); - let end = self.here(0); - self.tokens.push(Token::new( - plain, - text, - Span::new(self.file_id, start.start, end.start), - )); - } + fn push_token(&mut self, kind: TokenKind, text: impl Into, start: Span) { + let token = self.make_token(kind, text, start); + self.tokens.push(token); } fn here(&self, width: usize) -> Span { @@ -604,11 +543,16 @@ impl Lexer { } } -fn is_ident_start(c: char) -> bool { +pub(crate) fn is_identifier(text: &str) -> bool { + let mut characters = text.chars(); + characters.next().is_some_and(is_ident_start) && characters.all(is_ident_continue) +} + +pub(crate) fn is_ident_start(c: char) -> bool { c.is_ascii_alphabetic() || c == '_' } -fn is_ident_continue(c: char) -> bool { +pub(crate) fn is_ident_continue(c: char) -> bool { c.is_ascii_alphanumeric() || c == '_' } diff --git a/crates/opy-rs/src/manifest/mod.rs b/crates/opy-rs/src/manifest/mod.rs index 3584bf94..22bad222 100644 --- a/crates/opy-rs/src/manifest/mod.rs +++ b/crates/opy-rs/src/manifest/mod.rs @@ -573,25 +573,24 @@ impl Manifest { .map_err(Clone::clone) } + fn resolve_alias(&self, name: &str, kind: AliasKind) -> Option<&str> { + let alias = &self.aliases[*self.alias_by_source.get(name)?]; + (alias.kind == kind).then_some(alias.target.as_str()) + } + /// A generic (non-member) function by source name, alias-aware. pub fn resolve_function(&self, name: &str) -> Option<&Function> { self.function(name).or_else(|| { - let alias = self.alias_by_source.get(name)?; - let alias = &self.aliases[*alias]; - (alias.kind == AliasKind::FunctionAlias) - .then(|| self.function(&alias.target)) - .flatten() + self.resolve_alias(name, AliasKind::FunctionAlias) + .and_then(|target| self.function(target)) }) } /// A member function by source name, alias-aware. pub fn resolve_member(&self, name: &str) -> Option<&Function> { self.member(name).or_else(|| { - let alias = self.alias_by_source.get(name)?; - let alias = &self.aliases[*alias]; - (alias.kind == AliasKind::MemberAlias) - .then(|| self.member(&alias.target)) - .flatten() + self.resolve_alias(name, AliasKind::MemberAlias) + .and_then(|target| self.member(target)) }) } diff --git a/crates/opy-rs/src/parser.rs b/crates/opy-rs/src/parser.rs index f9ea2e05..2dd8e6f4 100644 --- a/crates/opy-rs/src/parser.rs +++ b/crates/opy-rs/src/parser.rs @@ -12,7 +12,7 @@ use crate::cst::{ RuleEntry, Stmt, SwitchArm, TopLevel, }; use crate::diag::{OpyError, Position, Span}; -use crate::lexer::{Token, TokenKind}; +use crate::lexer::{Token, TokenKind, is_identifier}; /// The outcome of a parse. #[derive(Debug, Default)] @@ -75,15 +75,22 @@ impl<'a> Parser<'a> { } } -fn is_identifier(text: &str) -> bool { - !text.is_empty() - && text.chars().enumerate().all(|(index, ch)| { - if index == 0 { - ch.is_ascii_alphabetic() || ch == '_' - } else { - ch.is_ascii_alphanumeric() || ch == '_' - } - }) +fn is_binary_operator(kind: TokenKind) -> bool { + matches!( + kind, + TokenKind::Plus + | TokenKind::Minus + | TokenKind::Star + | TokenKind::Slash + | TokenKind::Percent + | TokenKind::DoubleStar + | TokenKind::Eq + | TokenKind::Ne + | TokenKind::Lt + | TokenKind::Le + | TokenKind::Gt + | TokenKind::Ge + ) } fn unquote_annotation_arg(text: &str) -> String { @@ -137,50 +144,26 @@ impl Parser<'_> { | TokenKind::Comma | TokenKind::Colon | TokenKind::Assign - | TokenKind::Plus - | TokenKind::Minus - | TokenKind::Star - | TokenKind::Slash - | TokenKind::Percent - | TokenKind::DoubleStar - | TokenKind::Eq - | TokenKind::Ne - | TokenKind::Lt - | TokenKind::Le - | TokenKind::Gt - | TokenKind::Ge - ) || (token.kind == TokenKind::Ident - && matches!(token.text.as_str(), "and" | "or" | "in" | "not" | "if")) + ) || is_binary_operator(token.kind) + || (token.kind == TokenKind::Ident + && matches!(token.text.as_str(), "and" | "or" | "in" | "not" | "if")) }); let mut next = self.pos; while self.tokens[next].kind == TokenKind::Newline { next += 1; } let inside_delimiter_group = self.inside_delimiter_group(); - let next_allows_continuation = matches!( - self.tokens[next].kind, - TokenKind::Plus - | TokenKind::Minus - | TokenKind::Star - | TokenKind::Slash - | TokenKind::Percent - | TokenKind::DoubleStar - | TokenKind::Eq - | TokenKind::Ne - | TokenKind::Lt - | TokenKind::Le - | TokenKind::Gt - | TokenKind::Ge - ) || (inside_delimiter_group - && matches!( - self.tokens[next].kind, - TokenKind::LParen - | TokenKind::LBracket - | TokenKind::Dot - | TokenKind::RParen - | TokenKind::RBracket - | TokenKind::RBrace - )) + let next_allows_continuation = is_binary_operator(self.tokens[next].kind) + || (inside_delimiter_group + && matches!( + self.tokens[next].kind, + TokenKind::LParen + | TokenKind::LBracket + | TokenKind::Dot + | TokenKind::RParen + | TokenKind::RBracket + | TokenKind::RBrace + )) || (self.tokens[next].kind == TokenKind::Ident && matches!( self.tokens[next].text.as_str(), diff --git a/crates/opy-rs/src/parser/definitions.rs b/crates/opy-rs/src/parser/definitions.rs index a518fb8d..158036ab 100644 --- a/crates/opy-rs/src/parser/definitions.rs +++ b/crates/opy-rs/src/parser/definitions.rs @@ -187,12 +187,7 @@ impl Parser<'_> { self.error_at_current("@Slot and @Hero cannot be used together".to_string()); return false; } - let end = args.last().map_or(at.span.end, |arg| arg.span.end); - annotations.push(Annotation { - name, - args, - span: Span::new(at.span.file, at.span.start, end), - }); + annotations.push(annotation(name, args, at.span)); true } "Name" => { @@ -203,12 +198,7 @@ impl Parser<'_> { ); return false; } - let end = args.last().map_or(at.span.end, |arg| arg.span.end); - annotations.push(Annotation { - name, - args, - span: Span::new(at.span.file, at.span.start, end), - }); + annotations.push(annotation(name, args, at.span)); true } "SuppressWarnings" => { @@ -219,12 +209,7 @@ impl Parser<'_> { ); return false; } - let end = args.last().map_or(at.span.end, |arg| arg.span.end); - annotations.push(Annotation { - name, - args, - span: Span::new(at.span.file, at.span.start, end), - }); + annotations.push(annotation(name, args, at.span)); true } "Disabled" => { @@ -232,11 +217,7 @@ impl Parser<'_> { return false; } *disabled = true; - annotations.push(Annotation { - name, - args: Vec::new(), - span: at.span, - }); + annotations.push(annotation(name, Vec::new(), at.span)); true } "Delimiter" => { @@ -244,11 +225,7 @@ impl Parser<'_> { return false; } *delimiter = true; - annotations.push(Annotation { - name, - args: Vec::new(), - span: at.span, - }); + annotations.push(annotation(name, Vec::new(), at.span)); true } "NewPage" => { @@ -263,13 +240,8 @@ impl Parser<'_> { ); return false; } - let end = args.last().map_or(at.span.end, |arg| arg.span.end); *new_page = args.first().map(|arg| unquote_annotation_arg(&arg.text)); - annotations.push(Annotation { - name, - args, - span: Span::new(at.span.file, at.span.start, end), - }); + annotations.push(annotation(name, args, at.span)); true } other => { @@ -424,3 +396,12 @@ impl Parser<'_> { true } } + +fn annotation(name: String, args: Vec, directive_span: Span) -> Annotation { + let end = args.last().map_or(directive_span.end, |arg| arg.span.end); + Annotation { + name, + args, + span: Span::new(directive_span.file, directive_span.start, end), + } +} diff --git a/crates/opy-rs/src/parser/expressions.rs b/crates/opy-rs/src/parser/expressions.rs index ce5e6956..d15af6af 100644 --- a/crates/opy-rs/src/parser/expressions.rs +++ b/crates/opy-rs/src/parser/expressions.rs @@ -71,13 +71,7 @@ impl Parser<'_> { self.advance(); self.skip_expression_newlines(); let right = self.parse_and()?; - let span = Span::new(left.span().file, left.span().start, right.span().end); - left = Expr::Binary { - op: "or".to_string(), - left: Box::new(left), - right: Box::new(right), - span, - }; + left = binary("or", left, right); } Ok(left) } @@ -93,13 +87,7 @@ impl Parser<'_> { self.advance(); self.skip_expression_newlines(); let right = self.parse_not()?; - let span = Span::new(left.span().file, left.span().start, right.span().end); - left = Expr::Binary { - op: "and".to_string(), - left: Box::new(left), - right: Box::new(right), - span, - }; + left = binary("and", left, right); } Ok(left) } @@ -142,13 +130,7 @@ impl Parser<'_> { } self.skip_expression_newlines(); let right = self.parse_additive()?; - let span = Span::new(left.span().file, left.span().start, right.span().end); - left = Expr::Binary { - op: op.to_string(), - left: Box::new(left), - right: Box::new(right), - span, - }; + left = binary(op, left, right); } Ok(left) } @@ -169,13 +151,7 @@ impl Parser<'_> { operand: Box::new(operand), }; let right = self.parse_multiplicative_tail(unary)?; - let span = Span::new(left.span().file, left.span().start, right.span().end); - left = Expr::Binary { - op: "-".to_string(), - left: Box::new(left), - right: Box::new(right), - span, - }; + left = binary("-", left, right); continue; } let op = match self.peek_kind() { @@ -186,13 +162,7 @@ impl Parser<'_> { self.advance(); self.skip_expression_newlines(); let right = self.parse_multiplicative()?; - let span = Span::new(left.span().file, left.span().start, right.span().end); - left = Expr::Binary { - op: op.to_string(), - left: Box::new(left), - right: Box::new(right), - span, - }; + left = binary(op, left, right); } Ok(left) } @@ -215,13 +185,7 @@ impl Parser<'_> { self.advance(); self.skip_expression_newlines(); let right = self.parse_unary()?; - let span = Span::new(left.span().file, left.span().start, right.span().end); - left = Expr::Binary { - op: op.to_string(), - left: Box::new(left), - right: Box::new(right), - span, - }; + left = binary(op, left, right); } Ok(left) } @@ -257,13 +221,7 @@ impl Parser<'_> { self.advance(); self.skip_expression_newlines(); let exponent = self.parse_unary()?; - let span = Span::new(base.span().file, base.span().start, exponent.span().end); - return Ok(Expr::Binary { - op: "**".to_string(), - left: Box::new(base), - right: Box::new(exponent), - span, - }); + return Ok(binary("**", base, exponent)); } Ok(base) } @@ -365,6 +323,16 @@ impl Parser<'_> { Ok(base) } + fn has_more_delimited_items(&mut self, closing: TokenKind) -> bool { + self.skip_newlines(); + if self.peek_kind() != TokenKind::Comma { + return false; + } + self.advance(); + self.skip_newlines(); + self.peek_kind() != closing + } + /// `@Event name(args)`: positional expressions only (keyword arguments /// are a call-argument form, not an event form). pub(super) fn parse_event_args(&mut self, args: &mut Vec) -> Result<(), ()> { @@ -381,14 +349,7 @@ impl Parser<'_> { return Err(()); } args.push(expr); - self.skip_newlines(); - if self.peek_kind() == TokenKind::Comma { - self.advance(); - self.skip_newlines(); - if self.peek_kind() == TokenKind::RParen { - break; - } - } else { + if !self.has_more_delimited_items(TokenKind::RParen) { break; } } @@ -435,14 +396,7 @@ impl Parser<'_> { } Err(()) => return Err(()), } - self.skip_newlines(); - if self.peek_kind() == TokenKind::Comma { - self.advance(); - self.skip_newlines(); - if self.peek_kind() == TokenKind::RParen { - break; - } - } else { + if !self.has_more_delimited_items(TokenKind::RParen) { break; } } @@ -573,17 +527,10 @@ impl Parser<'_> { } elements.push(first); loop { - self.skip_newlines(); - if self.peek_kind() == TokenKind::Comma { - self.advance(); - self.skip_newlines(); - if self.peek_kind() == TokenKind::RBracket { - break; - } - elements.push(self.parse_expr()?); - } else { + if !self.has_more_delimited_items(TokenKind::RBracket) { break; } + elements.push(self.parse_expr()?); } let end = match self.expect(TokenKind::RBracket, "']'") { Ok(token) => token.span.end, @@ -666,14 +613,7 @@ impl Parser<'_> { let value = self.parse_expr()?; let span = Span::new(key.span().file, key.span().start, value.span().end); entries.push(DictEntry { key, value, span }); - self.skip_newlines(); - if self.peek_kind() == TokenKind::Comma { - self.advance(); - self.skip_newlines(); - if self.peek_kind() == TokenKind::RBrace { - break; - } - } else { + if !self.has_more_delimited_items(TokenKind::RBrace) { break; } } @@ -868,6 +808,16 @@ impl Parser<'_> { } } +fn binary(op: &str, left: Expr, right: Expr) -> Expr { + let span = Span::new(left.span().file, left.span().start, right.span().end); + Expr::Binary { + op: op.to_string(), + left: Box::new(left), + right: Box::new(right), + span, + } +} + fn display_tokens(tokens: &[Token]) -> String { let mut output = String::new(); for (index, token) in tokens.iter().enumerate() { diff --git a/crates/opy-rs/src/settings.rs b/crates/opy-rs/src/settings.rs index e6fc8692..42024196 100644 --- a/crates/opy-rs/src/settings.rs +++ b/crates/opy-rs/src/settings.rs @@ -19,6 +19,7 @@ use std::collections::HashMap; use crate::cst; use crate::diag::{OpyError, OpyResult, Position, Span}; use crate::hir; +use crate::lexer::{is_ident_continue, is_ident_start}; /// A project `settings { ... }` block. #[derive(Debug, Clone)] @@ -59,15 +60,7 @@ pub fn find_blocks(text: &str, file_id: u32) -> OpyResult> { let mut escaped = false; let mut seen_first_construct = false; while let Some(ch) = scanner.peek() { - if let Some(quote) = string_quote { - if escaped { - escaped = false; - } else if ch == '\\' { - escaped = true; - } else if ch == quote { - string_quote = None; - } - scanner.advance_by(1); + if scanner.advance_quoted(&mut string_quote, &mut escaped, ch) { continue; } if in_block_comment { @@ -212,15 +205,7 @@ fn match_block( keyword_span, )); }; - if let Some(quote) = string_quote { - if escaped { - escaped = false; - } else if ch == '\\' { - escaped = true; - } else if ch == quote { - string_quote = None; - } - scanner.advance_by(1); + if scanner.advance_quoted(&mut string_quote, &mut escaped, ch) { continue; } match ch { @@ -570,6 +555,21 @@ impl Cursor<'_> { } } + fn advance_quoted(&mut self, quote: &mut Option, escaped: &mut bool, ch: char) -> bool { + let Some(quote_char) = *quote else { + return false; + }; + if *escaped { + *escaped = false; + } else if ch == '\\' { + *escaped = true; + } else if ch == quote_char { + *quote = None; + } + self.advance_by(1); + true + } + fn skip_to_eol(&mut self) { while self.peek().is_some_and(|ch| ch != '\n') { self.advance_by(1); @@ -1093,14 +1093,6 @@ fn build_node( } } -fn is_ident_start(c: char) -> bool { - c.is_ascii_alphabetic() || c == '_' -} - -fn is_ident_continue(c: char) -> bool { - c.is_ascii_alphanumeric() || c == '_' -} - #[cfg(test)] mod tests { use super::*; From d15aacae3b118c79c97a597e28b29a9e73f3763b Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 02:46:28 +0800 Subject: [PATCH 21/67] refactor(opy-rs): reuse numeric operator folds --- .../opy-rs/src/compiler/operator_optimization.rs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/crates/opy-rs/src/compiler/operator_optimization.rs b/crates/opy-rs/src/compiler/operator_optimization.rs index ab12b514..a8ed9cb2 100644 --- a/crates/opy-rs/src/compiler/operator_optimization.rs +++ b/crates/opy-rs/src/compiler/operator_optimization.rs @@ -101,7 +101,7 @@ impl<'a> OperatorOptimizer<'a> { ("raiseToPower", 2) => self.power(args), ("-", 1) => self.negate(args), ("roundToInteger", 2) => Self::round(args), - ("absoluteValue", 1) => Self::absolute(args), + ("absoluteValue", 1) => Self::unary("absoluteValue", args, f64::abs), ("sin", 1) => Self::unary("sin", args, f64::sin), ("cos", 1) => Self::unary("cos", args, f64::cos), ("sinDeg", 1) => Self::unary("sinDeg", args, |degrees| { @@ -492,14 +492,6 @@ impl<'a> OperatorOptimizer<'a> { Rewrite::Same(call("roundToInteger", vec![number, direction])) } - fn absolute(args: Vec) -> Rewrite { - let [number] = one(args); - match number { - Value::Number(number) => Rewrite::Changed(Value::Number(number.abs())), - other => Rewrite::Same(call("absoluteValue", vec![other])), - } - } - fn unary(name: &str, args: Vec, apply: fn(f64) -> f64) -> Rewrite { let [number] = one(args); match number { @@ -590,11 +582,7 @@ impl<'a> OperatorOptimizer<'a> { } fn extremum(name: &str, args: Vec, apply: fn(f64, f64) -> f64) -> Rewrite { - let [left, right] = two(args); - match (&left, &right) { - (Value::Number(a), Value::Number(b)) => Rewrite::Changed(Value::Number(apply(*a, *b))), - _ => Rewrite::Same(call(name, vec![left, right])), - } + Self::binary(name, args, apply) } fn magnitude(args: Vec) -> Rewrite { From 8cc9a4d4023facd2a01d211fe9a8814c91a81fa7 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 03:11:42 +0800 Subject: [PATCH 22/67] refactor(opy-rs): share value tree traversal --- .../opy-rs/src/compiler/action_optimization.rs | 6 ++---- crates/opy-rs/src/compiler/mod.rs | 1 + crates/opy-rs/src/compiler/number_format.rs | 13 ++++++------- .../opy-rs/src/compiler/size_optimization.rs | 11 ++--------- crates/opy-rs/src/compiler/string_format.rs | 13 ++----------- crates/opy-rs/src/compiler/value_walk.rs | 18 ++++++++++++++++++ 6 files changed, 31 insertions(+), 31 deletions(-) create mode 100644 crates/opy-rs/src/compiler/value_walk.rs diff --git a/crates/opy-rs/src/compiler/action_optimization.rs b/crates/opy-rs/src/compiler/action_optimization.rs index 14967d92..0e12762f 100644 --- a/crates/opy-rs/src/compiler/action_optimization.rs +++ b/crates/opy-rs/src/compiler/action_optimization.rs @@ -7,6 +7,7 @@ use workshop_rs::{Action, Value}; use super::Compiler; use super::operator_optimization::falsy; use super::size_optimization::{action_values, is_empty_string}; +use super::value_walk::for_each_child; pub(super) struct ActionOptimizer<'a> { compiler: &'a Compiler, @@ -88,10 +89,7 @@ impl<'a> ActionOptimizer<'a> { fn nested(&self, value: &mut Value) { match value { Value::Call { name, args } => self.booleans(Kind::Value, name, args), - Value::Array(elements) => elements.iter_mut().for_each(|element| self.nested(element)), - Value::Vector { x, y, z } => [x, y, z].into_iter().for_each(|part| self.nested(part)), - Value::PlayerVariable { player, .. } => self.nested(player), - _ => {} + _ => for_each_child(value, |child| self.nested(child)), } } } diff --git a/crates/opy-rs/src/compiler/mod.rs b/crates/opy-rs/src/compiler/mod.rs index b37a748b..6f055207 100644 --- a/crates/opy-rs/src/compiler/mod.rs +++ b/crates/opy-rs/src/compiler/mod.rs @@ -28,6 +28,7 @@ mod operator_optimization; mod settings; mod size_optimization; mod string_format; +mod value_walk; pub(crate) use backend::MacroExpander; pub(super) use backend::{expand_macros, expand_macros_attributed, reject_unlowered_directives}; diff --git a/crates/opy-rs/src/compiler/number_format.rs b/crates/opy-rs/src/compiler/number_format.rs index 85398c12..f8049f86 100644 --- a/crates/opy-rs/src/compiler/number_format.rs +++ b/crates/opy-rs/src/compiler/number_format.rs @@ -3,15 +3,14 @@ use workshop_rs::Value; +use super::value_walk::for_each_child; + /// Cut every number in a value to what the reference writes for it. pub(super) fn trim_numbers(value: &mut Value) { - match value { - Value::Number(number) => *number = trimmed(*number), - Value::Array(values) => values.iter_mut().for_each(trim_numbers), - Value::Vector { x, y, z } => [x, y, z].into_iter().for_each(|v| trim_numbers(v)), - Value::PlayerVariable { player, .. } => trim_numbers(player), - Value::Call { args, .. } => args.iter_mut().for_each(trim_numbers), - _ => {} + if let Value::Number(number) = value { + *number = trimmed(*number); + } else { + for_each_child(value, trim_numbers); } } diff --git a/crates/opy-rs/src/compiler/size_optimization.rs b/crates/opy-rs/src/compiler/size_optimization.rs index 952e45c3..0c360625 100644 --- a/crates/opy-rs/src/compiler/size_optimization.rs +++ b/crates/opy-rs/src/compiler/size_optimization.rs @@ -7,6 +7,7 @@ use workshop_rs::{Action, ModifyOp, Value}; use self::literal_slots::{Slot, slot}; use super::Compiler; use super::operator_optimization::falsy; +use super::value_walk::for_each_child; mod literal_slots; @@ -257,16 +258,10 @@ impl<'a> SizeOptimizer<'a> { return self.nested(value); } self.call_arguments(Kind::Value, "vector", args); - for arg in args.iter_mut() { - self.nested(arg); - } } Value::Call { name, args } => { self.compared(name, args); self.call_arguments(Kind::Value, name, args); - for arg in args { - self.nested(arg); - } } Value::Array(elements) => { let coercions = self @@ -278,7 +273,6 @@ impl<'a> SizeOptimizer<'a> { .unwrap_or_default(); for element in elements { self.argument("array", 0, coercions, element); - self.nested(element); } } Value::Vector { x, y, z } => { @@ -288,12 +282,11 @@ impl<'a> SizeOptimizer<'a> { } for (index, component) in [&mut **x, &mut **y, &mut **z].into_iter().enumerate() { self.argument("vector", index, ParamCoercions::default(), component); - self.nested(component); } } - Value::PlayerVariable { player, .. } => self.nested(player), _ => {} } + for_each_child(value, |child| self.nested(child)); } fn compared(&self, name: &str, args: &mut [Value]) { diff --git a/crates/opy-rs/src/compiler/string_format.rs b/crates/opy-rs/src/compiler/string_format.rs index 904e533a..3c11f2ef 100644 --- a/crates/opy-rs/src/compiler/string_format.rs +++ b/crates/opy-rs/src/compiler/string_format.rs @@ -6,6 +6,7 @@ use workshop_rs::Value; use super::operator_optimization::same; +use super::value_walk::for_each_child; const MAX_LENGTH: usize = 128; const MAX_ARGS: usize = 3; @@ -119,17 +120,7 @@ pub(super) fn unsplit(tokens: Vec) -> Value { /// Splits every custom string in the tree into strings the Workshop accepts. pub(super) fn split_all(value: &mut Value) { - match value { - Value::Call { args, .. } => args.iter_mut().for_each(split_all), - Value::Array(elements) => elements.iter_mut().for_each(split_all), - Value::Vector { x, y, z } => { - split_all(x); - split_all(y); - split_all(z); - } - Value::PlayerVariable { player, .. } => split_all(player), - _ => {} - } + for_each_child(value, split_all); if let Some(tokens) = tokens(value) { *value = split(tokens); } diff --git a/crates/opy-rs/src/compiler/value_walk.rs b/crates/opy-rs/src/compiler/value_walk.rs new file mode 100644 index 00000000..3dd8358b --- /dev/null +++ b/crates/opy-rs/src/compiler/value_walk.rs @@ -0,0 +1,18 @@ +use workshop_rs::Value; + +pub(super) fn for_each_child(value: &mut Value, mut visit: impl FnMut(&mut Value)) { + match value { + Value::Array(children) | Value::Call { args: children, .. } => { + for child in children { + visit(child); + } + } + Value::Vector { x, y, z } => { + visit(x); + visit(y); + visit(z); + } + Value::PlayerVariable { player, .. } => visit(player), + _ => {} + } +} From 55f47d67ea50cec249865915a19cf4c900350d80 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 03:31:38 +0800 Subject: [PATCH 23/67] refactor(opy-rs): share compression decode lowering --- crates/opy-rs/src/compiler/lowering/values.rs | 182 +++++++++--------- 1 file changed, 89 insertions(+), 93 deletions(-) diff --git a/crates/opy-rs/src/compiler/lowering/values.rs b/crates/opy-rs/src/compiler/lowering/values.rs index caa9e755..c445abe1 100644 --- a/crates/opy-rs/src/compiler/lowering/values.rs +++ b/crates/opy-rs/src/compiler/lowering/values.rs @@ -1412,10 +1412,42 @@ impl<'a> Lowering<'a> { is_vector: bool, ) -> Result { let text = self.lower_value(text)?; + let (decoded, alphabet, variable_alphabet) = self.lower_compression_source(text); + let (width, min_decimal_place, offset) = if is_vector { + (3, -2.0, 5000.0) + } else { + (4, -3.0, 50000.0) + }; + let component = |this: &mut Self, component_offset| { + let value = this.lower_compressed_component( + alphabet, + variable_alphabet, + width, + min_decimal_place, + component_offset, + None, + ); + let offset = this.push_number(offset); + this.push_call("subtract", vec![value, offset]) + }; + if is_vector { + let x = component(self, 0); + let y = component(self, width * 2); + let z = component(self, width); + let vector = self.push_call("vector", vec![x, y, z]); + Ok(self.push_call("mappedArray", vec![decoded, vector])) + } else { + let number = component(self, 0); + Ok(self.push_call("mappedArray", vec![decoded, number])) + } + } + + fn lower_compression_source(&mut self, text: ValueId) -> (ValueId, ValueId, bool) { + let variable_alphabet = has_directive(self.hir, "useVariableForCompressionAlphabet"); let null = self.push_value(Value::Null); let separator = self.push_call("firstOf", vec![null]); let split = self.push_call("stringSplit", vec![text, separator]); - let alphabet = if has_directive(self.hir, "useVariableForCompressionAlphabet") { + let alphabet = if variable_alphabet { let variable = *self .globals .get(COMPRESSION_ALPHABET_NAME) @@ -1424,53 +1456,52 @@ impl<'a> Lowering<'a> { } else { self.lower_custom_string(compression_alphabet()) }; - let decoded = if has_directive(self.hir, "useVariableForCompressionAlphabet") { + let decoded = if variable_alphabet { split } else { let current = self.push_call("currentArrayElement", Vec::new()); let alphabet = self.push_call("appendToArray", vec![current, alphabet]); self.push_call("mappedArray", vec![split, alphabet]) }; - let width = if is_vector { 3 } else { 4 }; - let min_decimal_place = if is_vector { -2.0 } else { -3.0 }; - let offset = if is_vector { 5000.0 } else { 50000.0 }; - let component = |this: &mut Self, component_offset: usize| { - let current = this.push_call("currentArrayElement", Vec::new()); - let mut terms = Vec::with_capacity(width); - for index in 0..width { - let position = this.push_number((index + component_offset) as f64); - let character = this.push_call("charAt", vec![current, position]); - let formula_alphabet = - if has_directive(this.hir, "useVariableForCompressionAlphabet") { - alphabet - } else { - this.push_call("lastOf", vec![current]) - }; - let digit = this.push_call("strIndex", vec![formula_alphabet, character]); - let power = 100_f64.powf(index as f64 + min_decimal_place / 2.0); - let power = this.push_number(power); - terms.push(this.push_call("multiply", vec![power, digit])); - } - let mut value = terms - .first() - .copied() - .unwrap_or_else(|| this.push_number(0.0)); - for term in terms.into_iter().skip(1) { - value = this.push_call("add", vec![value, term]); + (decoded, alphabet, variable_alphabet) + } + + fn lower_compressed_component( + &mut self, + alphabet: ValueId, + variable_alphabet: bool, + width: usize, + min_decimal_place: f64, + component_offset: usize, + optimized_strict: Option, + ) -> ValueId { + let current = self.push_call("currentArrayElement", Vec::new()); + let mut terms = Vec::with_capacity(width); + for index in 0..width { + let position = self.push_number((index + component_offset) as f64); + let character = self.push_call("charAt", vec![current, position]); + let formula_alphabet = if variable_alphabet { + alphabet + } else { + self.push_call("lastOf", vec![current]) + }; + let digit = self.push_call("strIndex", vec![formula_alphabet, character]); + let power = 100_f64.powf(index as f64 + min_decimal_place / 2.0); + let power = self.push_number(power); + let weighted = self.push_call("multiply", vec![power, digit]); + if let Some(strict) = optimized_strict { + self.optimized_nodes.insert(weighted, strict); } - let offset = this.push_number(offset); - this.push_call("subtract", vec![value, offset]) - }; - if is_vector { - let x = component(self, 0); - let y = component(self, width * 2); - let z = component(self, width); - let vector = self.push_call("vector", vec![x, y, z]); - Ok(self.push_call("mappedArray", vec![decoded, vector])) - } else { - let number = component(self, 0); - Ok(self.push_call("mappedArray", vec![decoded, number])) + terms.push(weighted); } + let mut value = terms + .first() + .copied() + .unwrap_or_else(|| self.push_number(0.0)); + for term in terms.into_iter().skip(1) { + value = self.push_call("add", vec![value, term]); + } + value } fn lower_compressed_mode( @@ -1617,61 +1648,20 @@ impl<'a> Lowering<'a> { return Ok(self.lower_custom_string(compressed)); } let compressed_string = self.lower_custom_string(compressed); - let null = self.push_value(Value::Null); - let separator = self.push_call("firstOf", vec![null]); - let split = self.push_call("stringSplit", vec![compressed_string, separator]); - let alphabet_value = if has_directive(self.hir, "useVariableForCompressionAlphabet") { - let variable = *self - .globals - .get(COMPRESSION_ALPHABET_NAME) - .expect("compression alphabet variable is created"); - self.push_value(Value::GlobalVariable(self.global_names[variable].clone())) - } else { - self.lower_custom_string(compression_alphabet()) - }; - let decoded = if has_directive(self.hir, "useVariableForCompressionAlphabet") { - split - } else { - let current = self.push_call("currentArrayElement", Vec::new()); - let alphabet = self.push_call("appendToArray", vec![current, alphabet_value]); - self.push_call("mappedArray", vec![split, alphabet]) - }; + let (decoded, alphabet, variable_alphabet) = + self.lower_compression_source(compressed_string); let width = ((max_decimal_place - min_decimal_place + 1) / 2) as usize; let optimization = self.optimization_state_at(span.as_ref()); - let component = |this: &mut Self, component_offset: usize| { - let current = this.push_call("currentArrayElement", Vec::new()); - let mut terms = Vec::with_capacity(width); - for index in 0..width { - let position = this.push_number((index + component_offset) as f64); - let character = this.push_call("charAt", vec![current, position]); - let formula_alphabet = - if has_directive(this.hir, "useVariableForCompressionAlphabet") { - alphabet_value - } else { - this.push_call("lastOf", vec![current]) - }; - let digit = this.push_call("strIndex", vec![formula_alphabet, character]); - let power = 100_f64.powf(index as f64 + f64::from(min_decimal_place) / 2.0); - let power = this.push_number(power); - let weighted = this.push_call("multiply", vec![power, digit]); - if optimization.enabled { - this.optimized_nodes.insert(weighted, optimization.strict); - } - terms.push(weighted); - } - let mut value = terms - .first() - .copied() - .unwrap_or_else(|| this.push_number(0.0)); - for term in terms.into_iter().skip(1) { - value = this.push_call("add", vec![value, term]); - } - if is_vector || compression_offset == 0.0 { - value - } else { - let offset = this.push_number(compression_offset); - this.push_call("add", vec![value, offset]) - } + let optimized_strict = optimization.enabled.then_some(optimization.strict); + let component = |this: &mut Self, component_offset| { + this.lower_compressed_component( + alphabet, + variable_alphabet, + width, + f64::from(min_decimal_place), + component_offset, + optimized_strict, + ) }; let value = if is_vector { let x = component(self, 0); @@ -1688,6 +1678,12 @@ impl<'a> Lowering<'a> { self.push_call("mappedArray", vec![decoded, value]) } else { let number = component(self, 0); + let number = if compression_offset == 0.0 { + number + } else { + let offset = self.push_number(compression_offset); + self.push_call("add", vec![number, offset]) + }; self.push_call("mappedArray", vec![decoded, number]) }; Ok(value) From 056664fe6ea65b027cf416de35672d1941fc6114 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 03:36:18 +0800 Subject: [PATCH 24/67] refactor(opy-rs): share rule template values --- crates/opy-rs/src/lower.rs | 59 +++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/crates/opy-rs/src/lower.rs b/crates/opy-rs/src/lower.rs index e9472558..4ccb1af7 100644 --- a/crates/opy-rs/src/lower.rs +++ b/crates/opy-rs/src/lower.rs @@ -493,6 +493,31 @@ enum TemplateValue { Bool(bool), } +fn rule_template_values( + name: &str, + prefix: &str, + file: &str, + path: &str, + delimiter: bool, +) -> [(&'static str, TemplateValue); 14] { + [ + ("$rule", TemplateValue::String(name.to_string())), + ("$prefix", TemplateValue::String(prefix.to_string())), + ("$file", TemplateValue::String(file.to_string())), + ("$path", TemplateValue::String(path.to_string())), + ("$isDelimiter", TemplateValue::Bool(delimiter)), + ("$prefixTitle", TemplateValue::String(title_case(prefix))), + ("$prefixUpper", TemplateValue::String(prefix.to_uppercase())), + ("$prefixLower", TemplateValue::String(prefix.to_lowercase())), + ("$fileTitle", TemplateValue::String(title_case(file))), + ("$fileUpper", TemplateValue::String(file.to_uppercase())), + ("$fileLower", TemplateValue::String(file.to_lowercase())), + ("$pathTitle", TemplateValue::String(title_case(path))), + ("$pathUpper", TemplateValue::String(path.to_uppercase())), + ("$pathLower", TemplateValue::String(path.to_lowercase())), + ] +} + fn render_rule_name( name: &str, prefix: Option<&str>, @@ -512,22 +537,7 @@ fn render_rule_name( }; let (file, path) = rule_file_parts(span.file, files); let prefix = prefix.unwrap_or_default(); - let values = [ - ("$rule", TemplateValue::String(name.to_string())), - ("$prefix", TemplateValue::String(prefix.to_string())), - ("$file", TemplateValue::String(file.clone())), - ("$path", TemplateValue::String(path.clone())), - ("$isDelimiter", TemplateValue::Bool(delimiter)), - ("$prefixTitle", TemplateValue::String(title_case(prefix))), - ("$prefixUpper", TemplateValue::String(prefix.to_uppercase())), - ("$prefixLower", TemplateValue::String(prefix.to_lowercase())), - ("$fileTitle", TemplateValue::String(title_case(&file))), - ("$fileUpper", TemplateValue::String(file.to_uppercase())), - ("$fileLower", TemplateValue::String(file.to_lowercase())), - ("$pathTitle", TemplateValue::String(title_case(&path))), - ("$pathUpper", TemplateValue::String(path.to_uppercase())), - ("$pathLower", TemplateValue::String(path.to_lowercase())), - ]; + let values = rule_template_values(name, prefix, &file, &path, delimiter); evaluate_template(template, &values) .map(|name| strip_rule_name_formatting(&name)) .map_err(|message| { @@ -549,22 +559,7 @@ pub(crate) fn render_generated_rule_name(name: &str, preprocessing: &Preprocessi else { return name.to_string(); }; - let values = [ - ("$rule", TemplateValue::String(name.to_string())), - ("$prefix", TemplateValue::String(String::new())), - ("$file", TemplateValue::String(String::new())), - ("$path", TemplateValue::String(String::new())), - ("$isDelimiter", TemplateValue::Bool(false)), - ("$prefixTitle", TemplateValue::String(String::new())), - ("$prefixUpper", TemplateValue::String(String::new())), - ("$prefixLower", TemplateValue::String(String::new())), - ("$fileTitle", TemplateValue::String(String::new())), - ("$fileUpper", TemplateValue::String(String::new())), - ("$fileLower", TemplateValue::String(String::new())), - ("$pathTitle", TemplateValue::String(String::new())), - ("$pathUpper", TemplateValue::String(String::new())), - ("$pathLower", TemplateValue::String(String::new())), - ]; + let values = rule_template_values(name, "", "", "", false); evaluate_template(template, &values) .map(|rendered| strip_rule_name_formatting(&rendered)) .unwrap_or_else(|_| name.to_string()) From da5a3e92ae9b1f4a69e1709efee736b3f657218c Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 03:40:01 +0800 Subject: [PATCH 25/67] refactor(opy-rs): reuse shared directive lookup --- crates/opy-rs/src/compiler/lowering.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/crates/opy-rs/src/compiler/lowering.rs b/crates/opy-rs/src/compiler/lowering.rs index 540bac72..8820a1b1 100644 --- a/crates/opy-rs/src/compiler/lowering.rs +++ b/crates/opy-rs/src/compiler/lowering.rs @@ -2001,13 +2001,6 @@ fn is_zero_initializer(expr: &hir::Expr) -> bool { ) } -fn has_directive(hir: &hir::Program, name: &str) -> bool { - hir.preprocessing - .directives - .iter() - .any(|directive| directive.name == name) -} - fn directive_value<'a>(hir: &'a hir::Program, name: &str) -> Option<&'a str> { hir.preprocessing .directives From 3c7266791cb1b5ede7a9fba82e998fd124a4f02f Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 04:13:46 +0800 Subject: [PATCH 26/67] refactor(opy-rs): remove string token forwarder --- crates/opy-rs/src/compiler/string_format.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/opy-rs/src/compiler/string_format.rs b/crates/opy-rs/src/compiler/string_format.rs index 3c11f2ef..2faa3c32 100644 --- a/crates/opy-rs/src/compiler/string_format.rs +++ b/crates/opy-rs/src/compiler/string_format.rs @@ -53,16 +53,16 @@ pub(super) fn tokens(value: &Value) -> Option> { } /// Splices nested strings and turns constant arguments into text. -pub(super) fn merge(tokens: Vec) -> (Vec, bool) { +pub(super) fn merge(parts: Vec) -> (Vec, bool) { let mut changed = false; let mut merged: Vec = Vec::new(); - let mut pending: std::collections::VecDeque = tokens.into(); + let mut pending: std::collections::VecDeque = parts.into(); while let Some(token) = pending.pop_front() { let Token::Argument(argument) = token else { push_text(&mut merged, token); continue; }; - if let Some(inner) = tokens_of(&argument) { + if let Some(inner) = tokens(&argument) { changed = true; for token in inner.into_iter().rev() { pending.push_front(token); @@ -85,10 +85,6 @@ pub(super) fn merge(tokens: Vec) -> (Vec, bool) { (merged, changed) } -fn tokens_of(value: &Value) -> Option> { - tokens(value) -} - fn push_text(tokens: &mut Vec, token: Token) { if let (Token::Text(text), Some(Token::Text(last))) = (&token, tokens.last_mut()) { last.push_str(text); From 13ada4f9bf4d2d3640b5649dc78616a59212d529 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 04:13:52 +0800 Subject: [PATCH 27/67] refactor(opy-rs): read space widths from source table --- crates/opy-rs/src/compiler/blizzard_global.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/opy-rs/src/compiler/blizzard_global.rs b/crates/opy-rs/src/compiler/blizzard_global.rs index 99dadfb7..acf2e7ab 100644 --- a/crates/opy-rs/src/compiler/blizzard_global.rs +++ b/crates/opy-rs/src/compiler/blizzard_global.rs @@ -1211,16 +1211,13 @@ pub(crate) fn spaces(width: i32) -> String { return String::new(); } let width = width as usize; - let coins = SPACES - .iter() - .map(|(width, _)| *width as usize) - .collect::>(); let mut cost = vec![usize::MAX; width + 1]; let mut previous = vec![None; width + 1]; cost[0] = 0; for current in 1..=width { - for (index, coin) in coins.iter().enumerate() { - if *coin <= current && cost[current - coin] != usize::MAX { + for (index, (coin, _)) in SPACES.iter().enumerate() { + let coin = *coin as usize; + if coin <= current && cost[current - coin] != usize::MAX { let candidate = cost[current - coin] + 1; if candidate < cost[current] { cost[current] = candidate; @@ -1236,7 +1233,7 @@ pub(crate) fn spaces(width: i32) -> String { let mut remaining = width; while remaining > 0 { let index = previous[remaining].expect("reachable Blizzard Global width"); - let coin = coins[index]; + let coin = SPACES[index].0 as usize; result.push_str(SPACES[index].1); remaining -= coin; } From 7d3f991201b2882728940f2d348057d7b1c4d540 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 04:16:21 +0800 Subject: [PATCH 28/67] refactor(opy-rs): reuse lexer identifier rules --- crates/opy-rs/src/compiler/reconstruct.rs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/crates/opy-rs/src/compiler/reconstruct.rs b/crates/opy-rs/src/compiler/reconstruct.rs index 7fab5abe..cbfcc2ab 100644 --- a/crates/opy-rs/src/compiler/reconstruct.rs +++ b/crates/opy-rs/src/compiler/reconstruct.rs @@ -5,6 +5,7 @@ use workshop_rs::catalog::{Catalog, Locale}; use workshop_rs::program::{Action, Event, EventTarget, EventTeam, ModifyOp, Rule, Value}; use workshop_rs::source::Span; +use crate::lexer::is_identifier; use crate::manifest::{Function, FunctionKind, Manifest}; /// A structured reconstruction diagnostic naming one non-representable @@ -124,16 +125,6 @@ const RESERVED_NAMES: &[&str] = &[ "not", ]; -/// Whether `name` is a valid OPY identifier (the lexer's identifier rule). -fn is_opy_identifier(name: &str) -> bool { - let mut chars = name.chars(); - let Some(first) = chars.next() else { - return false; - }; - (first.is_ascii_alphabetic() || first == '_') - && chars.all(|c| c.is_ascii_alphanumeric() || c == '_') -} - /// Binary operator spellings the OPY frontend lowers to `Value::Call`s with /// the same name (source operators, not Workshop spellings like `add`). const BINARY_OPS: &[&str] = &[ @@ -268,7 +259,7 @@ impl<'a> Emitter<'a> { } fn check_variable_name(&mut self, name: &str, span: Option, kind: &str) { - if !is_opy_identifier(name) { + if !is_identifier(name) { self.issue( "unsupported-name", format!( From bfff7d244b150b56be0a8229d1af2f454138a9b2 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 04:36:23 +0800 Subject: [PATCH 29/67] refactor(opy-rs): localize cased progress lowering --- crates/opy-rs/src/compiler/lowering.rs | 105 ------------------ .../src/compiler/lowering/action_calls.rs | 105 ++++++++++++++++++ 2 files changed, 105 insertions(+), 105 deletions(-) diff --git a/crates/opy-rs/src/compiler/lowering.rs b/crates/opy-rs/src/compiler/lowering.rs index 8820a1b1..62adf249 100644 --- a/crates/opy-rs/src/compiler/lowering.rs +++ b/crates/opy-rs/src/compiler/lowering.rs @@ -22,111 +22,6 @@ use workshop_rs::{Event, EventTarget, EventTeam, ModifyOp, PlayerEventKind}; const COMPRESSION_ALPHABET_NAME: &str = "__compressionAlphabet__"; const EMPTY_STRING_NAME: &str = "__emptyString__"; -use super::blizzard_global; - -fn is_cased_color_tag(text: &[char], index: usize) -> Option { - let remaining = text[index..].iter().collect::(); - let is_tag = remaining - .get(..3) - .is_some_and(|prefix| prefix.eq_ignore_ascii_case("")); - if !is_tag { - return None; - } - remaining - .find('>') - .map(|offset| index + remaining[..=offset].chars().count()) -} - -fn cased_line(text: &str, text_count: usize) -> Vec { - let characters = text.chars().collect::>(); - let mut text_without_tags = String::new(); - let mut plain_index = 0; - while plain_index < characters.len() { - if let Some(end) = is_cased_color_tag(&characters, plain_index) { - plain_index = end; - } else { - text_without_tags.push(characters[plain_index]); - plain_index += 1; - } - } - let mut text_width = 0; - let mut found_lowercase = false; - for character in text_without_tags.chars() { - if let Some(glyph) = blizzard_global::cased_glyph(character) { - found_lowercase = true; - if !matches!(character, 'i' | 'j' | 'l') { - text_width = (glyph.lower_xmin - text_width).max(0); - break; - } - } else { - text_width += blizzard_global::width(character); - } - } - if !found_lowercase { - return vec![text.to_string(); text_count]; - } - - let mut outputs = vec![String::new(); text_count]; - let mut widths = vec![0; text_count]; - let mut text_index = 0; - let mut last_character = None; - let mut index = 0; - while index < characters.len() { - if let Some(end) = is_cased_color_tag(&characters, index) { - let tag = characters[index..end].iter().collect::(); - for output in &mut outputs { - output.push_str(&tag); - } - text_index = (text_index + 1) % text_count; - index = end; - continue; - } - let character = characters[index]; - if let Some(glyph) = blizzard_global::cased_glyph(character) { - text_index = (text_index + 1) % text_count; - let padding = (text_width - widths[text_index] - glyph.lower_xmin + glyph.xmin).max(0); - outputs[text_index].push_str(&blizzard_global::spaces(padding)); - widths[text_index] += padding; - outputs[text_index].push_str(glyph.lower); - widths[text_index] += glyph.lower_width; - last_character = Some(character); - } else if character != ' ' { - if outputs[text_index].is_empty() - || last_character - .and_then(blizzard_global::cased_glyph) - .is_some() - || last_character == Some(' ') - { - text_index = (text_index + 1) % text_count; - let padding = (text_width - widths[text_index]).max(0); - outputs[text_index].push_str(&blizzard_global::spaces(padding)); - widths[text_index] += padding; - } - outputs[text_index].push(character); - widths[text_index] += blizzard_global::width(character); - last_character = Some(character); - } else { - last_character = Some(character); - } - text_width += blizzard_global::cased_glyph(character) - .map_or_else(|| blizzard_global::width(character), |glyph| glyph.width); - index += 1; - } - let maximum = widths - .iter() - .copied() - .max() - .unwrap_or_default() - .max(text_width); - for (output, width) in outputs.iter_mut().zip(widths) { - output.push_str(&blizzard_global::spaces(maximum - width)); - } - outputs -} - #[derive(Debug, Clone)] enum Value { Number(f64), diff --git a/crates/opy-rs/src/compiler/lowering/action_calls.rs b/crates/opy-rs/src/compiler/lowering/action_calls.rs index 360d82f6..f7e48b4f 100644 --- a/crates/opy-rs/src/compiler/lowering/action_calls.rs +++ b/crates/opy-rs/src/compiler/lowering/action_calls.rs @@ -1,5 +1,110 @@ use super::*; +use super::super::blizzard_global; + +fn is_cased_color_tag(text: &[char], index: usize) -> Option { + let remaining = text[index..].iter().collect::(); + let is_tag = remaining + .get(..3) + .is_some_and(|prefix| prefix.eq_ignore_ascii_case("")); + if !is_tag { + return None; + } + remaining + .find('>') + .map(|offset| index + remaining[..=offset].chars().count()) +} + +fn cased_line(text: &str, text_count: usize) -> Vec { + let characters = text.chars().collect::>(); + let mut text_without_tags = String::new(); + let mut plain_index = 0; + while plain_index < characters.len() { + if let Some(end) = is_cased_color_tag(&characters, plain_index) { + plain_index = end; + } else { + text_without_tags.push(characters[plain_index]); + plain_index += 1; + } + } + let mut text_width = 0; + let mut found_lowercase = false; + for character in text_without_tags.chars() { + if let Some(glyph) = blizzard_global::cased_glyph(character) { + found_lowercase = true; + if !matches!(character, 'i' | 'j' | 'l') { + text_width = (glyph.lower_xmin - text_width).max(0); + break; + } + } else { + text_width += blizzard_global::width(character); + } + } + if !found_lowercase { + return vec![text.to_string(); text_count]; + } + + let mut outputs = vec![String::new(); text_count]; + let mut widths = vec![0; text_count]; + let mut text_index = 0; + let mut last_character = None; + let mut index = 0; + while index < characters.len() { + if let Some(end) = is_cased_color_tag(&characters, index) { + let tag = characters[index..end].iter().collect::(); + for output in &mut outputs { + output.push_str(&tag); + } + text_index = (text_index + 1) % text_count; + index = end; + continue; + } + let character = characters[index]; + if let Some(glyph) = blizzard_global::cased_glyph(character) { + text_index = (text_index + 1) % text_count; + let padding = (text_width - widths[text_index] - glyph.lower_xmin + glyph.xmin).max(0); + outputs[text_index].push_str(&blizzard_global::spaces(padding)); + widths[text_index] += padding; + outputs[text_index].push_str(glyph.lower); + widths[text_index] += glyph.lower_width; + last_character = Some(character); + } else if character != ' ' { + if outputs[text_index].is_empty() + || last_character + .and_then(blizzard_global::cased_glyph) + .is_some() + || last_character == Some(' ') + { + text_index = (text_index + 1) % text_count; + let padding = (text_width - widths[text_index]).max(0); + outputs[text_index].push_str(&blizzard_global::spaces(padding)); + widths[text_index] += padding; + } + outputs[text_index].push(character); + widths[text_index] += blizzard_global::width(character); + last_character = Some(character); + } else { + last_character = Some(character); + } + text_width += blizzard_global::cased_glyph(character) + .map_or_else(|| blizzard_global::width(character), |glyph| glyph.width); + index += 1; + } + let maximum = widths + .iter() + .copied() + .max() + .unwrap_or_default() + .max(text_width); + for (output, width) in outputs.iter_mut().zip(widths) { + output.push_str(&blizzard_global::spaces(maximum - width)); + } + outputs +} + impl<'a> Lowering<'a> { pub(super) fn lower_cased_progress_bar( &mut self, From b398ee8af081a63c5d5d37b44fe221cee6684bdf Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 04:59:16 +0800 Subject: [PATCH 30/67] refactor(opy-rs): isolate declaration lowering --- crates/opy-rs/src/compiler/lowering.rs | 718 +---------------- .../src/compiler/lowering/declarations.rs | 720 ++++++++++++++++++ 2 files changed, 721 insertions(+), 717 deletions(-) create mode 100644 crates/opy-rs/src/compiler/lowering/declarations.rs diff --git a/crates/opy-rs/src/compiler/lowering.rs b/crates/opy-rs/src/compiler/lowering.rs index 62adf249..35dbd1cf 100644 --- a/crates/opy-rs/src/compiler/lowering.rs +++ b/crates/opy-rs/src/compiler/lowering.rs @@ -1,5 +1,6 @@ mod action_calls; mod assignments; +mod declarations; mod presentation; mod rules; mod values; @@ -302,723 +303,6 @@ impl<'a> Lowering<'a> { Ok(()) } - fn translation_helper_index( - &self, - reserved: &HashSet, - ) -> Result, IntegrationError> { - let Some(translations) = self.hir.preprocessing.translations.as_ref() else { - return Ok(None); - }; - self.free_global_index( - reserved, - translations.span, - "no available global variable index remains for translations", - ) - } - - fn helper_global_index( - &self, - reserved: &HashSet, - directive: &str, - message: &str, - ) -> Result, IntegrationError> { - let Some(source) = self - .hir - .preprocessing - .directives - .iter() - .find(|item| item.name == directive) - else { - return Ok(None); - }; - self.free_global_index(reserved, source.span, message) - } - - fn free_global_index( - &self, - reserved: &HashSet, - span: Option, - message: &str, - ) -> Result, IntegrationError> { - (0..=127) - .rev() - .find(|index| !reserved.contains(index)) - .map(Some) - .ok_or_else(|| IntegrationError::new("index-exhausted", message, span)) - } - - pub(super) fn lower_declarations(&mut self) -> Result<(), IntegrationError> { - let (implicit_globals, implicit_players) = implicit_default_variables(self.hir); - for declaration in &self.hir.declarations { - if let hir::Declaration::GlobalVariable { - name, - index: Some(index), - span, - .. - } = declaration - { - for (implicit_name, implicit_span) in &implicit_globals { - if default_var_index(implicit_name) == Some(*index) { - return Err(IntegrationError::new( - "index-collision", - format!( - "duplicate use of index {index} for global variables '{implicit_name}' and '{name}'" - ), - implicit_span.or(*span), - )); - } - } - } - if let hir::Declaration::PlayerVariable { - name, - index: Some(index), - span, - .. - } = declaration - { - for (implicit_name, implicit_span) in &implicit_players { - if implicit_player_index(implicit_name) == *index { - return Err(IntegrationError::new( - "index-collision", - format!( - "duplicate use of index {index} for player variables '{implicit_name}' and '{name}'" - ), - implicit_span.or(*span), - )); - } - } - } - } - - let globals = self - .hir - .declarations - .iter() - .filter_map(|declaration| match declaration { - hir::Declaration::GlobalVariable { index, span, .. } => Some((*index, *span)), - _ => None, - }) - .collect::>(); - let players = self - .hir - .declarations - .iter() - .filter_map(|declaration| match declaration { - hir::Declaration::PlayerVariable { index, span, .. } => Some((*index, *span)), - _ => None, - }) - .collect::>(); - let subroutines = self - .hir - .declarations - .iter() - .filter_map(|declaration| match declaration { - hir::Declaration::Subroutine { index, span, .. } => Some((*index, *span)), - _ => None, - }) - .collect::>(); - let implicit_reserved = implicit_globals - .keys() - .map(|name| default_var_index(name).expect("implicit default variable names resolve")) - .collect::>(); - let implicit_player_reserved = implicit_players - .keys() - .map(|name| implicit_player_index(name)) - .collect::>(); - let mut helper_reserved = implicit_reserved.clone(); - helper_reserved.extend(self.hir.declarations.iter().filter_map(|declaration| { - match declaration { - hir::Declaration::GlobalVariable { - index: Some(index), .. - } => Some(*index), - _ => None, - } - })); - let translation_helper_index = self.translation_helper_index(&helper_reserved)?; - let mut global_reserved = implicit_reserved; - if let Some(index) = translation_helper_index { - helper_reserved.insert(index); - global_reserved.insert(index); - } - let compression_alphabet_index = self.helper_global_index( - &helper_reserved, - "useVariableForCompressionAlphabet", - "no available global variable index remains for the compression alphabet", - )?; - if let Some(index) = compression_alphabet_index { - helper_reserved.insert(index); - global_reserved.insert(index); - } - let empty_string_index = self.helper_global_index( - &helper_reserved, - "replaceEmptyStringByVariable", - "no available global variable index remains for the empty-string replacement", - )?; - if let Some(index) = empty_string_index { - helper_reserved.insert(index); - global_reserved.insert(index); - } - let empty = HashSet::new(); - let mut globals = globals; - let mut players = players; - let mut explicit_globals = global_reserved.clone(); - explicit_globals.extend(globals.iter().filter_map(|(index, _)| *index)); - let mut explicit_players = implicit_player_reserved.clone(); - explicit_players.extend(players.iter().filter_map(|(index, _)| *index)); - let global_names = - self.hir - .declarations - .iter() - .filter_map(|declaration| match declaration { - hir::Declaration::GlobalVariable { name, .. } => Some(name.as_str()), - _ => None, - }); - top_allocate_reserved_names(global_names, &mut globals, &mut explicit_globals); - let player_names = - self.hir - .declarations - .iter() - .filter_map(|declaration| match declaration { - hir::Declaration::PlayerVariable { name, .. } => Some(name.as_str()), - _ => None, - }); - top_allocate_reserved_names(player_names, &mut players, &mut explicit_players); - let global_indices = allocate_indices(&globals, &global_reserved, "global variable")?; - let player_indices = - allocate_indices(&players, &implicit_player_reserved, "player variable")?; - let subroutine_indices = allocate_indices(&subroutines, &empty, "subroutine")?; - let mut global_index = 0; - let mut player_index = 0; - let mut subroutine_index = 0; - - // Declared variables in source order (for duplicate detection and - // initializer action order), then merged with the implicit default - // variables and created in Workshop index order so the emitted - // variable tables are reference-compatible. - let mut declared_globals: Vec<(&str, u32, Option, Option)> = Vec::new(); - let mut global_initializers = Vec::new(); - let mut declared_players: Vec<(&str, u32, Option, Option)> = Vec::new(); - let mut player_initializers = Vec::new(); - let mut declared_subroutines: Vec<(&str, u32, Option, Option)> = - Vec::new(); - - for declaration in &self.hir.declarations { - match declaration { - hir::Declaration::GlobalVariable { - name, - index: _, - span, - name_span, - initializer, - } => { - let assigned = global_indices[global_index]; - global_index += 1; - if declared_globals - .iter() - .any(|(existing, ..)| *existing == name) - { - return Err(IntegrationError::new( - "symbol-collision", - format!("duplicate global variable '{name}'"), - *span, - )); - } - declared_globals.push((name, assigned, *span, *name_span)); - if let Some(init) = initializer { - if !is_zero_initializer(init) { - global_initializers.push((name, init, *span, *name_span)); - } - } - } - hir::Declaration::PlayerVariable { - name, - index: _, - span, - name_span, - initializer, - } => { - let assigned = player_indices[player_index]; - player_index += 1; - if declared_players - .iter() - .any(|(existing, ..)| *existing == name) - { - return Err(IntegrationError::new( - "symbol-collision", - format!("duplicate player variable '{name}'"), - *span, - )); - } - declared_players.push((name, assigned, *span, *name_span)); - if let Some(init) = initializer { - if !is_zero_initializer(init) { - player_initializers.push((name, init, *span, *name_span)); - } - } - } - hir::Declaration::Subroutine { - name, - span, - name_span, - .. - } => { - let assigned = subroutine_indices[subroutine_index]; - subroutine_index += 1; - if declared_subroutines - .iter() - .any(|(existing, ..)| *existing == name) - { - return Err(IntegrationError::new( - "symbol-collision", - format!("duplicate subroutine '{name}'"), - *span, - )); - } - declared_subroutines.push((name, assigned, *span, *name_span)); - } - hir::Declaration::Constant { name, value, span } => { - if self.constants.insert(name.clone(), value).is_some() { - return Err(IntegrationError::new( - "symbol-collision", - format!("duplicate constant '{name}'"), - *span, - )); - } - } - hir::Declaration::Macro { .. } => {} - } - } - - let mut planned_globals: Vec<(String, u32, Option, Option)> = - declared_globals - .into_iter() - .map(|(name, index, span, name_span)| (name.to_string(), index, span, name_span)) - .collect(); - planned_globals.extend(implicit_globals.iter().map(|(name, span)| { - ( - name.clone(), - default_var_index(name).expect("implicit default variable names resolve"), - *span, - None, - ) - })); - if let Some(index) = translation_helper_index { - planned_globals.push((TRANSLATION_HELPER_NAME.to_string(), index, None, None)); - } - if let Some(index) = compression_alphabet_index { - planned_globals.push((COMPRESSION_ALPHABET_NAME.to_string(), index, None, None)); - } - if let Some(index) = empty_string_index { - planned_globals.push((EMPTY_STRING_NAME.to_string(), index, None, None)); - } - planned_globals.sort_by_key(|(_, index, ..)| *index); - for (name, assigned, span, name_span) in planned_globals { - let _ = (span, name_span); - let id = self.global_names.len(); - self.global_names.push(name.clone()); - self.globals.insert(name.clone(), id); - self.program - .global_variables - .push(workshop_rs::Variable::with_index(name, assigned)); - self.program - .set_global_variable_spans( - id, - self.workshop_span(span)?, - self.workshop_span(name_span)?, - ) - .map_err(|error| { - IntegrationError::new("provenance", error.to_string(), span.or(name_span)) - })?; - } - - let mut planned_players: Vec<(String, u32, Option, Option)> = - declared_players - .into_iter() - .map(|(name, index, span, name_span)| (name.to_string(), index, span, name_span)) - .collect(); - planned_players.extend( - implicit_players - .iter() - .map(|(name, span)| (name.clone(), implicit_player_index(name), *span, None)), - ); - planned_players.sort_by_key(|(_, index, ..)| *index); - for (name, assigned, span, name_span) in planned_players { - let _ = (span, name_span); - let id = self.player_names.len(); - self.player_names.push(name.clone()); - self.players.insert(name, id); - self.program - .player_variables - .push(workshop_rs::Variable::with_index( - self.player_names[id].clone(), - assigned, - )); - self.program - .set_player_variable_spans( - id, - self.workshop_span(span)?, - self.workshop_span(name_span)?, - ) - .map_err(|error| { - IntegrationError::new("provenance", error.to_string(), span.or(name_span)) - })?; - } - - declared_subroutines.sort_by_key(|(_, index, ..)| *index); - for (name, assigned, span, name_span) in declared_subroutines { - let _ = (span, name_span); - let id = self.subroutine_names.len(); - self.subroutine_names.push(name.to_string()); - self.subroutines.insert(name.to_string(), id); - self.program - .subroutines - .push(workshop_rs::Subroutine::with_index(name, assigned)); - self.program - .set_subroutine_spans( - id, - self.workshop_span(span)?, - self.workshop_span(name_span)?, - ) - .map_err(|error| { - IntegrationError::new("provenance", error.to_string(), span.or(name_span)) - })?; - } - - let empty_string_initializer = empty_string_index - .map(|_| { - let variable = *self - .globals - .get(EMPTY_STRING_NAME) - .expect("empty string helper variable is created"); - let empty_array = self.push_call("emptyArray", Vec::new()); - let null = self.push_value(Value::Null); - let value = self.push_call("charAt", vec![empty_array, null]); - let action = self.push_action(Action::SetGlobalVariable { - variable: self.global_names[variable].clone(), - value, - }); - Ok(action) - }) - .transpose()?; - - if has_directive(self.hir, "disableInspector") { - let action = self.push_call_action("disableInspector", &[]); - self.push_generated_rule("Disable inspector", Event::Global, vec![action])?; - } - let translation_initializer = self - .hir - .preprocessing - .translations - .as_ref() - .map(|translations| { - let variable = *self - .globals - .get(TRANSLATION_HELPER_NAME) - .expect("translation helper variable is created"); - let value = self.lower_translation_helper(translations)?; - let action = self.push_action(Action::SetGlobalVariable { - variable: self.global_names[variable].clone(), - value, - }); - self.mark_action_origins(std::slice::from_ref(&action), translations.span); - self.mark_action_argument_origins(action, [translations.span]); - Ok(action) - }) - .transpose()?; - - let compression_alphabet_initializer = compression_alphabet_index - .map(|_| { - let variable = *self - .globals - .get(COMPRESSION_ALPHABET_NAME) - .expect("compression alphabet variable is created"); - let value = self.lower_custom_string(compression_alphabet()); - let action = self.push_action(Action::SetGlobalVariable { - variable: self.global_names[variable].clone(), - value, - }); - Ok(action) - }) - .transpose()?; - - let (uses_player_translation_var, no_detection_rule, no_tl_err) = - self.translation_player_options(); - if uses_player_translation_var && !no_detection_rule { - let translations = self - .hir - .preprocessing - .translations - .clone() - .expect("player translation mode requires translations"); - self.lower_translation_detection_rule(no_tl_err, &translations)?; - } - - if translation_initializer.is_some() - || empty_string_initializer.is_some() - || compression_alphabet_initializer.is_some() - || !global_initializers.is_empty() - { - let mut actions = Vec::with_capacity( - global_initializers.len() - + usize::from(translation_initializer.is_some()) - + usize::from(empty_string_initializer.is_some()) - + usize::from(compression_alphabet_initializer.is_some()), - ); - if let Some(action) = translation_initializer { - actions.push(action); - } - if let Some(action) = compression_alphabet_initializer { - actions.push(action); - } - if let Some(action) = empty_string_initializer { - actions.push(action); - } - for (name, init_expr, span, _target_span) in global_initializers { - let variable = *self.globals.get(name).expect("declared global is created"); - let value = self.lower_value(init_expr)?; - let action = self.push_action(Action::SetGlobalVariable { - variable: self.global_names[variable].clone(), - value, - }); - self.mark_action_origins(std::slice::from_ref(&action), span); - self.mark_action_argument_origins(action, [init_expr.span().copied()]); - actions.push(action); - } - let rule_index = self.program.rules.len(); - self.program.rules.push(rule_from_parts( - self.global_initializer_rule_name(), - false, - workshop_rs::Event::Global, - Vec::new(), - self.public_actions(&actions), - )); - let action_provenance = self.action_provenance(&actions); - self.set_rule_provenance(rule_index, None, std::iter::empty(), action_provenance)?; - } - - if uses_player_translation_var || !player_initializers.is_empty() { - let mut actions = Vec::with_capacity( - player_initializers.len() + usize::from(uses_player_translation_var), - ); - if uses_player_translation_var { - let variable = *self - .players - .get("__languageIndex__") - .expect("translation player variable is created"); - let player = self.push_value(Value::EventPlayer); - let value = self.push_number(if no_tl_err { 0.1 } else { 1.1 }); - actions.push(self.push_action(Action::SetPlayerVariable { - player, - variable: self.player_names[variable].clone(), - value, - })); - } - for (name, init_expr, span, _target_span) in player_initializers { - let variable = *self - .players - .get(name) - .expect("declared player variable is created"); - let player = self.push_value(Value::EventPlayer); - let value = self.lower_value(init_expr)?; - let action = self.push_action(Action::SetPlayerVariable { - player, - variable: self.player_names[variable].clone(), - value, - }); - self.mark_action_origins(std::slice::from_ref(&action), span); - self.mark_action_argument_origins(action, [None, init_expr.span().copied()]); - actions.push(action); - } - let rule_index = self.program.rules.len(); - self.program.rules.push(rule_from_parts( - self.player_initializer_rule_name(), - false, - workshop_rs::Event::EachPlayer, - Vec::new(), - self.public_actions(&actions), - )); - let action_provenance = self.action_provenance(&actions); - self.set_rule_provenance(rule_index, None, std::iter::empty(), action_provenance)?; - } - - Ok(()) - } - - fn push_generated_rule( - &mut self, - name: &str, - event: Event, - actions: Vec, - ) -> Result<(), IntegrationError> { - let rule_index = self.program.rules.len(); - self.program.rules.push(rule_from_parts( - name.to_string(), - false, - event, - Vec::new(), - self.public_actions(&actions), - )); - self.set_rule_provenance( - rule_index, - None, - std::iter::empty(), - self.action_provenance(&actions), - ) - } - - fn translation_player_options(&self) -> (bool, bool, bool) { - let Some(directive) = self - .hir - .preprocessing - .directives - .iter() - .find(|directive| directive.name == "translateWithPlayerVar") - else { - return (false, false, false); - }; - let options = directive.value.as_deref().unwrap_or_default(); - ( - true, - options - .split_whitespace() - .any(|option| option == "noDetectionRule"), - options.split_whitespace().any(|option| option == "noTlErr"), - ) - } - - fn lower_translation_detection_rule( - &mut self, - no_tl_err: bool, - translations: &hir::TranslationState, - ) -> Result<(), IntegrationError> { - let variable = self - .players - .get("__languageIndex__") - .copied() - .expect("translation player variable is created"); - let player = self.push_value(Value::EventPlayer); - let language = self.push_value(Value::PlayerVariable { - player, - variable: self.player_names[variable].clone(), - }); - let initial = self.push_number(if no_tl_err { 0.1 } else { 1.1 }); - let has_spawned = self.push_call("hasSpawned", vec![player]); - let is_dummy = self.push_call("isDummy", vec![player]); - let false_value = self.push_value(Value::Bool(false)); - let not_dummy = self.push_call("==", vec![is_dummy, false_value]); - let initial_language = self.push_call("==", vec![language, initial]); - - let facing = self.push_call("getFacingDirection", vec![player]); - let append = self.push_action(Action::ModifyPlayerVariable { - player, - variable: self.player_names[variable].clone(), - op: ModifyOp::AppendToArray, - value: facing, - }); - let ten = self.push_number(10.0); - let direction_index = self.translation_language_index(translations)?; - let horizontal = self.push_call("multiply", vec![ten, direction_index]); - let vertical = self.push_number(5.0); - let direction = self.push_call("directionFromAngles", vec![horizontal, vertical]); - let turn_rate = self.push_number(999_999_999_999.0); - let to_world = self.push_value(Value::Enum { - value_type: "Relativity".to_string(), - value: "TO_WORLD".to_string(), - }); - let reevaluation = self.push_value(Value::Enum { - value_type: "FacingReeval".to_string(), - value: "DIRECTION_AND_TURN_RATE".to_string(), - }); - let start_facing = self.push_call_action( - "startFacing", - &[player, direction, turn_rate, to_world, reevaluation], - ); - - let horizontal_angle = self.push_call("getHorizontalFacingAngle", vec![player]); - let one_hundred = self.push_number(100.0); - let horizontal_times_hundred = - self.push_call("multiply", vec![horizontal_angle, one_hundred]); - let nearest = self.push_value(Value::Enum { - value_type: "Rounding".to_string(), - value: "NEAREST".to_string(), - }); - let rounded_horizontal = - self.push_call("roundToInteger", vec![horizontal_times_hundred, nearest]); - let thousand = self.push_number(1000.0); - let modulo = self.push_call("modulo", vec![rounded_horizontal, thousand]); - let zero = self.push_number(0.0); - let modulo_zero = self.push_call("not", vec![modulo]); - let vertical_angle = self.push_call("getVerticalFacingAngle", vec![player]); - let vertical_difference = self.push_call("subtract", vec![vertical_angle, vertical]); - let vertical_delta = self.push_call("absoluteValue", vec![vertical_difference]); - let tolerance = self.push_number(0.01); - let vertical_close = self.push_call("<", vec![vertical_delta, tolerance]); - let wait_condition = self.push_call("and", vec![modulo_zero, vertical_close]); - let timeout = self.push_number(15.0); - let wait = self.push_call_action("waitUntil", &[wait_condition, timeout]); - - let ten_for_angle = self.push_number(10.0); - let horizontal_divided = self.push_call("divide", vec![horizontal_angle, ten_for_angle]); - let rounded_angle = self.push_call("roundToInteger", vec![horizontal_divided, nearest]); - let vertical_difference = self.push_call("subtract", vec![vertical_angle, vertical]); - let vertical_delta = self.push_call("absoluteValue", vec![vertical_difference]); - let vertical_match = self.push_call("<", vec![vertical_delta, tolerance]); - let one = self.push_number(1.0); - let matched_language = self.push_call("multiply", vec![vertical_match, rounded_angle]); - let language_value = self.push_call("max", vec![one, matched_language]); - let set_index = self.push_call_action( - "setPlayerVariableAtIndex", - &[language, zero, language_value], - ); - let stop_facing = self.push_call_action("stopFacing", &[player]); - let last = self.push_call("lastOf", vec![language]); - let set_facing = self.push_call_action("setFacing", &[player, last, to_world]); - let finish = if no_tl_err { - self.push_action(Action::ModifyPlayerVariable { - player, - variable: self.player_names[variable].clone(), - op: ModifyOp::Subtract, - value: one, - }) - } else { - let final_value = self.push_call("firstOf", vec![language]); - self.push_action(Action::SetPlayerVariable { - player, - variable: self.player_names[variable].clone(), - value: final_value, - }) - }; - - let actions = [ - append, - start_facing, - wait, - set_index, - stop_facing, - set_facing, - finish, - ]; - let rule_index = self.program.rules.len(); - self.program.rules.push(rule_from_parts( - "OverPy translation setup - Determine the player's language".to_string(), - false, - Event::EachPlayer, - vec![ - workshop_rs::Condition::new(self.materialize_value(has_spawned)), - workshop_rs::Condition::new(self.materialize_value(not_dummy)), - workshop_rs::Condition::new(self.materialize_value(initial_language)), - ], - self.public_actions(&actions), - )); - self.set_rule_provenance( - rule_index, - None, - [None, None, None], - self.action_provenance(&actions), - )?; - Ok(()) - } - fn lower_array(&mut self, elements: Vec) -> ValueId { let name = if elements.is_empty() { "emptyArray" diff --git a/crates/opy-rs/src/compiler/lowering/declarations.rs b/crates/opy-rs/src/compiler/lowering/declarations.rs new file mode 100644 index 00000000..7f81d7ca --- /dev/null +++ b/crates/opy-rs/src/compiler/lowering/declarations.rs @@ -0,0 +1,720 @@ +use super::*; + +impl<'a> Lowering<'a> { + fn translation_helper_index( + &self, + reserved: &HashSet, + ) -> Result, IntegrationError> { + let Some(translations) = self.hir.preprocessing.translations.as_ref() else { + return Ok(None); + }; + self.free_global_index( + reserved, + translations.span, + "no available global variable index remains for translations", + ) + } + + fn helper_global_index( + &self, + reserved: &HashSet, + directive: &str, + message: &str, + ) -> Result, IntegrationError> { + let Some(source) = self + .hir + .preprocessing + .directives + .iter() + .find(|item| item.name == directive) + else { + return Ok(None); + }; + self.free_global_index(reserved, source.span, message) + } + + fn free_global_index( + &self, + reserved: &HashSet, + span: Option, + message: &str, + ) -> Result, IntegrationError> { + (0..=127) + .rev() + .find(|index| !reserved.contains(index)) + .map(Some) + .ok_or_else(|| IntegrationError::new("index-exhausted", message, span)) + } + + pub(in crate::compiler) fn lower_declarations(&mut self) -> Result<(), IntegrationError> { + let (implicit_globals, implicit_players) = implicit_default_variables(self.hir); + for declaration in &self.hir.declarations { + if let hir::Declaration::GlobalVariable { + name, + index: Some(index), + span, + .. + } = declaration + { + for (implicit_name, implicit_span) in &implicit_globals { + if default_var_index(implicit_name) == Some(*index) { + return Err(IntegrationError::new( + "index-collision", + format!( + "duplicate use of index {index} for global variables '{implicit_name}' and '{name}'" + ), + implicit_span.or(*span), + )); + } + } + } + if let hir::Declaration::PlayerVariable { + name, + index: Some(index), + span, + .. + } = declaration + { + for (implicit_name, implicit_span) in &implicit_players { + if implicit_player_index(implicit_name) == *index { + return Err(IntegrationError::new( + "index-collision", + format!( + "duplicate use of index {index} for player variables '{implicit_name}' and '{name}'" + ), + implicit_span.or(*span), + )); + } + } + } + } + + let globals = self + .hir + .declarations + .iter() + .filter_map(|declaration| match declaration { + hir::Declaration::GlobalVariable { index, span, .. } => Some((*index, *span)), + _ => None, + }) + .collect::>(); + let players = self + .hir + .declarations + .iter() + .filter_map(|declaration| match declaration { + hir::Declaration::PlayerVariable { index, span, .. } => Some((*index, *span)), + _ => None, + }) + .collect::>(); + let subroutines = self + .hir + .declarations + .iter() + .filter_map(|declaration| match declaration { + hir::Declaration::Subroutine { index, span, .. } => Some((*index, *span)), + _ => None, + }) + .collect::>(); + let implicit_reserved = implicit_globals + .keys() + .map(|name| default_var_index(name).expect("implicit default variable names resolve")) + .collect::>(); + let implicit_player_reserved = implicit_players + .keys() + .map(|name| implicit_player_index(name)) + .collect::>(); + let mut helper_reserved = implicit_reserved.clone(); + helper_reserved.extend(self.hir.declarations.iter().filter_map(|declaration| { + match declaration { + hir::Declaration::GlobalVariable { + index: Some(index), .. + } => Some(*index), + _ => None, + } + })); + let translation_helper_index = self.translation_helper_index(&helper_reserved)?; + let mut global_reserved = implicit_reserved; + if let Some(index) = translation_helper_index { + helper_reserved.insert(index); + global_reserved.insert(index); + } + let compression_alphabet_index = self.helper_global_index( + &helper_reserved, + "useVariableForCompressionAlphabet", + "no available global variable index remains for the compression alphabet", + )?; + if let Some(index) = compression_alphabet_index { + helper_reserved.insert(index); + global_reserved.insert(index); + } + let empty_string_index = self.helper_global_index( + &helper_reserved, + "replaceEmptyStringByVariable", + "no available global variable index remains for the empty-string replacement", + )?; + if let Some(index) = empty_string_index { + helper_reserved.insert(index); + global_reserved.insert(index); + } + let empty = HashSet::new(); + let mut globals = globals; + let mut players = players; + let mut explicit_globals = global_reserved.clone(); + explicit_globals.extend(globals.iter().filter_map(|(index, _)| *index)); + let mut explicit_players = implicit_player_reserved.clone(); + explicit_players.extend(players.iter().filter_map(|(index, _)| *index)); + let global_names = + self.hir + .declarations + .iter() + .filter_map(|declaration| match declaration { + hir::Declaration::GlobalVariable { name, .. } => Some(name.as_str()), + _ => None, + }); + top_allocate_reserved_names(global_names, &mut globals, &mut explicit_globals); + let player_names = + self.hir + .declarations + .iter() + .filter_map(|declaration| match declaration { + hir::Declaration::PlayerVariable { name, .. } => Some(name.as_str()), + _ => None, + }); + top_allocate_reserved_names(player_names, &mut players, &mut explicit_players); + let global_indices = allocate_indices(&globals, &global_reserved, "global variable")?; + let player_indices = + allocate_indices(&players, &implicit_player_reserved, "player variable")?; + let subroutine_indices = allocate_indices(&subroutines, &empty, "subroutine")?; + let mut global_index = 0; + let mut player_index = 0; + let mut subroutine_index = 0; + + // Declared variables in source order (for duplicate detection and + // initializer action order), then merged with the implicit default + // variables and created in Workshop index order so the emitted + // variable tables are reference-compatible. + let mut declared_globals: Vec<(&str, u32, Option, Option)> = Vec::new(); + let mut global_initializers = Vec::new(); + let mut declared_players: Vec<(&str, u32, Option, Option)> = Vec::new(); + let mut player_initializers = Vec::new(); + let mut declared_subroutines: Vec<(&str, u32, Option, Option)> = + Vec::new(); + + for declaration in &self.hir.declarations { + match declaration { + hir::Declaration::GlobalVariable { + name, + index: _, + span, + name_span, + initializer, + } => { + let assigned = global_indices[global_index]; + global_index += 1; + if declared_globals + .iter() + .any(|(existing, ..)| *existing == name) + { + return Err(IntegrationError::new( + "symbol-collision", + format!("duplicate global variable '{name}'"), + *span, + )); + } + declared_globals.push((name, assigned, *span, *name_span)); + if let Some(init) = initializer { + if !is_zero_initializer(init) { + global_initializers.push((name, init, *span, *name_span)); + } + } + } + hir::Declaration::PlayerVariable { + name, + index: _, + span, + name_span, + initializer, + } => { + let assigned = player_indices[player_index]; + player_index += 1; + if declared_players + .iter() + .any(|(existing, ..)| *existing == name) + { + return Err(IntegrationError::new( + "symbol-collision", + format!("duplicate player variable '{name}'"), + *span, + )); + } + declared_players.push((name, assigned, *span, *name_span)); + if let Some(init) = initializer { + if !is_zero_initializer(init) { + player_initializers.push((name, init, *span, *name_span)); + } + } + } + hir::Declaration::Subroutine { + name, + span, + name_span, + .. + } => { + let assigned = subroutine_indices[subroutine_index]; + subroutine_index += 1; + if declared_subroutines + .iter() + .any(|(existing, ..)| *existing == name) + { + return Err(IntegrationError::new( + "symbol-collision", + format!("duplicate subroutine '{name}'"), + *span, + )); + } + declared_subroutines.push((name, assigned, *span, *name_span)); + } + hir::Declaration::Constant { name, value, span } => { + if self.constants.insert(name.clone(), value).is_some() { + return Err(IntegrationError::new( + "symbol-collision", + format!("duplicate constant '{name}'"), + *span, + )); + } + } + hir::Declaration::Macro { .. } => {} + } + } + + let mut planned_globals: Vec<(String, u32, Option, Option)> = + declared_globals + .into_iter() + .map(|(name, index, span, name_span)| (name.to_string(), index, span, name_span)) + .collect(); + planned_globals.extend(implicit_globals.iter().map(|(name, span)| { + ( + name.clone(), + default_var_index(name).expect("implicit default variable names resolve"), + *span, + None, + ) + })); + if let Some(index) = translation_helper_index { + planned_globals.push((TRANSLATION_HELPER_NAME.to_string(), index, None, None)); + } + if let Some(index) = compression_alphabet_index { + planned_globals.push((COMPRESSION_ALPHABET_NAME.to_string(), index, None, None)); + } + if let Some(index) = empty_string_index { + planned_globals.push((EMPTY_STRING_NAME.to_string(), index, None, None)); + } + planned_globals.sort_by_key(|(_, index, ..)| *index); + for (name, assigned, span, name_span) in planned_globals { + let _ = (span, name_span); + let id = self.global_names.len(); + self.global_names.push(name.clone()); + self.globals.insert(name.clone(), id); + self.program + .global_variables + .push(workshop_rs::Variable::with_index(name, assigned)); + self.program + .set_global_variable_spans( + id, + self.workshop_span(span)?, + self.workshop_span(name_span)?, + ) + .map_err(|error| { + IntegrationError::new("provenance", error.to_string(), span.or(name_span)) + })?; + } + + let mut planned_players: Vec<(String, u32, Option, Option)> = + declared_players + .into_iter() + .map(|(name, index, span, name_span)| (name.to_string(), index, span, name_span)) + .collect(); + planned_players.extend( + implicit_players + .iter() + .map(|(name, span)| (name.clone(), implicit_player_index(name), *span, None)), + ); + planned_players.sort_by_key(|(_, index, ..)| *index); + for (name, assigned, span, name_span) in planned_players { + let _ = (span, name_span); + let id = self.player_names.len(); + self.player_names.push(name.clone()); + self.players.insert(name, id); + self.program + .player_variables + .push(workshop_rs::Variable::with_index( + self.player_names[id].clone(), + assigned, + )); + self.program + .set_player_variable_spans( + id, + self.workshop_span(span)?, + self.workshop_span(name_span)?, + ) + .map_err(|error| { + IntegrationError::new("provenance", error.to_string(), span.or(name_span)) + })?; + } + + declared_subroutines.sort_by_key(|(_, index, ..)| *index); + for (name, assigned, span, name_span) in declared_subroutines { + let _ = (span, name_span); + let id = self.subroutine_names.len(); + self.subroutine_names.push(name.to_string()); + self.subroutines.insert(name.to_string(), id); + self.program + .subroutines + .push(workshop_rs::Subroutine::with_index(name, assigned)); + self.program + .set_subroutine_spans( + id, + self.workshop_span(span)?, + self.workshop_span(name_span)?, + ) + .map_err(|error| { + IntegrationError::new("provenance", error.to_string(), span.or(name_span)) + })?; + } + + let empty_string_initializer = empty_string_index + .map(|_| { + let variable = *self + .globals + .get(EMPTY_STRING_NAME) + .expect("empty string helper variable is created"); + let empty_array = self.push_call("emptyArray", Vec::new()); + let null = self.push_value(Value::Null); + let value = self.push_call("charAt", vec![empty_array, null]); + let action = self.push_action(Action::SetGlobalVariable { + variable: self.global_names[variable].clone(), + value, + }); + Ok(action) + }) + .transpose()?; + + if has_directive(self.hir, "disableInspector") { + let action = self.push_call_action("disableInspector", &[]); + self.push_generated_rule("Disable inspector", Event::Global, vec![action])?; + } + let translation_initializer = self + .hir + .preprocessing + .translations + .as_ref() + .map(|translations| { + let variable = *self + .globals + .get(TRANSLATION_HELPER_NAME) + .expect("translation helper variable is created"); + let value = self.lower_translation_helper(translations)?; + let action = self.push_action(Action::SetGlobalVariable { + variable: self.global_names[variable].clone(), + value, + }); + self.mark_action_origins(std::slice::from_ref(&action), translations.span); + self.mark_action_argument_origins(action, [translations.span]); + Ok(action) + }) + .transpose()?; + + let compression_alphabet_initializer = compression_alphabet_index + .map(|_| { + let variable = *self + .globals + .get(COMPRESSION_ALPHABET_NAME) + .expect("compression alphabet variable is created"); + let value = self.lower_custom_string(compression_alphabet()); + let action = self.push_action(Action::SetGlobalVariable { + variable: self.global_names[variable].clone(), + value, + }); + Ok(action) + }) + .transpose()?; + + let (uses_player_translation_var, no_detection_rule, no_tl_err) = + self.translation_player_options(); + if uses_player_translation_var && !no_detection_rule { + let translations = self + .hir + .preprocessing + .translations + .clone() + .expect("player translation mode requires translations"); + self.lower_translation_detection_rule(no_tl_err, &translations)?; + } + + if translation_initializer.is_some() + || empty_string_initializer.is_some() + || compression_alphabet_initializer.is_some() + || !global_initializers.is_empty() + { + let mut actions = Vec::with_capacity( + global_initializers.len() + + usize::from(translation_initializer.is_some()) + + usize::from(empty_string_initializer.is_some()) + + usize::from(compression_alphabet_initializer.is_some()), + ); + if let Some(action) = translation_initializer { + actions.push(action); + } + if let Some(action) = compression_alphabet_initializer { + actions.push(action); + } + if let Some(action) = empty_string_initializer { + actions.push(action); + } + for (name, init_expr, span, _target_span) in global_initializers { + let variable = *self.globals.get(name).expect("declared global is created"); + let value = self.lower_value(init_expr)?; + let action = self.push_action(Action::SetGlobalVariable { + variable: self.global_names[variable].clone(), + value, + }); + self.mark_action_origins(std::slice::from_ref(&action), span); + self.mark_action_argument_origins(action, [init_expr.span().copied()]); + actions.push(action); + } + let rule_index = self.program.rules.len(); + self.program.rules.push(rule_from_parts( + self.global_initializer_rule_name(), + false, + workshop_rs::Event::Global, + Vec::new(), + self.public_actions(&actions), + )); + let action_provenance = self.action_provenance(&actions); + self.set_rule_provenance(rule_index, None, std::iter::empty(), action_provenance)?; + } + + if uses_player_translation_var || !player_initializers.is_empty() { + let mut actions = Vec::with_capacity( + player_initializers.len() + usize::from(uses_player_translation_var), + ); + if uses_player_translation_var { + let variable = *self + .players + .get("__languageIndex__") + .expect("translation player variable is created"); + let player = self.push_value(Value::EventPlayer); + let value = self.push_number(if no_tl_err { 0.1 } else { 1.1 }); + actions.push(self.push_action(Action::SetPlayerVariable { + player, + variable: self.player_names[variable].clone(), + value, + })); + } + for (name, init_expr, span, _target_span) in player_initializers { + let variable = *self + .players + .get(name) + .expect("declared player variable is created"); + let player = self.push_value(Value::EventPlayer); + let value = self.lower_value(init_expr)?; + let action = self.push_action(Action::SetPlayerVariable { + player, + variable: self.player_names[variable].clone(), + value, + }); + self.mark_action_origins(std::slice::from_ref(&action), span); + self.mark_action_argument_origins(action, [None, init_expr.span().copied()]); + actions.push(action); + } + let rule_index = self.program.rules.len(); + self.program.rules.push(rule_from_parts( + self.player_initializer_rule_name(), + false, + workshop_rs::Event::EachPlayer, + Vec::new(), + self.public_actions(&actions), + )); + let action_provenance = self.action_provenance(&actions); + self.set_rule_provenance(rule_index, None, std::iter::empty(), action_provenance)?; + } + + Ok(()) + } + + fn push_generated_rule( + &mut self, + name: &str, + event: Event, + actions: Vec, + ) -> Result<(), IntegrationError> { + let rule_index = self.program.rules.len(); + self.program.rules.push(rule_from_parts( + name.to_string(), + false, + event, + Vec::new(), + self.public_actions(&actions), + )); + self.set_rule_provenance( + rule_index, + None, + std::iter::empty(), + self.action_provenance(&actions), + ) + } + + pub(super) fn translation_player_options(&self) -> (bool, bool, bool) { + let Some(directive) = self + .hir + .preprocessing + .directives + .iter() + .find(|directive| directive.name == "translateWithPlayerVar") + else { + return (false, false, false); + }; + let options = directive.value.as_deref().unwrap_or_default(); + ( + true, + options + .split_whitespace() + .any(|option| option == "noDetectionRule"), + options.split_whitespace().any(|option| option == "noTlErr"), + ) + } + + fn lower_translation_detection_rule( + &mut self, + no_tl_err: bool, + translations: &hir::TranslationState, + ) -> Result<(), IntegrationError> { + let variable = self + .players + .get("__languageIndex__") + .copied() + .expect("translation player variable is created"); + let player = self.push_value(Value::EventPlayer); + let language = self.push_value(Value::PlayerVariable { + player, + variable: self.player_names[variable].clone(), + }); + let initial = self.push_number(if no_tl_err { 0.1 } else { 1.1 }); + let has_spawned = self.push_call("hasSpawned", vec![player]); + let is_dummy = self.push_call("isDummy", vec![player]); + let false_value = self.push_value(Value::Bool(false)); + let not_dummy = self.push_call("==", vec![is_dummy, false_value]); + let initial_language = self.push_call("==", vec![language, initial]); + + let facing = self.push_call("getFacingDirection", vec![player]); + let append = self.push_action(Action::ModifyPlayerVariable { + player, + variable: self.player_names[variable].clone(), + op: ModifyOp::AppendToArray, + value: facing, + }); + let ten = self.push_number(10.0); + let direction_index = self.translation_language_index(translations)?; + let horizontal = self.push_call("multiply", vec![ten, direction_index]); + let vertical = self.push_number(5.0); + let direction = self.push_call("directionFromAngles", vec![horizontal, vertical]); + let turn_rate = self.push_number(999_999_999_999.0); + let to_world = self.push_value(Value::Enum { + value_type: "Relativity".to_string(), + value: "TO_WORLD".to_string(), + }); + let reevaluation = self.push_value(Value::Enum { + value_type: "FacingReeval".to_string(), + value: "DIRECTION_AND_TURN_RATE".to_string(), + }); + let start_facing = self.push_call_action( + "startFacing", + &[player, direction, turn_rate, to_world, reevaluation], + ); + + let horizontal_angle = self.push_call("getHorizontalFacingAngle", vec![player]); + let one_hundred = self.push_number(100.0); + let horizontal_times_hundred = + self.push_call("multiply", vec![horizontal_angle, one_hundred]); + let nearest = self.push_value(Value::Enum { + value_type: "Rounding".to_string(), + value: "NEAREST".to_string(), + }); + let rounded_horizontal = + self.push_call("roundToInteger", vec![horizontal_times_hundred, nearest]); + let thousand = self.push_number(1000.0); + let modulo = self.push_call("modulo", vec![rounded_horizontal, thousand]); + let zero = self.push_number(0.0); + let modulo_zero = self.push_call("not", vec![modulo]); + let vertical_angle = self.push_call("getVerticalFacingAngle", vec![player]); + let vertical_difference = self.push_call("subtract", vec![vertical_angle, vertical]); + let vertical_delta = self.push_call("absoluteValue", vec![vertical_difference]); + let tolerance = self.push_number(0.01); + let vertical_close = self.push_call("<", vec![vertical_delta, tolerance]); + let wait_condition = self.push_call("and", vec![modulo_zero, vertical_close]); + let timeout = self.push_number(15.0); + let wait = self.push_call_action("waitUntil", &[wait_condition, timeout]); + + let ten_for_angle = self.push_number(10.0); + let horizontal_divided = self.push_call("divide", vec![horizontal_angle, ten_for_angle]); + let rounded_angle = self.push_call("roundToInteger", vec![horizontal_divided, nearest]); + let vertical_difference = self.push_call("subtract", vec![vertical_angle, vertical]); + let vertical_delta = self.push_call("absoluteValue", vec![vertical_difference]); + let vertical_match = self.push_call("<", vec![vertical_delta, tolerance]); + let one = self.push_number(1.0); + let matched_language = self.push_call("multiply", vec![vertical_match, rounded_angle]); + let language_value = self.push_call("max", vec![one, matched_language]); + let set_index = self.push_call_action( + "setPlayerVariableAtIndex", + &[language, zero, language_value], + ); + let stop_facing = self.push_call_action("stopFacing", &[player]); + let last = self.push_call("lastOf", vec![language]); + let set_facing = self.push_call_action("setFacing", &[player, last, to_world]); + let finish = if no_tl_err { + self.push_action(Action::ModifyPlayerVariable { + player, + variable: self.player_names[variable].clone(), + op: ModifyOp::Subtract, + value: one, + }) + } else { + let final_value = self.push_call("firstOf", vec![language]); + self.push_action(Action::SetPlayerVariable { + player, + variable: self.player_names[variable].clone(), + value: final_value, + }) + }; + + let actions = [ + append, + start_facing, + wait, + set_index, + stop_facing, + set_facing, + finish, + ]; + let rule_index = self.program.rules.len(); + self.program.rules.push(rule_from_parts( + "OverPy translation setup - Determine the player's language".to_string(), + false, + Event::EachPlayer, + vec![ + workshop_rs::Condition::new(self.materialize_value(has_spawned)), + workshop_rs::Condition::new(self.materialize_value(not_dummy)), + workshop_rs::Condition::new(self.materialize_value(initial_language)), + ], + self.public_actions(&actions), + )); + self.set_rule_provenance( + rule_index, + None, + [None, None, None], + self.action_provenance(&actions), + )?; + Ok(()) + } +} From a038c2aa47c6da8a03c8113099b8aa40e873b83d Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 05:17:33 +0800 Subject: [PATCH 31/67] refactor(opy-rs): combine declaration allocation scans --- .../src/compiler/lowering/declarations.rs | 121 +++++++----------- 1 file changed, 46 insertions(+), 75 deletions(-) diff --git a/crates/opy-rs/src/compiler/lowering/declarations.rs b/crates/opy-rs/src/compiler/lowering/declarations.rs index 7f81d7ca..6cbc838b 100644 --- a/crates/opy-rs/src/compiler/lowering/declarations.rs +++ b/crates/opy-rs/src/compiler/lowering/declarations.rs @@ -48,74 +48,6 @@ impl<'a> Lowering<'a> { pub(in crate::compiler) fn lower_declarations(&mut self) -> Result<(), IntegrationError> { let (implicit_globals, implicit_players) = implicit_default_variables(self.hir); - for declaration in &self.hir.declarations { - if let hir::Declaration::GlobalVariable { - name, - index: Some(index), - span, - .. - } = declaration - { - for (implicit_name, implicit_span) in &implicit_globals { - if default_var_index(implicit_name) == Some(*index) { - return Err(IntegrationError::new( - "index-collision", - format!( - "duplicate use of index {index} for global variables '{implicit_name}' and '{name}'" - ), - implicit_span.or(*span), - )); - } - } - } - if let hir::Declaration::PlayerVariable { - name, - index: Some(index), - span, - .. - } = declaration - { - for (implicit_name, implicit_span) in &implicit_players { - if implicit_player_index(implicit_name) == *index { - return Err(IntegrationError::new( - "index-collision", - format!( - "duplicate use of index {index} for player variables '{implicit_name}' and '{name}'" - ), - implicit_span.or(*span), - )); - } - } - } - } - - let globals = self - .hir - .declarations - .iter() - .filter_map(|declaration| match declaration { - hir::Declaration::GlobalVariable { index, span, .. } => Some((*index, *span)), - _ => None, - }) - .collect::>(); - let players = self - .hir - .declarations - .iter() - .filter_map(|declaration| match declaration { - hir::Declaration::PlayerVariable { index, span, .. } => Some((*index, *span)), - _ => None, - }) - .collect::>(); - let subroutines = self - .hir - .declarations - .iter() - .filter_map(|declaration| match declaration { - hir::Declaration::Subroutine { index, span, .. } => Some((*index, *span)), - _ => None, - }) - .collect::>(); let implicit_reserved = implicit_globals .keys() .map(|name| default_var_index(name).expect("implicit default variable names resolve")) @@ -125,14 +57,55 @@ impl<'a> Lowering<'a> { .map(|name| implicit_player_index(name)) .collect::>(); let mut helper_reserved = implicit_reserved.clone(); - helper_reserved.extend(self.hir.declarations.iter().filter_map(|declaration| { + let mut globals = Vec::new(); + let mut players = Vec::new(); + let mut subroutines = Vec::new(); + for declaration in &self.hir.declarations { match declaration { hir::Declaration::GlobalVariable { - index: Some(index), .. - } => Some(*index), - _ => None, + name, index, span, .. + } => { + if let Some(index) = index { + for (implicit_name, implicit_span) in &implicit_globals { + if default_var_index(implicit_name) == Some(*index) { + return Err(IntegrationError::new( + "index-collision", + format!( + "duplicate use of index {index} for global variables '{implicit_name}' and '{name}'" + ), + implicit_span.or(*span), + )); + } + } + helper_reserved.insert(*index); + } + globals.push((*index, *span)); + } + hir::Declaration::PlayerVariable { + name, index, span, .. + } => { + if let Some(index) = index { + for (implicit_name, implicit_span) in &implicit_players { + if implicit_player_index(implicit_name) == *index { + return Err(IntegrationError::new( + "index-collision", + format!( + "duplicate use of index {index} for player variables '{implicit_name}' and '{name}'" + ), + implicit_span.or(*span), + )); + } + } + } + players.push((*index, *span)); + } + hir::Declaration::Subroutine { index, span, .. } => { + subroutines.push((*index, *span)); + } + hir::Declaration::Constant { .. } | hir::Declaration::Macro { .. } => {} } - })); + } + let translation_helper_index = self.translation_helper_index(&helper_reserved)?; let mut global_reserved = implicit_reserved; if let Some(index) = translation_helper_index { @@ -158,8 +131,6 @@ impl<'a> Lowering<'a> { global_reserved.insert(index); } let empty = HashSet::new(); - let mut globals = globals; - let mut players = players; let mut explicit_globals = global_reserved.clone(); explicit_globals.extend(globals.iter().filter_map(|(index, _)| *index)); let mut explicit_players = implicit_player_reserved.clone(); From ff5e84c52153053033d243001748d88095719483 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 05:38:53 +0800 Subject: [PATCH 32/67] refactor(opy-rs): streamline declaration planning --- crates/opy-rs/src/compiler/lowering.rs | 12 +++++ .../src/compiler/lowering/declarations.rs | 54 +++++++------------ 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/crates/opy-rs/src/compiler/lowering.rs b/crates/opy-rs/src/compiler/lowering.rs index 35dbd1cf..0322bed5 100644 --- a/crates/opy-rs/src/compiler/lowering.rs +++ b/crates/opy-rs/src/compiler/lowering.rs @@ -1102,6 +1102,18 @@ fn top_allocate_reserved_names<'a>( } } +fn allocate_named_indices<'a>( + names: impl Iterator, + entries: &mut [(Option, Option)], + pre_reserved: &HashSet, + kind: &str, +) -> Result, IntegrationError> { + let mut reserved = pre_reserved.clone(); + reserved.extend(entries.iter().filter_map(|(index, _)| *index)); + top_allocate_reserved_names(names, entries, &mut reserved); + allocate_indices(entries, pre_reserved, kind) +} + fn allocate_indices( entries: &[(Option, Option)], pre_reserved: &HashSet, diff --git a/crates/opy-rs/src/compiler/lowering/declarations.rs b/crates/opy-rs/src/compiler/lowering/declarations.rs index 6cbc838b..5011290f 100644 --- a/crates/opy-rs/src/compiler/lowering/declarations.rs +++ b/crates/opy-rs/src/compiler/lowering/declarations.rs @@ -131,31 +131,30 @@ impl<'a> Lowering<'a> { global_reserved.insert(index); } let empty = HashSet::new(); - let mut explicit_globals = global_reserved.clone(); - explicit_globals.extend(globals.iter().filter_map(|(index, _)| *index)); - let mut explicit_players = implicit_player_reserved.clone(); - explicit_players.extend(players.iter().filter_map(|(index, _)| *index)); - let global_names = + let global_indices = allocate_named_indices( self.hir .declarations .iter() .filter_map(|declaration| match declaration { hir::Declaration::GlobalVariable { name, .. } => Some(name.as_str()), _ => None, - }); - top_allocate_reserved_names(global_names, &mut globals, &mut explicit_globals); - let player_names = + }), + &mut globals, + &global_reserved, + "global variable", + )?; + let player_indices = allocate_named_indices( self.hir .declarations .iter() .filter_map(|declaration| match declaration { hir::Declaration::PlayerVariable { name, .. } => Some(name.as_str()), _ => None, - }); - top_allocate_reserved_names(player_names, &mut players, &mut explicit_players); - let global_indices = allocate_indices(&globals, &global_reserved, "global variable")?; - let player_indices = - allocate_indices(&players, &implicit_player_reserved, "player variable")?; + }), + &mut players, + &implicit_player_reserved, + "player variable", + )?; let subroutine_indices = allocate_indices(&subroutines, &empty, "subroutine")?; let mut global_index = 0; let mut player_index = 0; @@ -165,9 +164,9 @@ impl<'a> Lowering<'a> { // initializer action order), then merged with the implicit default // variables and created in Workshop index order so the emitted // variable tables are reference-compatible. - let mut declared_globals: Vec<(&str, u32, Option, Option)> = Vec::new(); + let mut planned_globals: Vec<(String, u32, Option, Option)> = Vec::new(); let mut global_initializers = Vec::new(); - let mut declared_players: Vec<(&str, u32, Option, Option)> = Vec::new(); + let mut planned_players: Vec<(String, u32, Option, Option)> = Vec::new(); let mut player_initializers = Vec::new(); let mut declared_subroutines: Vec<(&str, u32, Option, Option)> = Vec::new(); @@ -183,9 +182,9 @@ impl<'a> Lowering<'a> { } => { let assigned = global_indices[global_index]; global_index += 1; - if declared_globals + if planned_globals .iter() - .any(|(existing, ..)| *existing == name) + .any(|(existing, ..)| existing == name) { return Err(IntegrationError::new( "symbol-collision", @@ -193,7 +192,7 @@ impl<'a> Lowering<'a> { *span, )); } - declared_globals.push((name, assigned, *span, *name_span)); + planned_globals.push((name.clone(), assigned, *span, *name_span)); if let Some(init) = initializer { if !is_zero_initializer(init) { global_initializers.push((name, init, *span, *name_span)); @@ -209,9 +208,9 @@ impl<'a> Lowering<'a> { } => { let assigned = player_indices[player_index]; player_index += 1; - if declared_players + if planned_players .iter() - .any(|(existing, ..)| *existing == name) + .any(|(existing, ..)| existing == name) { return Err(IntegrationError::new( "symbol-collision", @@ -219,7 +218,7 @@ impl<'a> Lowering<'a> { *span, )); } - declared_players.push((name, assigned, *span, *name_span)); + planned_players.push((name.clone(), assigned, *span, *name_span)); if let Some(init) = initializer { if !is_zero_initializer(init) { player_initializers.push((name, init, *span, *name_span)); @@ -259,11 +258,6 @@ impl<'a> Lowering<'a> { } } - let mut planned_globals: Vec<(String, u32, Option, Option)> = - declared_globals - .into_iter() - .map(|(name, index, span, name_span)| (name.to_string(), index, span, name_span)) - .collect(); planned_globals.extend(implicit_globals.iter().map(|(name, span)| { ( name.clone(), @@ -283,7 +277,6 @@ impl<'a> Lowering<'a> { } planned_globals.sort_by_key(|(_, index, ..)| *index); for (name, assigned, span, name_span) in planned_globals { - let _ = (span, name_span); let id = self.global_names.len(); self.global_names.push(name.clone()); self.globals.insert(name.clone(), id); @@ -301,11 +294,6 @@ impl<'a> Lowering<'a> { })?; } - let mut planned_players: Vec<(String, u32, Option, Option)> = - declared_players - .into_iter() - .map(|(name, index, span, name_span)| (name.to_string(), index, span, name_span)) - .collect(); planned_players.extend( implicit_players .iter() @@ -313,7 +301,6 @@ impl<'a> Lowering<'a> { ); planned_players.sort_by_key(|(_, index, ..)| *index); for (name, assigned, span, name_span) in planned_players { - let _ = (span, name_span); let id = self.player_names.len(); self.player_names.push(name.clone()); self.players.insert(name, id); @@ -336,7 +323,6 @@ impl<'a> Lowering<'a> { declared_subroutines.sort_by_key(|(_, index, ..)| *index); for (name, assigned, span, name_span) in declared_subroutines { - let _ = (span, name_span); let id = self.subroutine_names.len(); self.subroutine_names.push(name.to_string()); self.subroutines.insert(name.to_string(), id); From a78de2584b33e40dfea9b268af65b2ad53cea7c4 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 05:52:24 +0800 Subject: [PATCH 33/67] refactor(opy-rs): mutate expanded hir in place --- crates/opy-rs/src/compiler/backend.rs | 509 +++++++++++--------------- 1 file changed, 209 insertions(+), 300 deletions(-) diff --git a/crates/opy-rs/src/compiler/backend.rs b/crates/opy-rs/src/compiler/backend.rs index 11793d4d..c1f61d7a 100644 --- a/crates/opy-rs/src/compiler/backend.rs +++ b/crates/opy-rs/src/compiler/backend.rs @@ -79,7 +79,7 @@ fn expand_macros_with( hir::Declaration::GlobalVariable { initializer, .. } | hir::Declaration::PlayerVariable { initializer, .. } => { if let Some(initializer) = initializer { - **initializer = expander.expand_expr(initializer, &bindings, None)?; + expander.expand_expr_in_place(initializer, &bindings, None)?; } } _ => {} @@ -89,15 +89,15 @@ fn expand_macros_with( match entry { RuleEntry::Rule(rule) => { for argument in &mut rule.event.args { - *argument = expander.expand_expr(argument, &bindings, None)?; + expander.expand_expr_in_place(argument, &bindings, None)?; } for condition in &mut rule.conditions { - *condition = expander.expand_expr(condition, &bindings, None)?; + expander.expand_expr_in_place(condition, &bindings, None)?; } - rule.actions = expander.expand_stmts(&rule.actions, &bindings, None)?; + expander.expand_stmts(&mut rule.actions, &bindings, None)?; } RuleEntry::SubroutineDef { body, .. } => { - *body = expander.expand_stmts(body, &bindings, None)?; + expander.expand_stmts(body, &bindings, None)?; } } } @@ -107,156 +107,103 @@ fn expand_macros_with( impl MacroExpander { fn expand_stmts( &mut self, - statements: &[Stmt], + statements: &mut Vec, bindings: &MacroBindings, site: Option, - ) -> Result, IntegrationError> { + ) -> Result<(), IntegrationError> { let mut expanded = Vec::new(); - for statement in statements { - if let Stmt::Expr { expr, .. } = statement { - if let Expr::MacroCall { name, args, span } = expr.as_ref() { - let args = args - .iter() - .map(|arg| self.expand_expr(arg, bindings, site)) - .collect::, _>>()?; - expanded.extend(self.expand_macro_body(name, &args, *span, site)?); + for mut statement in statements.drain(..) { + if let Stmt::Expr { expr, .. } = &mut statement { + if let Expr::MacroCall { name, args, span } = expr.as_mut() { + for argument in args.iter_mut() { + self.expand_expr_in_place(argument, bindings, site)?; + } + expanded.extend(self.expand_macro_body(name, args, *span, site)?); continue; } } - expanded.push(self.expand_stmt(statement, bindings, site)?); - } - Ok(expanded) - } - - fn expand_stmt( - &mut self, - statement: &Stmt, - bindings: &MacroBindings, - site: Option, - ) -> Result { - let mut expanded = self.expand_stmt_inner(statement, bindings, site)?; - if let Some(site) = site { - set_stmt_span(&mut expanded, site); + self.expand_stmt_inner(&mut statement, bindings, site)?; + if let Some(site) = site { + set_stmt_span(&mut statement, site); + } + expanded.push(statement); } - Ok(expanded) + *statements = expanded; + Ok(()) } fn expand_stmt_inner( &mut self, - statement: &Stmt, + statement: &mut Stmt, bindings: &MacroBindings, site: Option, - ) -> Result { - Ok(match statement { - Stmt::Expr { expr, span } => Stmt::Expr { - expr: Box::new(self.expand_expr(expr, bindings, site)?), - span: *span, - }, - Stmt::Assign { - target, - value, - span, - } => Stmt::Assign { - target: Box::new(self.expand_expr(target, bindings, site)?), - value: Box::new(self.expand_expr(value, bindings, site)?), - span: *span, - }, + ) -> Result<(), IntegrationError> { + match statement { + Stmt::Expr { expr, .. } => self.expand_expr_in_place(expr, bindings, site)?, + Stmt::Assign { target, value, .. } => { + self.expand_expr_in_place(target, bindings, site)?; + self.expand_expr_in_place(value, bindings, site)?; + } Stmt::If { - branches, - r#else, - span, - } => Stmt::If { - branches: branches - .iter() - .map(|branch| { - Ok(hir::types::IfBranch { - condition: Box::new(self.expand_expr( - &branch.condition, - bindings, - site, - )?), - body: self.expand_stmts(&branch.body, bindings, site)?, - }) - }) - .collect::, IntegrationError>>()?, - r#else: r#else - .as_ref() - .map(|body| self.expand_stmts(body, bindings, site)) - .transpose()?, - span: *span, - }, + branches, r#else, .. + } => { + for branch in branches { + self.expand_expr_in_place(&mut branch.condition, bindings, site)?; + self.expand_stmts(&mut branch.body, bindings, site)?; + } + if let Some(body) = r#else { + self.expand_stmts(body, bindings, site)?; + } + } Stmt::For { variable, iterable, body, - span, - } => Stmt::For { - variable: Box::new(self.expand_expr(variable, bindings, site)?), - iterable: Box::new(self.expand_expr(iterable, bindings, site)?), - body: self.expand_stmts(body, bindings, site)?, - span: *span, - }, + .. + } => { + self.expand_expr_in_place(variable, bindings, site)?; + self.expand_expr_in_place(iterable, bindings, site)?; + self.expand_stmts(body, bindings, site)?; + } Stmt::While { - condition, - body, - span, - } => Stmt::While { - condition: Box::new(self.expand_expr(condition, bindings, site)?), - body: self.expand_stmts(body, bindings, site)?, - span: *span, - }, - Stmt::DoWhile { - condition, - body, - span, - } => Stmt::DoWhile { - condition: Box::new(self.expand_expr(condition, bindings, site)?), - body: self.expand_stmts(body, bindings, site)?, - span: *span, - }, - Stmt::Switch { value, arms, span } => Stmt::Switch { - value: Box::new(self.expand_expr(value, bindings, site)?), - arms: arms - .iter() - .map(|arm| match arm { - SwitchArm::Case { value, body, span } => Ok(SwitchArm::Case { - value: Box::new(self.expand_expr(value, bindings, site)?), - body: self.expand_stmts(body, bindings, site)?, - span: site.or(*span), - }), - SwitchArm::Default { body, span } => Ok(SwitchArm::Default { - body: self.expand_stmts(body, bindings, site)?, - span: site.or(*span), - }), - }) - .collect::, IntegrationError>>()?, - span: *span, - }, - Stmt::Delete { target, span } => Stmt::Delete { - target: Box::new(self.expand_expr(target, bindings, site)?), - span: *span, - }, - Stmt::Goto { - label, - offset, - rule_start, - span, - } => Stmt::Goto { - label: label.clone(), - offset: offset - .as_ref() - .map(|offset| self.expand_expr(offset, bindings, site).map(Box::new)) - .transpose()?, - rule_start: *rule_start, - span: *span, - }, + condition, body, .. + } + | Stmt::DoWhile { + condition, body, .. + } => { + self.expand_expr_in_place(condition, bindings, site)?; + self.expand_stmts(body, bindings, site)?; + } + Stmt::Switch { value, arms, .. } => { + self.expand_expr_in_place(value, bindings, site)?; + for arm in arms { + match arm { + SwitchArm::Case { value, body, span } => { + self.expand_expr_in_place(value, bindings, site)?; + self.expand_stmts(body, bindings, site)?; + *span = site.or(*span); + } + SwitchArm::Default { body, span } => { + self.expand_stmts(body, bindings, site)?; + *span = site.or(*span); + } + } + } + } + Stmt::Delete { target, .. } => self.expand_expr_in_place(target, bindings, site)?, + Stmt::Goto { offset, .. } => { + if let Some(offset) = offset { + self.expand_expr_in_place(offset, bindings, site)?; + } + } Stmt::Break { .. } | Stmt::Return { .. } - | Stmt::CallSubroutine { .. } - | Stmt::Pass { .. } | Stmt::Continue { .. } - | Stmt::Label { .. } => statement.clone(), - }) + | Stmt::Label { .. } + | Stmt::CallSubroutine { .. } + | Stmt::Pass { .. } => {} + } + Ok(()) } pub(crate) fn expand_expr( @@ -265,20 +212,18 @@ impl MacroExpander { bindings: &MacroBindings, site: Option, ) -> Result { - let mut expanded = self.expand_expr_inner(expression, bindings, site)?; - if let Some(site) = site { - set_expr_span(&mut expanded, site); - } + let mut expanded = expression.clone(); + self.expand_expr_in_place(&mut expanded, bindings, site)?; Ok(expanded) } - fn expand_expr_inner( + fn expand_expr_in_place( &mut self, - expression: &Expr, + expression: &mut Expr, bindings: &MacroBindings, site: Option, - ) -> Result { - match expression { + ) -> Result<(), IntegrationError> { + let replacement = match expression { Expr::MacroParam { name, span } => { let value = bindings.get(name).ok_or_else(|| { IntegrationError::new( @@ -287,18 +232,17 @@ impl MacroExpander { *span, ) })?; - if site.is_some() { - self.expand_expr(value, bindings, site) + Some(if site.is_some() { + self.expand_expr(value, bindings, site)? } else { - Ok(value.clone()) - } + value.clone() + }) } Expr::MacroCall { name, args, span } => { - let args = args - .iter() - .map(|arg| self.expand_expr(arg, bindings, site)) - .collect::, _>>()?; - let body = self.expand_macro_body(name, &args, *span, site)?; + for argument in args.iter_mut() { + self.expand_expr_in_place(argument, bindings, site)?; + } + let body = self.expand_macro_body(name, args, *span, site)?; if body.len() != 1 { return Err(IntegrationError::new( "macro-invalid", @@ -306,174 +250,139 @@ impl MacroExpander { *span, )); } - match body.into_iter().next().expect("one macro body statement") { - Stmt::Expr { expr, .. } => Ok(*expr), - _ => Err(IntegrationError::new( - "macro-invalid", - format!("macro '{name}' must produce an expression in value position"), - *span, - )), + Some( + match body.into_iter().next().expect("one macro body statement") { + Stmt::Expr { expr, .. } => *expr, + _ => { + return Err(IntegrationError::new( + "macro-invalid", + format!( + "macro '{name}' must produce an expression in value position" + ), + *span, + )); + } + }, + ) + } + _ => None, + }; + if let Some(replacement) = replacement { + *expression = replacement; + if let Some(site) = site { + set_expr_span(expression, site); + } + return Ok(()); + } + + match expression { + Expr::Array { elements, .. } => { + for element in elements { + self.expand_expr_in_place(element, bindings, site)?; + } + } + Expr::Dict { entries, .. } => { + for entry in entries { + self.expand_expr_in_place(&mut entry.key, bindings, site)?; + self.expand_expr_in_place(&mut entry.value, bindings, site)?; + entry.span = site.or(entry.span); } } - Expr::Array { elements, span } => Ok(Expr::Array { - elements: elements - .iter() - .map(|element| self.expand_expr(element, bindings, site)) - .collect::, _>>()?, - span: *span, - }), - Expr::Dict { entries, span } => Ok(Expr::Dict { - entries: entries - .iter() - .map(|entry| { - Ok(hir::DictEntry { - key: Box::new(self.expand_expr(&entry.key, bindings, site)?), - value: Box::new(self.expand_expr(&entry.value, bindings, site)?), - span: site.or(entry.span), - }) - }) - .collect::, IntegrationError>>()?, - span: *span, - }), Expr::Comprehension { element, - variable, variable_span, - index, index_span, iterable, condition, - span, - } => Ok(Expr::Comprehension { - element: Box::new(self.expand_expr(element, bindings, site)?), - variable: variable.clone(), - variable_span: site.or(*variable_span), - index: index.clone(), - index_span: site.or(*index_span), - iterable: Box::new(self.expand_expr(iterable, bindings, site)?), - condition: condition - .as_ref() - .map(|condition| self.expand_expr(condition, bindings, site).map(Box::new)) - .transpose()?, - span: *span, - }), + .. + } => { + self.expand_expr_in_place(element, bindings, site)?; + self.expand_expr_in_place(iterable, bindings, site)?; + if let Some(condition) = condition { + self.expand_expr_in_place(condition, bindings, site)?; + } + *variable_span = site.or(*variable_span); + *index_span = site.or(*index_span); + } Expr::Lambda { - params, - param_spans, - body, - span, - } => Ok(Expr::Lambda { - params: params.clone(), - param_spans: param_spans.iter().map(|span| site.or(*span)).collect(), - body: Box::new(self.expand_expr(body, bindings, site)?), - span: *span, - }), - Expr::Type { name, args, span } => Ok(Expr::Type { - name: name.clone(), - args: args - .iter() - .map(|arg| self.expand_expr(arg, bindings, site)) - .collect::, _>>()?, - span: *span, - }), - Expr::Vector { x, y, z, span } => Ok(Expr::Vector { - x: Box::new(self.expand_expr(x, bindings, site)?), - y: Box::new(self.expand_expr(y, bindings, site)?), - z: Box::new(self.expand_expr(z, bindings, site)?), - span: *span, - }), + param_spans, body, .. + } => { + self.expand_expr_in_place(body, bindings, site)?; + for span in param_spans { + *span = site.or(*span); + } + } + Expr::Type { args, .. } | Expr::Call { args, .. } | Expr::Format { args, .. } => { + for argument in args { + self.expand_expr_in_place(argument, bindings, site)?; + } + } + Expr::Vector { x, y, z, .. } => { + self.expand_expr_in_place(x, bindings, site)?; + self.expand_expr_in_place(y, bindings, site)?; + self.expand_expr_in_place(z, bindings, site)?; + } Expr::PlayerVar { player, - name, member_span, - span, - } => Ok(Expr::PlayerVar { - player: Box::new(self.expand_expr(player, bindings, site)?), - name: name.clone(), - member_span: site.or(*member_span), - span: *span, - }), + .. + } => { + self.expand_expr_in_place(player, bindings, site)?; + *member_span = site.or(*member_span); + } Expr::Member { receiver, - member, member_span, - span, - } => Ok(Expr::Member { - receiver: Box::new(self.expand_expr(receiver, bindings, site)?), - member: member.clone(), - member_span: site.or(*member_span), - span: *span, - }), - Expr::Call { - name, - args, - debug_source, - span, - } => Ok(Expr::Call { - name: name.clone(), - args: args - .iter() - .map(|arg| self.expand_expr(arg, bindings, site)) - .collect::, _>>()?, - debug_source: debug_source.clone(), - span: *span, - }), - Expr::ReceiverCall { - receiver, - name, - args, - span, - } => Ok(Expr::ReceiverCall { - receiver: Box::new(self.expand_expr(receiver, bindings, site)?), - name: name.clone(), - args: args - .iter() - .map(|arg| self.expand_expr(arg, bindings, site)) - .collect::, _>>()?, - span: *span, - }), - Expr::Binary { - op, - left, - right, - span, - } => Ok(Expr::Binary { - op: op.clone(), - left: Box::new(self.expand_expr(left, bindings, site)?), - right: Box::new(self.expand_expr(right, bindings, site)?), - span: *span, - }), + .. + } => { + self.expand_expr_in_place(receiver, bindings, site)?; + *member_span = site.or(*member_span); + } + Expr::ReceiverCall { receiver, args, .. } => { + self.expand_expr_in_place(receiver, bindings, site)?; + for argument in args { + self.expand_expr_in_place(argument, bindings, site)?; + } + } + Expr::Binary { left, right, .. } => { + self.expand_expr_in_place(left, bindings, site)?; + self.expand_expr_in_place(right, bindings, site)?; + } Expr::Conditional { then_value, condition, else_value, - span, - } => Ok(Expr::Conditional { - then_value: Box::new(self.expand_expr(then_value, bindings, site)?), - condition: Box::new(self.expand_expr(condition, bindings, site)?), - else_value: Box::new(self.expand_expr(else_value, bindings, site)?), - span: *span, - }), - Expr::Unary { op, operand, span } => Ok(Expr::Unary { - op: op.clone(), - operand: Box::new(self.expand_expr(operand, bindings, site)?), - span: *span, - }), - Expr::Index { array, index, span } => Ok(Expr::Index { - array: Box::new(self.expand_expr(array, bindings, site)?), - index: Box::new(self.expand_expr(index, bindings, site)?), - span: *span, - }), - Expr::Format { text, args, span } => Ok(Expr::Format { - text: text.clone(), - args: args - .iter() - .map(|arg| self.expand_expr(arg, bindings, site)) - .collect::, _>>()?, - span: *span, - }), - _ => Ok(expression.clone()), + .. + } => { + self.expand_expr_in_place(then_value, bindings, site)?; + self.expand_expr_in_place(condition, bindings, site)?; + self.expand_expr_in_place(else_value, bindings, site)?; + } + Expr::Unary { operand, .. } => { + self.expand_expr_in_place(operand, bindings, site)?; + } + Expr::Index { array, index, .. } => { + self.expand_expr_in_place(array, bindings, site)?; + self.expand_expr_in_place(index, bindings, site)?; + } + Expr::Number { .. } + | Expr::String { .. } + | Expr::Bool { .. } + | Expr::Null { .. } + | Expr::StringModifier { .. } + | Expr::Local { .. } + | Expr::Enum { .. } + | Expr::GlobalVar { .. } + | Expr::HostPlayer { .. } + | Expr::EventPlayer { .. } + | Expr::Constant { .. } + | Expr::MacroCall { .. } + | Expr::MacroParam { .. } => {} + } + if let Some(site) = site { + set_expr_span(expression, site); } + Ok(()) } fn expand_macro_body( @@ -483,7 +392,7 @@ impl MacroExpander { span: Option, parent_site: Option, ) -> Result, IntegrationError> { - let Some((params, body)) = self.macros.get(name).cloned() else { + let Some((params, mut body)) = self.macros.get(name).cloned() else { return Err(IntegrationError::new( "unsupported-integration-surface", format!("macro '{name}' has no declaration"), @@ -514,7 +423,7 @@ impl MacroExpander { } self.stack.push(name.to_string()); let site = parent_site.or_else(|| self.attribute_to_site.then_some(span).flatten()); - let result = self.expand_stmts(&body, &bindings, site); + let result = self.expand_stmts(&mut body, &bindings, site).map(|()| body); self.stack.pop(); result } From f0fd06bd5298b542ae73d2409d126e324f9e7040 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 06:02:20 +0800 Subject: [PATCH 34/67] refactor(opy-rs): share rule name formatting filter --- crates/opy-rs/src/compiler/lowering.rs | 10 +--------- crates/opy-rs/src/lower.rs | 26 ++++++++++++-------------- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/crates/opy-rs/src/compiler/lowering.rs b/crates/opy-rs/src/compiler/lowering.rs index 0322bed5..f9e789ad 100644 --- a/crates/opy-rs/src/compiler/lowering.rs +++ b/crates/opy-rs/src/compiler/lowering.rs @@ -1663,15 +1663,7 @@ const FILTERED_RULE_NAME_WORDS: [(&str, &str, bool); 28] = [ /// invisible formatting characters, and the Workshop's filtered words are split /// with a soft hyphen, as the pinned OverPy does when it writes them. pub(super) fn escape_bad_words(name: &str) -> String { - let mut text: Vec = name - .chars() - .filter(|character| { - !matches!( - character, - '\u{200B}' | '\u{200E}' | '\u{200F}' | '\u{FEFF}' | '\u{061C}' - ) - }) - .collect(); + let mut text: Vec = crate::lower::strip_rule_name_formatting(name).collect(); for (head, tail, standalone) in FILTERED_RULE_NAME_WORDS { text = split_filtered_word(&text, head, tail, standalone); } diff --git a/crates/opy-rs/src/lower.rs b/crates/opy-rs/src/lower.rs index 4ccb1af7..502cf54a 100644 --- a/crates/opy-rs/src/lower.rs +++ b/crates/opy-rs/src/lower.rs @@ -476,15 +476,13 @@ fn prefixed_rule_name(name: &str, prefix: Option<&str>, delimiter: bool) -> Stri } } -fn strip_rule_name_formatting(text: &str) -> String { - text.chars() - .filter(|character| { - !matches!( - character, - '\u{200B}' | '\u{200E}' | '\u{200F}' | '\u{FEFF}' | '\u{061C}' - ) - }) - .collect() +pub(crate) fn strip_rule_name_formatting(text: &str) -> impl Iterator + '_ { + text.chars().filter(|character| { + !matches!( + character, + '\u{200B}' | '\u{200E}' | '\u{200F}' | '\u{FEFF}' | '\u{061C}' + ) + }) } #[derive(Clone, Debug)] @@ -531,15 +529,15 @@ fn render_rule_name( .as_ref() .map(|value| value.value.as_str()) else { - return Ok(strip_rule_name_formatting(&prefixed_rule_name( - name, prefix, delimiter, - ))); + return Ok( + strip_rule_name_formatting(&prefixed_rule_name(name, prefix, delimiter)).collect(), + ); }; let (file, path) = rule_file_parts(span.file, files); let prefix = prefix.unwrap_or_default(); let values = rule_template_values(name, prefix, &file, &path, delimiter); evaluate_template(template, &values) - .map(|name| strip_rule_name_formatting(&name)) + .map(|name| strip_rule_name_formatting(&name).collect()) .map_err(|message| { OpyError::at( "rule-prefix-template-invalid", @@ -561,7 +559,7 @@ pub(crate) fn render_generated_rule_name(name: &str, preprocessing: &Preprocessi }; let values = rule_template_values(name, "", "", "", false); evaluate_template(template, &values) - .map(|rendered| strip_rule_name_formatting(&rendered)) + .map(|rendered| strip_rule_name_formatting(&rendered).collect()) .unwrap_or_else(|_| name.to_string()) } From 56809ce09f148caabbb1e3b166adc388161ea2d1 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 06:07:13 +0800 Subject: [PATCH 35/67] refactor(opy-rs): reuse generated rule construction --- .../src/compiler/lowering/declarations.rs | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/crates/opy-rs/src/compiler/lowering/declarations.rs b/crates/opy-rs/src/compiler/lowering/declarations.rs index 5011290f..50021616 100644 --- a/crates/opy-rs/src/compiler/lowering/declarations.rs +++ b/crates/opy-rs/src/compiler/lowering/declarations.rs @@ -440,16 +440,8 @@ impl<'a> Lowering<'a> { self.mark_action_argument_origins(action, [init_expr.span().copied()]); actions.push(action); } - let rule_index = self.program.rules.len(); - self.program.rules.push(rule_from_parts( - self.global_initializer_rule_name(), - false, - workshop_rs::Event::Global, - Vec::new(), - self.public_actions(&actions), - )); - let action_provenance = self.action_provenance(&actions); - self.set_rule_provenance(rule_index, None, std::iter::empty(), action_provenance)?; + let name = self.global_initializer_rule_name(); + self.push_generated_rule(&name, Event::Global, actions)?; } if uses_player_translation_var || !player_initializers.is_empty() { @@ -485,16 +477,8 @@ impl<'a> Lowering<'a> { self.mark_action_argument_origins(action, [None, init_expr.span().copied()]); actions.push(action); } - let rule_index = self.program.rules.len(); - self.program.rules.push(rule_from_parts( - self.player_initializer_rule_name(), - false, - workshop_rs::Event::EachPlayer, - Vec::new(), - self.public_actions(&actions), - )); - let action_provenance = self.action_provenance(&actions); - self.set_rule_provenance(rule_index, None, std::iter::empty(), action_provenance)?; + let name = self.player_initializer_rule_name(); + self.push_generated_rule(&name, Event::EachPlayer, actions)?; } Ok(()) From ebfc7366e131d0d6873d4a44f9c03ae52ee2fdaf Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 06:18:54 +0800 Subject: [PATCH 36/67] refactor(opy-rs): centralize rule provenance writes --- crates/opy-rs/src/compiler/lowering.rs | 30 +++++++++---------- .../src/compiler/lowering/declarations.rs | 24 ++++----------- crates/opy-rs/src/compiler/lowering/rules.rs | 20 +++++-------- 3 files changed, 29 insertions(+), 45 deletions(-) diff --git a/crates/opy-rs/src/compiler/lowering.rs b/crates/opy-rs/src/compiler/lowering.rs index f9e789ad..edb61e4a 100644 --- a/crates/opy-rs/src/compiler/lowering.rs +++ b/crates/opy-rs/src/compiler/lowering.rs @@ -909,28 +909,28 @@ impl<'a> Lowering<'a> { } } - fn set_rule_provenance( + fn append_rule( &mut self, - rule: usize, + rule_value: workshop_rs::Rule, + actions: &[ActionId], span: Option, - conditions: C, - actions: A, - ) -> Result<(), IntegrationError> - where - C: IntoIterator>, - A: IntoIterator, Vec>)>, - { + condition_spans: impl IntoIterator>, + ) -> Result<(), IntegrationError> { + let rule_index = self.program.rules.len(); + self.program.rules.push(rule_value); self.program - .set_rule_span(rule, self.workshop_span(span)?) + .set_rule_span(rule_index, self.workshop_span(span)?) .map_err(|error| IntegrationError::new("provenance", error.to_string(), span))?; - for (index, span) in conditions.into_iter().enumerate() { + for (index, span) in condition_spans.into_iter().enumerate() { self.program - .set_condition_span(rule, index, self.workshop_span(span)?) + .set_condition_span(rule_index, index, self.workshop_span(span)?) .map_err(|error| IntegrationError::new("provenance", error.to_string(), span))?; } - for (index, (span, argument_spans)) in actions.into_iter().enumerate() { + for (index, (span, argument_spans)) in + self.action_provenance(actions).into_iter().enumerate() + { self.program - .set_action_span(rule, index, self.workshop_span(span)?) + .set_action_span(rule_index, index, self.workshop_span(span)?) .map_err(|error| IntegrationError::new("provenance", error.to_string(), span))?; for (argument, span) in argument_spans.into_iter().enumerate() { let Some(span) = span else { @@ -938,7 +938,7 @@ impl<'a> Lowering<'a> { }; self.program .set_action_argument_span( - rule, + rule_index, index, argument, self.workshop_span(Some(span))?, diff --git a/crates/opy-rs/src/compiler/lowering/declarations.rs b/crates/opy-rs/src/compiler/lowering/declarations.rs index 50021616..6049e75b 100644 --- a/crates/opy-rs/src/compiler/lowering/declarations.rs +++ b/crates/opy-rs/src/compiler/lowering/declarations.rs @@ -490,20 +490,14 @@ impl<'a> Lowering<'a> { event: Event, actions: Vec, ) -> Result<(), IntegrationError> { - let rule_index = self.program.rules.len(); - self.program.rules.push(rule_from_parts( + let rule = rule_from_parts( name.to_string(), false, event, Vec::new(), self.public_actions(&actions), - )); - self.set_rule_provenance( - rule_index, - None, - std::iter::empty(), - self.action_provenance(&actions), - ) + ); + self.append_rule(rule, &actions, None, std::iter::empty()) } pub(super) fn translation_player_options(&self) -> (bool, bool, bool) { @@ -638,8 +632,7 @@ impl<'a> Lowering<'a> { set_facing, finish, ]; - let rule_index = self.program.rules.len(); - self.program.rules.push(rule_from_parts( + let rule = rule_from_parts( "OverPy translation setup - Determine the player's language".to_string(), false, Event::EachPlayer, @@ -649,13 +642,8 @@ impl<'a> Lowering<'a> { workshop_rs::Condition::new(self.materialize_value(initial_language)), ], self.public_actions(&actions), - )); - self.set_rule_provenance( - rule_index, - None, - [None, None, None], - self.action_provenance(&actions), - )?; + ); + self.append_rule(rule, &actions, None, [None, None, None])?; Ok(()) } } diff --git a/crates/opy-rs/src/compiler/lowering/rules.rs b/crates/opy-rs/src/compiler/lowering/rules.rs index c126c6c0..75925f15 100644 --- a/crates/opy-rs/src/compiler/lowering/rules.rs +++ b/crates/opy-rs/src/compiler/lowering/rules.rs @@ -51,8 +51,7 @@ impl<'a> Lowering<'a> { if elide_noop_switch && !rule.disabled { return Ok(()); } - let rule_index = self.program.rules.len(); - self.program.rules.push(rule_from_parts( + let value = rule_from_parts( rule.name.clone(), rule.disabled, event, @@ -73,13 +72,12 @@ impl<'a> Lowering<'a> { }) .collect(), self.public_actions(&actions), - )); - let action_provenance = self.action_provenance(&actions); - self.set_rule_provenance( - rule_index, + ); + self.append_rule( + value, + &actions, rule.span, condition_exprs.iter().map(|expr| expr.span().copied()), - action_provenance, )?; Ok(()) } @@ -161,16 +159,14 @@ impl<'a> Lowering<'a> { { return Ok(()); } - let rule_index = self.program.rules.len(); - self.program.rules.push(rule_from_parts( + let value = rule_from_parts( self.subroutine_rule_name(name), false, event, Vec::new(), self.public_actions(&actions), - )); - let action_provenance = self.action_provenance(&actions); - self.set_rule_provenance(rule_index, span, std::iter::empty(), action_provenance)?; + ); + self.append_rule(value, &actions, span, std::iter::empty())?; Ok(()) } From d6c4de6a1314e92bbca1024506f77038bedeed8b Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 06:23:34 +0800 Subject: [PATCH 37/67] refactor(opy-rs): unify logical operator folding --- .../src/compiler/operator_optimization.rs | 47 +++++-------------- 1 file changed, 11 insertions(+), 36 deletions(-) diff --git a/crates/opy-rs/src/compiler/operator_optimization.rs b/crates/opy-rs/src/compiler/operator_optimization.rs index a8ed9cb2..87ceae95 100644 --- a/crates/opy-rs/src/compiler/operator_optimization.rs +++ b/crates/opy-rs/src/compiler/operator_optimization.rs @@ -90,8 +90,7 @@ impl<'a> OperatorOptimizer<'a> { (">", 2) => Self::ordering(">", args, |a, b| a > b, false), (">=", 2) => Self::ordering(">=", args, |a, b| a >= b, true), ("not", 1) => self.not(args), - ("and", 2) => self.and(args), - ("or", 2) => self.or(args), + ("and", 2) | ("or", 2) => self.logical(args, &name), ("ifThenElse", 3) => self.if_then_else(args), ("add", 2) => self.add(args), ("subtract", 2) => self.subtract(args), @@ -250,56 +249,32 @@ impl<'a> OperatorOptimizer<'a> { Rewrite::Same(not(operand)) } - fn and(&self, args: Vec) -> Rewrite { + fn logical(&self, args: Vec, name: &str) -> Rewrite { + let conjunction = name == "and"; + let opposite = if conjunction { "or" } else { "and" }; let [left, right] = two(args); if falsy(&left) { - return Rewrite::Changed(left); + return Rewrite::Changed(if conjunction { left } else { right }); } if !self.strict && falsy(&right) { - return Rewrite::Changed(right); + return Rewrite::Changed(if conjunction { right } else { left }); } if self.truthy(&left) { - return Rewrite::Changed(right); + return Rewrite::Changed(if conjunction { right } else { left }); } if !self.strict && self.truthy(&right) { - return Rewrite::Changed(left); + return Rewrite::Changed(if conjunction { left } else { right }); } if same(&left, &right) { return Rewrite::Changed(left); } if !self.strict && (negates(&right, &left) || negates(&left, &right)) { - return Rewrite::Changed(Value::Bool(false)); + return Rewrite::Changed(Value::Bool(!conjunction)); } if let (Some(a), Some(b)) = (negated(&left), negated(&right)) { - return Rewrite::Changed(not(call("or", vec![a.clone(), b.clone()]))); + return Rewrite::Changed(not(call(opposite, vec![a.clone(), b.clone()]))); } - Rewrite::Same(call("and", vec![left, right])) - } - - fn or(&self, args: Vec) -> Rewrite { - let [left, right] = two(args); - if falsy(&left) { - return Rewrite::Changed(right); - } - if !self.strict && falsy(&right) { - return Rewrite::Changed(left); - } - if self.truthy(&left) { - return Rewrite::Changed(left); - } - if !self.strict && self.truthy(&right) { - return Rewrite::Changed(right); - } - if same(&left, &right) { - return Rewrite::Changed(left); - } - if !self.strict && (negates(&right, &left) || negates(&left, &right)) { - return Rewrite::Changed(Value::Bool(true)); - } - if let (Some(a), Some(b)) = (negated(&left), negated(&right)) { - return Rewrite::Changed(not(call("and", vec![a.clone(), b.clone()]))); - } - Rewrite::Same(call("or", vec![left, right])) + Rewrite::Same(call(name, vec![left, right])) } fn if_then_else(&self, args: Vec) -> Rewrite { From b8b882ecce7ce6528b3476697a43015e0632d082 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 06:32:58 +0800 Subject: [PATCH 38/67] refactor(opy-rs): reuse literal value predicates --- .../src/compiler/operator_optimization.rs | 28 +++---------------- .../opy-rs/src/compiler/size_optimization.rs | 21 ++------------ 2 files changed, 7 insertions(+), 42 deletions(-) diff --git a/crates/opy-rs/src/compiler/operator_optimization.rs b/crates/opy-rs/src/compiler/operator_optimization.rs index 87ceae95..639bd607 100644 --- a/crates/opy-rs/src/compiler/operator_optimization.rs +++ b/crates/opy-rs/src/compiler/operator_optimization.rs @@ -524,17 +524,7 @@ impl<'a> OperatorOptimizer<'a> { fn string_length(args: Vec) -> Rewrite { let [text] = one(args); - let literal = match &text { - Value::String(text) => Some(text), - Value::Call { name, args } if name == "customString" && args.len() == 1 => { - match &args[0] { - Value::String(text) => Some(text), - _ => None, - } - } - _ => None, - }; - match literal { + match literal_text(&text) { Some(text) => Rewrite::Changed(Value::Number(text.chars().count() as f64)), None => Rewrite::Same(call("strLen", vec![text])), } @@ -739,17 +729,7 @@ impl<'a> OperatorOptimizer<'a> { fn char_at(args: Vec) -> Rewrite { let [text, index] = two(args); - let literal = match &text { - Value::String(text) => Some(text.as_str()), - Value::Call { name, args } if name == "customString" && args.len() == 1 => { - match &args[0] { - Value::String(text) => Some(text.as_str()), - _ => None, - } - } - _ => None, - }; - if let (Some(text), Value::Number(index)) = (literal, &index) { + if let (Some(text), Value::Number(index)) = (literal_text(&text), &index) { let position = index.max(0.0) as usize; let character = text .chars() @@ -1024,7 +1004,7 @@ fn three(args: Vec) -> [Value; 3] { } /// The text of a string literal that carries no format arguments. -fn literal_text(value: &Value) -> Option<&str> { +pub(super) fn literal_text(value: &Value) -> Option<&str> { match value { Value::String(text) => Some(text), Value::Call { name, args } if name == "customString" && args.len() == 1 => match &args[0] { @@ -1060,7 +1040,7 @@ fn zero_vector(value: &Value) -> bool { number_components(value).is_some_and(|components| components == [0.0, 0.0, 0.0]) } -fn number_components(value: &Value) -> Option<[f64; 3]> { +pub(super) fn number_components(value: &Value) -> Option<[f64; 3]> { match value { Value::Call { name, args } if name == "vector" => match args.as_slice() { [Value::Number(x), Value::Number(y), Value::Number(z)] => Some([*x, *y, *z]), diff --git a/crates/opy-rs/src/compiler/size_optimization.rs b/crates/opy-rs/src/compiler/size_optimization.rs index 0c360625..564e9445 100644 --- a/crates/opy-rs/src/compiler/size_optimization.rs +++ b/crates/opy-rs/src/compiler/size_optimization.rs @@ -6,7 +6,7 @@ use workshop_rs::{Action, ModifyOp, Value}; use self::literal_slots::{Slot, slot}; use super::Compiler; -use super::operator_optimization::falsy; +use super::operator_optimization::{falsy, literal_text, number_components}; use super::value_walk::for_each_child; mod literal_slots; @@ -377,26 +377,11 @@ fn compact_vector(x: &Value, y: &Value, z: &Value) -> Option { } pub(super) fn is_empty_string(value: &Value) -> bool { - match value { - Value::String(text) => text.is_empty(), - Value::Call { name, args } => { - name == "customString" - && args.len() == 1 - && matches!(&args[0], Value::String(text) if text.is_empty()) - } - _ => false, - } + literal_text(value).is_some_and(str::is_empty) } fn is_zero_vector(value: &Value) -> bool { - let is_zero = |value: &Value| matches!(value, Value::Number(number) if *number == 0.0); - match value { - Value::Vector { x, y, z } => is_zero(x) && is_zero(y) && is_zero(z), - Value::Call { name, args } => { - name == "vector" && args.len() == 3 && args.iter().all(is_zero) - } - _ => false, - } + number_components(value) == Some([0.0; 3]) } fn modify_op_of(value: &Value) -> Option { From 9e9039d725e5116c98b44a4f4508e1affd765c9c Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 07:20:51 +0800 Subject: [PATCH 39/67] refactor(opy-rs): share size optimizer boolean rewrite --- .../opy-rs/src/compiler/size_optimization.rs | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/crates/opy-rs/src/compiler/size_optimization.rs b/crates/opy-rs/src/compiler/size_optimization.rs index 564e9445..8a28b90c 100644 --- a/crates/opy-rs/src/compiler/size_optimization.rs +++ b/crates/opy-rs/src/compiler/size_optimization.rs @@ -30,11 +30,7 @@ impl<'a> SizeOptimizer<'a> { Action::ForGlobalVariable { stop, step, .. } | Action::ForPlayerVariable { stop, step, .. } => { for bound in [stop, step] { - match bound { - Value::Number(zero) if *zero == 0.0 => *bound = Value::Bool(false), - Value::Number(one) if *one == 1.0 => *bound = Value::Bool(true), - _ => {} - } + boolean_number(bound); } } Action::Call { name, args } => { @@ -88,11 +84,7 @@ impl<'a> SizeOptimizer<'a> { | ModifyOp::Max | ModifyOp::Min | ModifyOp::RemoveFromArrayByIndex => { - if *number == 0.0 { - *value = Value::Bool(false); - } else if *number == 1.0 { - *value = Value::Bool(true); - } + boolean_number(value); } ModifyOp::AppendToArray | ModifyOp::RemoveFromArrayByValue if *number == 0.0 => { *value = Value::Null; @@ -108,11 +100,7 @@ impl<'a> SizeOptimizer<'a> { _ => return, }; if let Some(index) = args.get_mut(index) { - match index { - Value::Number(number) if *number == 0.0 => *index = Value::Bool(false), - Value::Number(number) if *number == 1.0 => *index = Value::Bool(true), - _ => {} - } + boolean_number(index); } let is_modify = name.starts_with("modify"); let op = if is_modify { @@ -172,12 +160,8 @@ impl<'a> SizeOptimizer<'a> { _ => return, }; for position in positions { - if let Some(arg @ Value::Number(_)) = args.get_mut(*position) { - match arg { - Value::Number(number) if *number == 0.0 => *arg = Value::Bool(false), - Value::Number(number) if *number == 1.0 => *arg = Value::Bool(true), - _ => {} - } + if let Some(arg) = args.get_mut(*position) { + boolean_number(arg); } } } @@ -331,6 +315,17 @@ fn number_of(value: &Value) -> Option { } } +fn boolean_number(value: &mut Value) { + let boolean = match value { + Value::Number(number) if *number == 0.0 => Some(false), + Value::Number(number) if *number == 1.0 => Some(true), + _ => None, + }; + if let Some(boolean) = boolean { + *value = Value::Bool(boolean); + } +} + /// The shorter spellings of a vector: the sum of two directions, or one /// component scaling its direction. fn compact_vector(x: &Value, y: &Value, z: &Value) -> Option { From 863cc566d936d6d0f84ee014148947ab12628a68 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 07:33:09 +0800 Subject: [PATCH 40/67] refactor(opy-rs): group parser directive state --- crates/opy-rs/src/parser/definitions.rs | 84 ++++++++++--------------- 1 file changed, 33 insertions(+), 51 deletions(-) diff --git a/crates/opy-rs/src/parser/definitions.rs b/crates/opy-rs/src/parser/definitions.rs index 158036ab..c6b30e81 100644 --- a/crates/opy-rs/src/parser/definitions.rs +++ b/crates/opy-rs/src/parser/definitions.rs @@ -1,5 +1,15 @@ use super::*; +#[derive(Default)] +struct DirectiveState { + event: Option, + conditions: Vec, + annotations: Vec, + disabled: bool, + delimiter: bool, + new_page: Option, +} + impl Parser<'_> { pub(super) fn parse_rule( &mut self, @@ -43,12 +53,7 @@ impl Parser<'_> { // directives and actions. The rule itself is still top-level, so any // indentation greater than its column belongs to this rule. let body_indent = line_indent + 1; - let mut event = None; - let mut conditions = Vec::new(); - let mut annotations = Vec::new(); - let mut disabled = false; - let mut delimiter = false; - let mut new_page = None; + let mut directives = DirectiveState::default(); let mut actions = Vec::new(); loop { self.skip_newlines(); @@ -56,15 +61,7 @@ impl Parser<'_> { break; } if self.peek_kind() == TokenKind::At { - if !self.parse_directive( - &mut event, - &mut conditions, - &mut annotations, - &mut disabled, - &mut delimiter, - &mut new_page, - false, - ) { + if !self.parse_directive(&mut directives, false) { self.recover_line(); } continue; @@ -78,33 +75,31 @@ impl Parser<'_> { name, span: Span::new(start.span.file, start.span.start, name_token_span.end), name_span, - disabled, - delimiter, - new_page, - annotations, + disabled: directives.disabled, + delimiter: directives.delimiter, + new_page: directives.new_page, + annotations: directives.annotations, rule_prefix, - event: event.unwrap_or_else(|| Event { + event: directives.event.unwrap_or_else(|| Event { name: "global".to_string(), args: Vec::new(), span: start.span, }), - conditions, + conditions: directives.conditions, actions, })); true } - #[allow(clippy::too_many_arguments)] - pub(super) fn parse_directive( - &mut self, - event: &mut Option, - conditions: &mut Vec, - annotations: &mut Vec, - disabled: &mut bool, - delimiter: &mut bool, - new_page: &mut Option, - subroutine: bool, - ) -> bool { + fn parse_directive(&mut self, directives: &mut DirectiveState, subroutine: bool) -> bool { + let DirectiveState { + event, + conditions, + annotations, + disabled, + delimiter, + new_page, + } = directives; let at = self.advance(); let name = match self.expect_ident("a directive name after '@'") { Ok(name) => name, @@ -348,36 +343,23 @@ impl Parser<'_> { Some(indent) => indent, None => return false, }; - let mut annotations = Vec::new(); - let mut event = None; - let mut conditions = Vec::new(); - let mut disabled = false; - let mut delimiter = false; - let mut new_page = None; + let mut directives = DirectiveState::default(); loop { self.skip_newlines(); if self.peek_kind() != TokenKind::At { break; } - if !self.parse_directive( - &mut event, - &mut conditions, - &mut annotations, - &mut disabled, - &mut delimiter, - &mut new_page, - true, - ) { + if !self.parse_directive(&mut directives, true) { self.recover_line(); return false; } } - if event.is_some() || !conditions.is_empty() { + if directives.event.is_some() || !directives.conditions.is_empty() { self.error_at_current("subroutines cannot have events or conditions".to_string()); return false; } - let _ = (disabled, delimiter, new_page); - let presentation_name = annotations + let presentation_name = directives + .annotations .iter() .find(|annotation| annotation.name == "Name") .and_then(|annotation| annotation.args.first()) @@ -390,7 +372,7 @@ impl Parser<'_> { span, name_span, body, - annotations, + annotations: directives.annotations, rule_prefix, }); true From 36a211c0941c7375368049b89594d50c4800bd4f Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 07:52:08 +0800 Subject: [PATCH 41/67] refactor(opy-rs): share reconstruction call resolution --- crates/opy-rs/src/compiler/reconstruct.rs | 162 +++++++++------------- 1 file changed, 68 insertions(+), 94 deletions(-) diff --git a/crates/opy-rs/src/compiler/reconstruct.rs b/crates/opy-rs/src/compiler/reconstruct.rs index cbfcc2ab..05004bb2 100644 --- a/crates/opy-rs/src/compiler/reconstruct.rs +++ b/crates/opy-rs/src/compiler/reconstruct.rs @@ -1307,46 +1307,15 @@ impl<'a> Emitter<'a> { ); return; } - let Some(entry) = self.manifest.resolve_function(name) else { - match self.manifest.resolve_member(name) { - Some(entry) if entry.kind.is_action() => { - self.emit_member_call(entry, args, indent, span, argument_spans); - } - Some(_) => { - self.issue( - "unsupported-action-call", - format!( - "member value '{name}' cannot be emitted as an action on \ - the reconstruction surface" - ), - span, - ); - } - None => { - self.issue( - "unsupported-action-call", - format!( - "action call '{name}' has no OPY source form on the \ - reconstruction surface" - ), - span, - ); + let (entry, member) = + match Self::resolve_call_entry(self.manifest, name, FunctionKind::Action) { + Ok(call) => call, + Err(message) => { + self.issue("unsupported-action-call", message, span); + return; } - } - return; - }; - if !entry.kind.is_action() { - self.issue( - "unsupported-action-call", - format!( - "value function '{name}' cannot be emitted as an action on \ - the reconstruction surface" - ), - span, - ); - return; - } - if args.is_empty() && self.subroutine_names.contains(name) { + }; + if !member && args.is_empty() && self.subroutine_names.contains(name) { self.issue( "unsupported-action-call", format!( @@ -1358,10 +1327,60 @@ impl<'a> Emitter<'a> { return; } self.out.push_str(indent); - self.emit_manifest_call(entry, args, false, span, argument_spans); + self.emit_manifest_call(entry, args, member, span, argument_spans); self.out.push('\n'); } + fn resolve_call_entry<'manifest>( + manifest: &'manifest Manifest, + name: &str, + expected: FunctionKind, + ) -> Result<(&'manifest Function, bool), String> { + let expected_action = expected.is_action(); + let (entry, member) = match manifest.resolve_function(name) { + Some(entry) => (entry, false), + None => match manifest.resolve_member(name) { + Some(entry) => (entry, true), + None => { + let message = if expected_action { + format!( + "action call '{name}' has no OPY source form on the \ + reconstruction surface" + ) + } else { + format!( + "value call '{name}' has no OPY source form on the \ + reconstruction surface" + ) + }; + return Err(message); + } + }, + }; + if expected_action != entry.kind.is_action() { + let message = match (expected_action, member) { + (true, true) => format!( + "member value '{name}' cannot be emitted as an action on \ + the reconstruction surface" + ), + (true, false) => format!( + "value function '{name}' cannot be emitted as an action on \ + the reconstruction surface" + ), + (false, true) => format!( + "member action '{name}' cannot be emitted as a value on \ + the reconstruction surface" + ), + (false, false) => format!( + "action function '{name}' cannot be emitted as a value on \ + the reconstruction surface" + ), + }; + return Err(message); + } + Ok((entry, member)) + } + /// Emit a manifest function call with explicit full-arity arguments, no /// indent and no trailing newline (the caller frames the line). The OPY /// frontend fills declared defaults at recompile time, so any Workshop call @@ -1478,20 +1497,6 @@ impl<'a> Emitter<'a> { self.out.push(')'); } - /// A member call: `receiver.name(args...)`. - fn emit_member_call( - &mut self, - entry: &Function, - args: &[Value], - indent: &str, - span: Option, - argument_spans: &[Option], - ) { - self.out.push_str(indent); - self.emit_manifest_call(entry, args, true, span, argument_spans); - self.out.push('\n'); - } - /// Validate a provided argument against its manifest parameter: enum /// domains are enforced (like the frontend) and `variable`-required /// parameters must be variable references. @@ -1791,46 +1796,15 @@ impl<'a> Emitter<'a> { ); return; } - let Some(entry) = self.manifest.resolve_function(name) else { - match self.manifest.resolve_member(name) { - Some(entry) if entry.kind.is_value() => { - self.emit_manifest_call(entry, args, true, span, &[]); - } - Some(_) => { - self.issue( - "unsupported-value-call", - format!( - "member action '{name}' cannot be emitted as a value on \ - the reconstruction surface" - ), - span, - ); - } - None => { - self.issue( - "unsupported-value-call", - format!( - "value call '{name}' has no OPY source form on the \ - reconstruction surface" - ), - span, - ); + let (entry, member) = + match Self::resolve_call_entry(self.manifest, name, FunctionKind::Value) { + Ok(call) => call, + Err(message) => { + self.issue("unsupported-value-call", message, span); + return; } - } - return; - }; - if !entry.kind.is_value() { - self.issue( - "unsupported-value-call", - format!( - "action function '{name}' cannot be emitted as a value on the \ - reconstruction surface" - ), - span, - ); - return; - } - if crate::lower::policy::function_context(&entry.id).is_some() { + }; + if !member && crate::lower::policy::function_context(&entry.id).is_some() { self.issue( "unsupported-value-call", format!( @@ -1841,7 +1815,7 @@ impl<'a> Emitter<'a> { ); return; } - self.emit_manifest_call(entry, args, false, span, &[]); + self.emit_manifest_call(entry, args, member, span, &[]); } fn emit_string_literal(&mut self, value: &str) { From b5ad67b797626773a4655661e24b3247d2013ef4 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 08:03:34 +0800 Subject: [PATCH 42/67] refactor(opy-rs): borrow settings constant expressions --- crates/opy-rs/src/settings.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/opy-rs/src/settings.rs b/crates/opy-rs/src/settings.rs index 42024196..228a27c2 100644 --- a/crates/opy-rs/src/settings.rs +++ b/crates/opy-rs/src/settings.rs @@ -315,16 +315,10 @@ pub(crate) fn resolve_hir_settings( .declarations .iter() .filter_map(|declaration| match declaration { - hir::Declaration::Constant { name, value, .. } => { - Some((name.clone(), value.as_ref().clone())) - } + hir::Declaration::Constant { name, value, .. } => Some((name.clone(), value.as_ref())), _ => None, }) .collect::>(); - let constant_refs = constants - .iter() - .map(|(name, value)| (name.clone(), value)) - .collect::>(); let mut expander = crate::compiler::MacroExpander::from_program(program); let Some(settings) = program.settings.take() else { return Ok(()); @@ -332,7 +326,7 @@ pub(crate) fn resolve_hir_settings( let children = settings .children .into_iter() - .map(|node| resolve_hir_node(node, cst_program, &constant_refs, &mut expander)) + .map(|node| resolve_hir_node(node, cst_program, &constants, &mut expander)) .collect::>>()?; program.settings = Some(hir::Settings { span: settings.span, From bbf9a719d72fd39a076f25adf3dd9e6e5993aed7 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 08:22:08 +0800 Subject: [PATCH 43/67] perf(opy-rs): avoid duplicate HIR condition validation --- crates/opy-rs/src/hir/validate.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/opy-rs/src/hir/validate.rs b/crates/opy-rs/src/hir/validate.rs index 4c31724b..3ef56c5d 100644 --- a/crates/opy-rs/src/hir/validate.rs +++ b/crates/opy-rs/src/hir/validate.rs @@ -828,9 +828,6 @@ fn check_expr(value: &Value) -> Result<(), HirError> { } } } - if let Some(condition) = object.get("condition") { - check_expr(condition)?; - } if let Some(body) = object.get("body") { check_expr(body)?; } From 5fc9aae233bb52ae5cb1d4a791910244b3f614e9 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 08:22:21 +0800 Subject: [PATCH 44/67] refactor(opy-rs): share cased glyph record construction --- crates/opy-rs/src/compiler/blizzard_global.rs | 217 +++--------------- 1 file changed, 34 insertions(+), 183 deletions(-) diff --git a/crates/opy-rs/src/compiler/blizzard_global.rs b/crates/opy-rs/src/compiler/blizzard_global.rs index acf2e7ab..d45edf2c 100644 --- a/crates/opy-rs/src/compiler/blizzard_global.rs +++ b/crates/opy-rs/src/compiler/blizzard_global.rs @@ -1241,189 +1241,40 @@ pub(crate) fn spaces(width: i32) -> String { } pub(crate) fn cased_glyph(character: char) -> Option { - Some(match character { - 'a' => CasedGlyph { - lower: "a", - width: 285, - xmin: 21, - lower_width: 512, - lower_xmin: 130, - }, - 'b' => CasedGlyph { - lower: "b", - width: 285, - xmin: 28, - lower_width: 512, - lower_xmin: 136, - }, - 'c' => CasedGlyph { - lower: "c", - width: 256, - xmin: 16, - lower_width: 512, - lower_xmin: 142, - }, - 'd' => CasedGlyph { - lower: "d", - width: 285, - xmin: 13, - lower_width: 512, - lower_xmin: 136, - }, - 'e' => CasedGlyph { - lower: "e", - width: 285, - xmin: 20, - lower_width: 512, - lower_xmin: 135, - }, - 'f' => CasedGlyph { - lower: "f", - width: 142, - xmin: 9, - lower_width: 512, - lower_xmin: 195, - }, - 'g' => CasedGlyph { - lower: "g", - width: 285, - xmin: 15, - lower_width: 512, - lower_xmin: 138, - }, - 'h' => CasedGlyph { - lower: "h", - width: 285, - xmin: 36, - lower_width: 512, - lower_xmin: 150, - }, - 'i' => CasedGlyph { - lower: "і", - width: 114, - xmin: 34, - lower_width: 115, - lower_xmin: 36, - }, - 'j' => CasedGlyph { - lower: "ј", - width: 114, - xmin: -9, - lower_width: 115, - lower_xmin: -9, - }, - 'k' => CasedGlyph { - lower: "k", - width: 256, - xmin: 30, - lower_width: 512, - lower_xmin: 142, - }, - 'l' => CasedGlyph { - lower: "I", - width: 114, - xmin: 35, - lower_width: 142, - lower_xmin: 51, - }, - 'm' => CasedGlyph { - lower: "m", - width: 426, - xmin: 36, - lower_width: 512, - lower_xmin: 79, - }, - 'n' => CasedGlyph { - lower: "n", - width: 285, - xmin: 36, - lower_width: 512, - lower_xmin: 149, - }, - 'o' => CasedGlyph { - lower: "o", - width: 285, - xmin: 18, - lower_width: 512, - lower_xmin: 135, - }, - 'p' => CasedGlyph { - lower: "p", - width: 285, - xmin: 28, - lower_width: 512, - lower_xmin: 136, - }, - 'q' => CasedGlyph { - lower: "q", - width: 285, - xmin: 13, - lower_width: 512, - lower_xmin: 136, - }, - 'r' => CasedGlyph { - lower: "r", - width: 170, - xmin: 35, - lower_width: 512, - lower_xmin: 191, - }, - 's' => CasedGlyph { - lower: "s", - width: 256, - xmin: 17, - lower_width: 512, - lower_xmin: 147, - }, - 't' => CasedGlyph { - lower: "t", - width: 142, - xmin: 7, - lower_width: 512, - lower_xmin: 195, - }, - 'u' => CasedGlyph { - lower: "u", - width: 285, - xmin: 33, - lower_width: 512, - lower_xmin: 149, - }, - 'v' => CasedGlyph { - lower: "v", - width: 256, - xmin: 5, - lower_width: 512, - lower_xmin: 134, - }, - 'w' => CasedGlyph { - lower: "w", - width: 370, - xmin: 3, - lower_width: 512, - lower_xmin: 76, - }, - 'x' => CasedGlyph { - lower: "x", - width: 256, - xmin: 9, - lower_width: 512, - lower_xmin: 139, - }, - 'y' => CasedGlyph { - lower: "y", - width: 256, - xmin: 10, - lower_width: 512, - lower_xmin: 139, - }, - 'z' => CasedGlyph { - lower: "z", - width: 256, - xmin: 16, - lower_width: 512, - lower_xmin: 147, - }, + let (lower, width, xmin, lower_width, lower_xmin) = match character { + 'a' => ("a", 285, 21, 512, 130), + 'b' => ("b", 285, 28, 512, 136), + 'c' => ("c", 256, 16, 512, 142), + 'd' => ("d", 285, 13, 512, 136), + 'e' => ("e", 285, 20, 512, 135), + 'f' => ("f", 142, 9, 512, 195), + 'g' => ("g", 285, 15, 512, 138), + 'h' => ("h", 285, 36, 512, 150), + 'i' => ("і", 114, 34, 115, 36), + 'j' => ("ј", 114, -9, 115, -9), + 'k' => ("k", 256, 30, 512, 142), + 'l' => ("I", 114, 35, 142, 51), + 'm' => ("m", 426, 36, 512, 79), + 'n' => ("n", 285, 36, 512, 149), + 'o' => ("o", 285, 18, 512, 135), + 'p' => ("p", 285, 28, 512, 136), + 'q' => ("q", 285, 13, 512, 136), + 'r' => ("r", 170, 35, 512, 191), + 's' => ("s", 256, 17, 512, 147), + 't' => ("t", 142, 7, 512, 195), + 'u' => ("u", 285, 33, 512, 149), + 'v' => ("v", 256, 5, 512, 134), + 'w' => ("w", 370, 3, 512, 76), + 'x' => ("x", 256, 9, 512, 139), + 'y' => ("y", 256, 10, 512, 139), + 'z' => ("z", 256, 16, 512, 147), _ => return None, + }; + Some(CasedGlyph { + lower, + width, + xmin, + lower_width, + lower_xmin, }) } From 08cc6f67ffa7abc3e85590c2237c88b7f7dffe15 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 08:34:21 +0800 Subject: [PATCH 45/67] refactor(opy-rs): consolidate expression parser helpers --- crates/opy-rs/src/lexer.rs | 22 ++++++++++-------- crates/opy-rs/src/parser.rs | 14 +----------- crates/opy-rs/src/parser/expressions.rs | 30 +++++++++++-------------- 3 files changed, 27 insertions(+), 39 deletions(-) diff --git a/crates/opy-rs/src/lexer.rs b/crates/opy-rs/src/lexer.rs index c6b19391..871921a8 100644 --- a/crates/opy-rs/src/lexer.rs +++ b/crates/opy-rs/src/lexer.rs @@ -358,15 +358,7 @@ impl Lexer { value.push(decoded); continue; } - value.push(match escaped { - 'n' => '\n', - 't' => '\t', - 'r' => '\r', - '\\' => '\\', - '"' => '"', - '\'' => '\'', - other => other, - }); + value.push(decode_string_escape(escaped)); self.advance(); continue; } @@ -548,6 +540,18 @@ pub(crate) fn is_identifier(text: &str) -> bool { characters.next().is_some_and(is_ident_start) && characters.all(is_ident_continue) } +pub(crate) fn decode_string_escape(character: char) -> char { + match character { + 'n' => '\n', + 't' => '\t', + 'r' => '\r', + '\\' => '\\', + '"' => '"', + '\'' => '\'', + other => other, + } +} + pub(crate) fn is_ident_start(c: char) -> bool { c.is_ascii_alphabetic() || c == '_' } diff --git a/crates/opy-rs/src/parser.rs b/crates/opy-rs/src/parser.rs index 2dd8e6f4..1edecdc9 100644 --- a/crates/opy-rs/src/parser.rs +++ b/crates/opy-rs/src/parser.rs @@ -12,7 +12,7 @@ use crate::cst::{ RuleEntry, Stmt, SwitchArm, TopLevel, }; use crate::diag::{OpyError, Position, Span}; -use crate::lexer::{Token, TokenKind, is_identifier}; +use crate::lexer::{Token, TokenKind, decode_string_escape, is_identifier}; /// The outcome of a parse. #[derive(Debug, Default)] @@ -351,18 +351,6 @@ fn shift_span(span: Span, origin: Position) -> Span { ) } -fn decode_string_escape(character: char) -> char { - match character { - 'n' => '\n', - 't' => '\t', - 'r' => '\r', - '\\' => '\\', - '"' => '"', - '\'' => '\'', - other => other, - } -} - fn is_string_modifier(text: &str) -> bool { matches!(text, "f" | "w" | "l" | "b" | "c" | "t") } diff --git a/crates/opy-rs/src/parser/expressions.rs b/crates/opy-rs/src/parser/expressions.rs index d15af6af..7ff26eda 100644 --- a/crates/opy-rs/src/parser/expressions.rs +++ b/crates/opy-rs/src/parser/expressions.rs @@ -61,33 +61,29 @@ impl Parser<'_> { } pub(super) fn parse_or(&mut self) -> Result { - self.skip_expression_newlines(); - let mut left = self.parse_and()?; - loop { - self.skip_expression_newlines(); - if !self.is_ident("or") { - break; - } - self.advance(); - self.skip_expression_newlines(); - let right = self.parse_and()?; - left = binary("or", left, right); - } - Ok(left) + self.parse_ident_chain("or", |parser| parser.parse_and()) } pub(super) fn parse_and(&mut self) -> Result { + self.parse_ident_chain("and", |parser| parser.parse_not()) + } + + fn parse_ident_chain( + &mut self, + operator: &str, + mut parse_next: impl FnMut(&mut Self) -> Result, + ) -> Result { self.skip_expression_newlines(); - let mut left = self.parse_not()?; + let mut left = parse_next(self)?; loop { self.skip_expression_newlines(); - if !self.is_ident("and") { + if !self.is_ident(operator) { break; } self.advance(); self.skip_expression_newlines(); - let right = self.parse_not()?; - left = binary("and", left, right); + let right = parse_next(self)?; + left = binary(operator, left, right); } Ok(left) } From ea7a6fb994f677831b902037b1778833b00c0e54 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 08:46:35 +0800 Subject: [PATCH 46/67] refactor(opy-rs): share settings string escape decoding --- crates/opy-rs/src/settings.rs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/crates/opy-rs/src/settings.rs b/crates/opy-rs/src/settings.rs index 228a27c2..556c6298 100644 --- a/crates/opy-rs/src/settings.rs +++ b/crates/opy-rs/src/settings.rs @@ -19,7 +19,7 @@ use std::collections::HashMap; use crate::cst; use crate::diag::{OpyError, OpyResult, Position, Span}; use crate::hir; -use crate::lexer::{is_ident_continue, is_ident_start}; +use crate::lexer::{decode_string_escape, is_ident_continue, is_ident_start}; /// A project `settings { ... }` block. #[derive(Debug, Clone)] @@ -148,12 +148,7 @@ fn match_block( }; scanner.advance_by(1); if escaped { - path.push(match ch { - 'n' => '\n', - 'r' => '\r', - 't' => '\t', - other => other, - }); + path.push(decode_string_escape(ch)); escaped = false; } else if ch == '\\' { escaped = true; @@ -986,12 +981,7 @@ impl Cursor<'_> { } if ch == '\\' { let escaped = self.advance()?; - match escaped { - 'n' => value.push('\n'), - 't' => value.push('\t'), - 'r' => value.push('\r'), - other => value.push(other), - } + value.push(decode_string_escape(escaped)); } else { value.push(ch); } From 1b4f4e8e8a05247e8e22b104da38eba8d4552782 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 08:50:46 +0800 Subject: [PATCH 47/67] refactor(opy-rs): reuse lexer identifier boundaries --- crates/opy-rs/src/preprocess/macros.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/crates/opy-rs/src/preprocess/macros.rs b/crates/opy-rs/src/preprocess/macros.rs index a063c77a..14b2beac 100644 --- a/crates/opy-rs/src/preprocess/macros.rs +++ b/crates/opy-rs/src/preprocess/macros.rs @@ -441,10 +441,6 @@ pub(super) fn shift_expansion_spans(tokens: &mut [Token], origin: Span) { } fn replace_identifier(source: &str, identifier: &str, replacement: &str) -> String { - fn is_word(character: Option) -> bool { - character.is_some_and(|character| character.is_ascii_alphanumeric() || character == '_') - } - let source_chars: Vec = source.chars().collect(); let identifier_chars: Vec = identifier.chars().collect(); if identifier_chars.is_empty() { @@ -456,12 +452,13 @@ fn replace_identifier(source: &str, identifier: &str, replacement: &str) -> Stri let end = index + identifier_chars.len(); if end <= source_chars.len() && source_chars[index..end] == identifier_chars - && !is_word( - index - .checked_sub(1) - .and_then(|position| source_chars.get(position).copied()), - ) - && !is_word(source_chars.get(end).copied()) + && !index + .checked_sub(1) + .and_then(|position| source_chars.get(position).copied()) + .is_some_and(crate::lexer::is_ident_continue) + && !source_chars + .get(end) + .is_some_and(|character| crate::lexer::is_ident_continue(*character)) { result.push_str(replacement); index = end; From 643e0bec53c704108d88e0e6f6b25d503c70c9bc Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 09:02:05 +0800 Subject: [PATCH 48/67] refactor(opy-rs): share initializer reconstruction scan --- crates/opy-rs/src/compiler/reconstruct.rs | 94 ++++++++--------------- 1 file changed, 31 insertions(+), 63 deletions(-) diff --git a/crates/opy-rs/src/compiler/reconstruct.rs b/crates/opy-rs/src/compiler/reconstruct.rs index 05004bb2..b07ce50c 100644 --- a/crates/opy-rs/src/compiler/reconstruct.rs +++ b/crates/opy-rs/src/compiler/reconstruct.rs @@ -505,8 +505,8 @@ impl<'a> Emitter<'a> { // ---- emission ---- fn emit_program(&mut self, layout: &RuleLayout) { - let global_initializers = self.collect_global_initializers(layout.global_init); - let player_initializers = self.collect_player_initializers(layout.player_init); + let global_initializers = self.collect_initializers(layout.global_init, true); + let player_initializers = self.collect_initializers(layout.player_init, false); self.check_initializer_slot(&global_initializers); // Declarations. @@ -654,9 +654,10 @@ impl<'a> Emitter<'a> { /// Map initializer rule actions onto declaration positions (table order), /// validating the rule's Sets are in table order like the frontend's /// synthesized initializer rule. - fn collect_global_initializers( + fn collect_initializers( &mut self, initializer: Option>, + global: bool, ) -> std::collections::HashMap)> { let mut initializers = std::collections::HashMap::new(); let Some((rule_index, rule)) = initializer else { @@ -665,76 +666,40 @@ impl<'a> Emitter<'a> { let mut previous: Option = None; for (action_index, action) in rule.actions.iter().enumerate() { let span = self.program.action_span(rule_index, action_index); - let Action::SetGlobalVariable { variable, value } = action else { - continue; - }; - let Some(variable_position) = self - .program - .global_variables - .iter() - .position(|declaration| declaration.name == *variable) - else { - self.issue( - "unsupported-dangling", - format!("unknown global variable '{variable}'"), - span, - ); - continue; - }; - self.record_initializer_position( - &mut previous, - variable_position, - variable, - "global", - span, - ); - initializers.insert( - variable_position, + let (player, variable, value) = match (global, action) { + (true, Action::SetGlobalVariable { variable, value }) => (None, variable, value), ( - value.clone(), - self.program - .action_argument_span(rule_index, action_index, 0), - ), - ); - } - initializers - } - - fn collect_player_initializers( - &mut self, - initializer: Option>, - ) -> std::collections::HashMap)> { - let mut initializers = std::collections::HashMap::new(); - let Some((rule_index, rule)) = initializer else { - return initializers; - }; - let mut previous: Option = None; - for (action_index, action) in rule.actions.iter().enumerate() { - let span = self.program.action_span(rule_index, action_index); - let Action::SetPlayerVariable { - player, - variable, - value, - } = action - else { - continue; + false, + Action::SetPlayerVariable { + player, + variable, + value, + }, + ) => (Some(player), variable, value), + _ => continue, }; - if !self.is_event_player(player) { + if player.is_some_and(|player| !self.is_event_player(player)) { self.issue( "unsupported-init-rule", "player initializer targets a non-event-player expression", span, ); } - let Some(variable_position) = self - .program - .player_variables + let variables = if global { + &self.program.global_variables + } else { + &self.program.player_variables + }; + let Some(variable_position) = variables .iter() .position(|declaration| declaration.name == *variable) else { self.issue( "unsupported-dangling", - format!("unknown player variable '{variable}'"), + format!( + "unknown {} variable '{variable}'", + if global { "global" } else { "player" } + ), span, ); continue; @@ -743,15 +708,18 @@ impl<'a> Emitter<'a> { &mut previous, variable_position, variable, - "player", + if global { "global" } else { "player" }, span, ); initializers.insert( variable_position, ( value.clone(), - self.program - .action_argument_span(rule_index, action_index, 1), + self.program.action_argument_span( + rule_index, + action_index, + usize::from(!global), + ), ), ); } From ec381e7989842cf695270f6d7ab61127dab94c1a Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 09:10:01 +0800 Subject: [PATCH 49/67] refactor(opy-rs): share block indentation parsing --- crates/opy-rs/src/parser.rs | 9 ++++ crates/opy-rs/src/parser/declarations.rs | 24 +++-------- crates/opy-rs/src/parser/definitions.rs | 19 +++------ crates/opy-rs/src/parser/statements.rs | 52 +++++------------------- 4 files changed, 31 insertions(+), 73 deletions(-) diff --git a/crates/opy-rs/src/parser.rs b/crates/opy-rs/src/parser.rs index 1edecdc9..d7ad5833 100644 --- a/crates/opy-rs/src/parser.rs +++ b/crates/opy-rs/src/parser.rs @@ -298,6 +298,15 @@ impl Parser<'_> { Some(indent) } + pub(super) fn expect_block_indent( + &mut self, + line_indent: u32, + colon_context: &str, + ) -> Result { + self.expect(TokenKind::Colon, colon_context)?; + self.block_indent(line_indent).ok_or(()) + } + fn expect_statement_end(&mut self, what: &str) -> Result<(), ()> { let continued_line = self .tokens diff --git a/crates/opy-rs/src/parser/declarations.rs b/crates/opy-rs/src/parser/declarations.rs index 9cd8cd09..b628085d 100644 --- a/crates/opy-rs/src/parser/declarations.rs +++ b/crates/opy-rs/src/parser/declarations.rs @@ -92,16 +92,10 @@ impl Parser<'_> { Ok(name) => name, Err(()) => return false, }; - if self - .expect(TokenKind::Colon, "':' after the enum name") - .is_err() - { + let Ok(body_indent) = + self.expect_block_indent(start.span.start.col, "':' after the enum name") + else { return false; - } - let line_indent = start.span.start.col; - let body_indent = match self.block_indent(line_indent) { - Some(indent) => indent, - None => return false, }; let mut members = Vec::new(); loop { @@ -198,16 +192,10 @@ impl Parser<'_> { if qualified { args.insert(0, "self".to_string()); } - if self - .expect(TokenKind::Colon, "':' after the macro signature") - .is_err() - { + let Ok(body_indent) = + self.expect_block_indent(start.span.start.col, "':' after the macro signature") + else { return false; - } - let line_indent = start.span.start.col; - let body_indent = match self.block_indent(line_indent) { - Some(indent) => indent, - None => return false, }; let body = self.parse_block(body_indent); if !self.allow_macro_redeclaration diff --git a/crates/opy-rs/src/parser/definitions.rs b/crates/opy-rs/src/parser/definitions.rs index c6b30e81..92977142 100644 --- a/crates/opy-rs/src/parser/definitions.rs +++ b/crates/opy-rs/src/parser/definitions.rs @@ -39,16 +39,13 @@ impl Parser<'_> { .max(name_token_span.start.col + 1), ), ); + let line_indent = start.span.start.col; if self - .expect(TokenKind::Colon, "':' after the rule name") + .expect_block_indent(line_indent, "':' after the rule name") .is_err() { return false; } - let line_indent = start.span.start.col; - if self.block_indent(line_indent).is_none() { - return false; - } // OverPy accepts a small amount of indentation drift between rule // directives and actions. The rule itself is still top-level, so any // indentation greater than its column belongs to this rule. @@ -332,16 +329,10 @@ impl Parser<'_> { ); return false; } - if self - .expect(TokenKind::Colon, "':' after the subroutine signature") - .is_err() - { + let Ok(body_indent) = + self.expect_block_indent(start.span.start.col, "':' after the subroutine signature") + else { return false; - } - let line_indent = start.span.start.col; - let body_indent = match self.block_indent(line_indent) { - Some(indent) => indent, - None => return false, }; let mut directives = DirectiveState::default(); loop { diff --git a/crates/opy-rs/src/parser/statements.rs b/crates/opy-rs/src/parser/statements.rs index 21767519..b5fd6f46 100644 --- a/crates/opy-rs/src/parser/statements.rs +++ b/crates/opy-rs/src/parser/statements.rs @@ -360,14 +360,8 @@ impl Parser<'_> { } self.advance(); let iterable = self.parse_expr()?; - if self - .expect(TokenKind::Colon, "':' after the for header") - .is_err() - { - return Err(()); - } - let line_indent = start.span.start.col; - let body_indent = self.block_indent(line_indent).ok_or(())?; + let body_indent = + self.expect_block_indent(start.span.start.col, "':' after the for header")?; let body = self.parse_block(body_indent); Ok(Stmt::For { variable, @@ -380,14 +374,8 @@ impl Parser<'_> { pub(super) fn parse_while(&mut self) -> Result { let start = self.advance(); let condition = self.parse_expr()?; - if self - .expect(TokenKind::Colon, "':' after the while condition") - .is_err() - { - return Err(()); - } - let line_indent = start.span.start.col; - let body_indent = self.block_indent(line_indent).ok_or(())?; + let body_indent = + self.expect_block_indent(start.span.start.col, "':' after the while condition")?; let body = self.parse_block(body_indent); Ok(Stmt::While { condition, @@ -398,10 +386,7 @@ impl Parser<'_> { pub(super) fn parse_do_while(&mut self) -> Result { let start = self.advance(); - if self.expect(TokenKind::Colon, "':' after `do`").is_err() { - return Err(()); - } - let body_indent = self.block_indent(start.span.start.col).ok_or(())?; + let body_indent = self.expect_block_indent(start.span.start.col, "':' after `do`")?; let body = self.parse_block(body_indent); if !self.is_ident("while") { self.error_at_current("expected `while` after the do block".to_string()); @@ -423,13 +408,8 @@ impl Parser<'_> { pub(super) fn parse_switch(&mut self) -> Result { let start = self.advance(); let value = self.parse_expr()?; - if self - .expect(TokenKind::Colon, "':' after the switch value") - .is_err() - { - return Err(()); - } - let body_indent = self.block_indent(start.span.start.col).ok_or(())?; + let body_indent = + self.expect_block_indent(start.span.start.col, "':' after the switch value")?; let mut arms = Vec::new(); loop { self.skip_newlines(); @@ -444,13 +424,8 @@ impl Parser<'_> { if self.is_ident("case") { let case_start = self.advance(); let case_value = self.parse_expr()?; - if self - .expect(TokenKind::Colon, "':' after the case value") - .is_err() - { - return Err(()); - } - let case_body_indent = self.block_indent(body_indent).ok_or(())?; + let case_body_indent = + self.expect_block_indent(body_indent, "':' after the case value")?; let body = self.parse_block(case_body_indent); arms.push(SwitchArm::Case { value: case_value, @@ -459,13 +434,8 @@ impl Parser<'_> { }); } else if self.is_ident("default") { let default_start = self.advance(); - if self - .expect(TokenKind::Colon, "':' after `default`") - .is_err() - { - return Err(()); - } - let default_body_indent = self.block_indent(body_indent).ok_or(())?; + let default_body_indent = + self.expect_block_indent(body_indent, "':' after `default`")?; arms.push(SwitchArm::Default { body: self.parse_block(default_body_indent), span: default_start.span, From 33334ba3e042f7db0f99f978f981d922cef98d9e Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 09:15:28 +0800 Subject: [PATCH 50/67] refactor(opy-rs): share colon body parsing --- crates/opy-rs/src/parser/statements.rs | 45 ++++++++++++-------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/crates/opy-rs/src/parser/statements.rs b/crates/opy-rs/src/parser/statements.rs index b5fd6f46..fdd2c3df 100644 --- a/crates/opy-rs/src/parser/statements.rs +++ b/crates/opy-rs/src/parser/statements.rs @@ -253,13 +253,7 @@ impl Parser<'_> { let start = self.advance(); let line_indent = start.span.start.col; let condition = self.parse_expr()?; - if self - .expect(TokenKind::Colon, "':' after the if condition") - .is_err() - { - return Err(()); - } - let body = self.parse_colon_body(line_indent)?; + let body = self.expect_colon_body(line_indent, "':' after the if condition")?; let continued_inline_body = self.last_colon_body_continued; if continued_inline_body { self.errors.push(OpyError::at( @@ -288,33 +282,25 @@ impl Parser<'_> { Ok(expr) => expr, Err(()) => return Err(()), }; - if self - .expect(TokenKind::Colon, "':' after the elif condition") - .is_err() - { - return Err(()); - } - let body = self.parse_colon_body(branch_start.span.start.col)?; + let body = self.expect_colon_body( + branch_start.span.start.col, + "':' after the elif condition", + )?; branches.push(IfBranch { condition, body }); } else if self.is_ident("else") { let branch_start = self.advance(); if self.is_ident("if") { self.advance(); let condition = self.parse_expr()?; - if self - .expect(TokenKind::Colon, "':' after the else-if condition") - .is_err() - { - return Err(()); - } - let body = self.parse_colon_body(branch_start.span.start.col)?; + let body = self.expect_colon_body( + branch_start.span.start.col, + "':' after the else-if condition", + )?; branches.push(IfBranch { condition, body }); continue; } - if self.expect(TokenKind::Colon, "':' after `else`").is_err() { - return Err(()); - } - let body = self.parse_colon_body(branch_start.span.start.col)?; + let body = + self.expect_colon_body(branch_start.span.start.col, "':' after `else`")?; r#else = Some(body); break; } else { @@ -351,6 +337,15 @@ impl Parser<'_> { } } + fn expect_colon_body( + &mut self, + line_indent: u32, + colon_context: &str, + ) -> Result, ()> { + self.expect(TokenKind::Colon, colon_context)?; + self.parse_colon_body(line_indent) + } + pub(super) fn parse_for(&mut self) -> Result { let start = self.advance(); let variable = self.parse_postfix()?; From b8e7688bce3656abe7fc6fd2a1e7ad0b930d5d4c Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 09:23:53 +0800 Subject: [PATCH 51/67] refactor(opy-rs): share compression eligibility checks --- crates/opy-rs/src/lower/special_forms.rs | 49 +++++++++++------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/crates/opy-rs/src/lower/special_forms.rs b/crates/opy-rs/src/lower/special_forms.rs index 0a3382d2..edb48d07 100644 --- a/crates/opy-rs/src/lower/special_forms.rs +++ b/crates/opy-rs/src/lower/special_forms.rs @@ -31,16 +31,20 @@ fn is_compressible_column(values: &[&Expr]) -> bool { else { return false; }; - let Some(first) = numbers.first() else { + is_compressible_components(&numbers) +} + +fn is_compressible_components(values: &[Vec]) -> bool { + let Some(first) = values.first() else { return false; }; - let is_vector = first.len() == 3; - numbers.iter().all(|value| { - value.len() == first.len() - && (value.len() == 3) == is_vector - && value - .iter() - .all(|component| component.abs() < if is_vector { 4999.0 } else { 49999.0 }) + let limit = match first.len() { + 1 => 49999.0, + 3 => 4999.0, + _ => return false, + }; + values.iter().all(|components| { + components.len() == first.len() && components.iter().all(|value| value.abs() < limit) }) } @@ -336,23 +340,16 @@ impl Lowerer { /// Whether `compressed()` accepts these elements: only numbers, or only /// vectors of numbers. fn is_compressible(values: &[HirExpr]) -> bool { - if values.is_empty() { - return false; - } - let vectors = values - .iter() - .all(|value| matches!(value, HirExpr::Vector { .. })); - if vectors { - return values.iter().all(|value| { - let HirExpr::Vector { x, y, z, .. } = value else { - return false; - }; - [x, y, z].into_iter().all(|component| { - crate::hir::visit::literal_number(component).is_some_and(|v| v.abs() < 4999.0) - }) - }); - } - values + let values = values .iter() - .all(|value| crate::hir::visit::literal_number(value).is_some_and(|v| v.abs() < 49999.0)) + .map(|value| match value { + HirExpr::Vector { x, y, z, .. } => Some(vec![ + crate::hir::visit::literal_number(x)?, + crate::hir::visit::literal_number(y)?, + crate::hir::visit::literal_number(z)?, + ]), + _ => crate::hir::visit::literal_number(value).map(|number| vec![number]), + }) + .collect::>>(); + values.as_deref().is_some_and(is_compressible_components) } From 4e009fccf96bcf9280661fa54bb83dbc14a395c0 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 09:37:56 +0800 Subject: [PATCH 52/67] refactor(opy-rs): share compressed component classification --- crates/opy-rs/src/compiler/lowering/values.rs | 6 ++---- crates/opy-rs/src/lower.rs | 7 +++++++ crates/opy-rs/src/lower/special_forms.rs | 11 ++--------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/crates/opy-rs/src/compiler/lowering/values.rs b/crates/opy-rs/src/compiler/lowering/values.rs index c445abe1..aeac18f1 100644 --- a/crates/opy-rs/src/compiler/lowering/values.rs +++ b/crates/opy-rs/src/compiler/lowering/values.rs @@ -1540,12 +1540,10 @@ impl<'a> Lowering<'a> { else { return Err(self.unsupported("compressed requires literal numbers or vectors", span)); }; - let is_vector = numbers.first().is_some_and(|value| value.len() == 3); - if numbers.iter().any(|value| (value.len() == 3) != is_vector) { + let Some((is_vector, limit)) = crate::lower::compressed_component_mode(&numbers) else { return Err(self.unsupported("compressed cannot mix numbers and vectors", span)); - } + }; let flattened = numbers.iter().flatten().copied().collect::>(); - let limit = if is_vector { 4999.0 } else { 49999.0 }; if flattened.iter().any(|value| value.abs() >= limit) { return Err(self.unsupported("compressed values exceed the supported magnitude", span)); } diff --git a/crates/opy-rs/src/lower.rs b/crates/opy-rs/src/lower.rs index 502cf54a..7230b0fc 100644 --- a/crates/opy-rs/src/lower.rs +++ b/crates/opy-rs/src/lower.rs @@ -112,6 +112,13 @@ mod special_forms; mod statements; mod textures; +pub(crate) fn compressed_component_mode(values: &[Vec]) -> Option<(bool, f64)> { + let component_count = values.first()?.len(); + let is_vector = component_count == 3; + (matches!(component_count, 1 | 3) && values.iter().all(|value| value.len() == component_count)) + .then_some((is_vector, if is_vector { 4999.0 } else { 49999.0 })) +} + /// Lower a parsed program into the Opy HIR contract. pub fn lower( program: &cst::Program, diff --git a/crates/opy-rs/src/lower/special_forms.rs b/crates/opy-rs/src/lower/special_forms.rs index edb48d07..4b442a2e 100644 --- a/crates/opy-rs/src/lower/special_forms.rs +++ b/crates/opy-rs/src/lower/special_forms.rs @@ -35,17 +35,10 @@ fn is_compressible_column(values: &[&Expr]) -> bool { } fn is_compressible_components(values: &[Vec]) -> bool { - let Some(first) = values.first() else { + let Some((_, limit)) = crate::lower::compressed_component_mode(values) else { return false; }; - let limit = match first.len() { - 1 => 49999.0, - 3 => 4999.0, - _ => return false, - }; - values.iter().all(|components| { - components.len() == first.len() && components.iter().all(|value| value.abs() < limit) - }) + values.iter().flatten().all(|value| value.abs() < limit) } impl Lowerer { From 23159bf0f578598ab9e32ea3cff4758b3279ba9c Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 09:43:39 +0800 Subject: [PATCH 53/67] refactor(opy-rs): share declaration visibility check --- crates/opy-rs/src/lower/declarations.rs | 34 ++++++++++++------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/crates/opy-rs/src/lower/declarations.rs b/crates/opy-rs/src/lower/declarations.rs index acbbeb8b..5e2cf1d5 100644 --- a/crates/opy-rs/src/lower/declarations.rs +++ b/crates/opy-rs/src/lower/declarations.rs @@ -1,5 +1,15 @@ use super::*; +fn declaration_visible( + declarations: &HashMap, + name: &str, + current_order: usize, +) -> bool { + declarations + .get(name) + .is_some_and(|order| *order <= current_order) +} + impl Lowerer { pub(super) fn lower_declaration(&mut self, decl: &Decl) -> Option { match decl { @@ -154,9 +164,7 @@ impl Lowerer { } pub(super) fn subroutine_visible(&self, name: &str) -> bool { - self.subroutine_declarations - .get(name) - .is_some_and(|order| *order <= self.current_order) + declaration_visible(&self.subroutine_declarations, name, self.current_order) || self .subroutine_definitions .iter() @@ -164,33 +172,23 @@ impl Lowerer { } pub(super) fn global_visible(&self, name: &str) -> bool { - self.global_declarations - .get(name) - .is_some_and(|order| *order <= self.current_order) + declaration_visible(&self.global_declarations, name, self.current_order) } pub(super) fn player_visible(&self, name: &str) -> bool { - self.player_declarations - .get(name) - .is_some_and(|order| *order <= self.current_order) + declaration_visible(&self.player_declarations, name, self.current_order) } pub(super) fn macro_visible(&self, name: &str) -> bool { - self.macro_declarations - .get(name) - .is_some_and(|order| *order <= self.current_order) + declaration_visible(&self.macro_declarations, name, self.current_order) } pub(super) fn constant_visible(&self, name: &str) -> bool { - self.constant_declarations - .get(name) - .is_some_and(|order| *order <= self.current_order) + declaration_visible(&self.constant_declarations, name, self.current_order) } pub(super) fn enum_visible(&self, name: &str) -> bool { - self.enum_declarations - .get(name) - .is_some_and(|order| *order <= self.current_order) + declaration_visible(&self.enum_declarations, name, self.current_order) } /// A declaration initializer: integer-`0` literal initializers are From 11e889aef07e99d2e21c98e837fba9f8c52cc7f2 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 09:57:26 +0800 Subject: [PATCH 54/67] refactor(opy-rs): borrow macro templates during expansion --- crates/opy-rs/src/compiler/backend.rs | 34 +++++++++++++-------------- crates/opy-rs/src/settings.rs | 4 ++-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/opy-rs/src/compiler/backend.rs b/crates/opy-rs/src/compiler/backend.rs index c1f61d7a..371d9046 100644 --- a/crates/opy-rs/src/compiler/backend.rs +++ b/crates/opy-rs/src/compiler/backend.rs @@ -24,25 +24,24 @@ pub(crate) fn reject_unlowered_directives(hir: &hir::Program) -> Result<(), Inte Ok(()) } -pub(crate) type MacroBindings = HashMap; +pub(crate) type MacroBindings<'a> = HashMap<&'a str, Expr>; -pub(crate) struct MacroExpander { - macros: HashMap, Vec)>, +pub(crate) struct MacroExpander<'a> { + macros: HashMap<&'a str, (&'a [String], &'a [Stmt])>, stack: Vec, /// Relocate expanded spans to the invocation site; only the source /// attribution pass sets it, so diagnostics keep the definition spans. attribute_to_site: bool, } -impl MacroExpander { - pub(crate) fn from_program(program: &hir::Program) -> Self { - let macros = program - .declarations +impl<'a> MacroExpander<'a> { + pub(crate) fn from_declarations(declarations: &'a [hir::Declaration]) -> Self { + let macros = declarations .iter() .filter_map(|declaration| match declaration { hir::Declaration::Macro { name, args, body, .. - } => Some((name.clone(), (args.clone(), body.clone()))), + } => Some((name.as_str(), (args.as_slice(), body.as_slice()))), _ => None, }) .collect(); @@ -69,7 +68,7 @@ fn expand_macros_with( program: &hir::Program, attribute_to_site: bool, ) -> Result { - let mut expander = MacroExpander::from_program(program); + let mut expander = MacroExpander::from_declarations(&program.declarations); expander.attribute_to_site = attribute_to_site; let mut expanded = program.clone(); let bindings = MacroBindings::new(); @@ -104,11 +103,11 @@ fn expand_macros_with( Ok(expanded) } -impl MacroExpander { +impl<'a> MacroExpander<'a> { fn expand_stmts( &mut self, statements: &mut Vec, - bindings: &MacroBindings, + bindings: &MacroBindings<'_>, site: Option, ) -> Result<(), IntegrationError> { let mut expanded = Vec::new(); @@ -135,7 +134,7 @@ impl MacroExpander { fn expand_stmt_inner( &mut self, statement: &mut Stmt, - bindings: &MacroBindings, + bindings: &MacroBindings<'_>, site: Option, ) -> Result<(), IntegrationError> { match statement { @@ -209,7 +208,7 @@ impl MacroExpander { pub(crate) fn expand_expr( &mut self, expression: &Expr, - bindings: &MacroBindings, + bindings: &MacroBindings<'_>, site: Option, ) -> Result { let mut expanded = expression.clone(); @@ -220,12 +219,12 @@ impl MacroExpander { fn expand_expr_in_place( &mut self, expression: &mut Expr, - bindings: &MacroBindings, + bindings: &MacroBindings<'_>, site: Option, ) -> Result<(), IntegrationError> { let replacement = match expression { Expr::MacroParam { name, span } => { - let value = bindings.get(name).ok_or_else(|| { + let value = bindings.get(name.as_str()).ok_or_else(|| { IntegrationError::new( "unsupported-integration-surface", format!("macro parameter '{name}' has no expansion binding"), @@ -392,7 +391,7 @@ impl MacroExpander { span: Option, parent_site: Option, ) -> Result, IntegrationError> { - let Some((params, mut body)) = self.macros.get(name).cloned() else { + let Some((params, template)) = self.macros.get(name) else { return Err(IntegrationError::new( "unsupported-integration-surface", format!("macro '{name}' has no declaration"), @@ -418,11 +417,12 @@ impl MacroExpander { )); } let mut bindings = MacroBindings::new(); - for (param, arg) in params.into_iter().zip(args.iter()) { + for (param, arg) in params.iter().zip(args.iter()) { bindings.insert(param, arg.clone()); } self.stack.push(name.to_string()); let site = parent_site.or_else(|| self.attribute_to_site.then_some(span).flatten()); + let mut body = template.to_vec(); let result = self.expand_stmts(&mut body, &bindings, site).map(|()| body); self.stack.pop(); result diff --git a/crates/opy-rs/src/settings.rs b/crates/opy-rs/src/settings.rs index 556c6298..875226d8 100644 --- a/crates/opy-rs/src/settings.rs +++ b/crates/opy-rs/src/settings.rs @@ -314,7 +314,7 @@ pub(crate) fn resolve_hir_settings( _ => None, }) .collect::>(); - let mut expander = crate::compiler::MacroExpander::from_program(program); + let mut expander = crate::compiler::MacroExpander::from_declarations(&program.declarations); let Some(settings) = program.settings.take() else { return Ok(()); }; @@ -334,7 +334,7 @@ fn resolve_hir_node( node: hir::SettingsNode, cst_program: &cst::Program, constants: &HashMap, - expander: &mut crate::compiler::MacroExpander, + expander: &mut crate::compiler::MacroExpander<'_>, ) -> OpyResult { match node { hir::SettingsNode::Group { From be6f8aab0d9efcb9c044b3c1ca5909b44ebc4c0e Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 10:07:52 +0800 Subject: [PATCH 55/67] refactor(opy-rs): share disabled-rule diagnostics --- crates/opy-rs/src/compiler/reconstruct.rs | 26 +++++++++-------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/crates/opy-rs/src/compiler/reconstruct.rs b/crates/opy-rs/src/compiler/reconstruct.rs index b07ce50c..7bbca37b 100644 --- a/crates/opy-rs/src/compiler/reconstruct.rs +++ b/crates/opy-rs/src/compiler/reconstruct.rs @@ -197,6 +197,14 @@ impl<'a> Emitter<'a> { }); } + fn report_disabled_rule(&mut self, rule_index: usize, name: &str) { + self.issue( + "unsupported-disabled-rule", + format!("rule '{name}' is disabled; the OPY surface cannot express it"), + self.program.rule_span(rule_index), + ); + } + // ---- table validation ---- fn validate_tables(&mut self) { @@ -556,14 +564,7 @@ impl<'a> Emitter<'a> { // Subroutine bodies. for (rule_index, rule) in &layout.sub_rules { if rule.disabled { - self.issue( - "unsupported-disabled-rule", - format!( - "rule '{}' is disabled; the OPY surface cannot express it", - rule.name - ), - self.program.rule_span(*rule_index), - ); + self.report_disabled_rule(*rule_index, &rule.name); continue; } let Event::Subroutine(subroutine_name) = &rule.event else { @@ -587,14 +588,7 @@ impl<'a> Emitter<'a> { // Rules. for (rule_index, rule) in &layout.normal_rules { if rule.disabled { - self.issue( - "unsupported-disabled-rule", - format!( - "rule '{}' is disabled; the OPY surface cannot express it", - rule.name - ), - self.program.rule_span(*rule_index), - ); + self.report_disabled_rule(*rule_index, &rule.name); continue; } if rule.actions.is_empty() { From 367bcbab8e21329deaac5cf73ca1a87ba7cd1347 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 10:17:33 +0800 Subject: [PATCH 56/67] refactor(opy-rs): share variable declaration emission --- crates/opy-rs/src/compiler/reconstruct.rs | 62 +++++++++++------------ 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/crates/opy-rs/src/compiler/reconstruct.rs b/crates/opy-rs/src/compiler/reconstruct.rs index 7bbca37b..593f577e 100644 --- a/crates/opy-rs/src/compiler/reconstruct.rs +++ b/crates/opy-rs/src/compiler/reconstruct.rs @@ -518,38 +518,8 @@ impl<'a> Emitter<'a> { self.check_initializer_slot(&global_initializers); // Declarations. - for (position, variable) in self.program.global_variables.iter().enumerate() { - self.out.push_str("globalvar "); - self.out.push_str(&variable.name); - match global_initializers.get(&position) { - Some(value) => { - self.out.push_str(" = "); - self.emit_initializer(&value.0, value.1); - } - None => { - self.out.push(' '); - self.out - .push_str(&variable.index.unwrap_or(position as u32).to_string()); - } - } - self.out.push('\n'); - } - for (position, variable) in self.program.player_variables.iter().enumerate() { - self.out.push_str("playervar "); - self.out.push_str(&variable.name); - match player_initializers.get(&position) { - Some(value) => { - self.out.push_str(" = "); - self.emit_initializer(&value.0, value.1); - } - None => { - self.out.push(' '); - self.out - .push_str(&variable.index.unwrap_or(position as u32).to_string()); - } - } - self.out.push('\n'); - } + self.emit_variable_declarations(true, &global_initializers); + self.emit_variable_declarations(false, &player_initializers); if self.program.subroutines.is_empty() { self.out.push('\n'); } else { @@ -645,6 +615,34 @@ impl<'a> Emitter<'a> { } } + fn emit_variable_declarations( + &mut self, + global: bool, + initializers: &std::collections::HashMap)>, + ) { + let (kind, variables) = if global { + ("globalvar ", &self.program.global_variables) + } else { + ("playervar ", &self.program.player_variables) + }; + for (position, variable) in variables.iter().enumerate() { + self.out.push_str(kind); + self.out.push_str(&variable.name); + match initializers.get(&position) { + Some(value) => { + self.out.push_str(" = "); + self.emit_initializer(&value.0, value.1); + } + None => { + self.out.push(' '); + self.out + .push_str(&variable.index.unwrap_or(position as u32).to_string()); + } + } + self.out.push('\n'); + } + } + /// Map initializer rule actions onto declaration positions (table order), /// validating the rule's Sets are in table order like the frontend's /// synthesized initializer rule. From ec609083e668ecce51359d039e7a694ceed019e2 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 10:31:19 +0800 Subject: [PATCH 57/67] refactor(opy-rs): centralize HIR program traversal --- crates/opy-rs/src/hir/visit.rs | 34 +++++++++++++++++++++++++++++++++- crates/opy-rs/src/tooling.rs | 34 +--------------------------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/crates/opy-rs/src/hir/visit.rs b/crates/opy-rs/src/hir/visit.rs index 8bb799e1..2d06646b 100644 --- a/crates/opy-rs/src/hir/visit.rs +++ b/crates/opy-rs/src/hir/visit.rs @@ -1,4 +1,4 @@ -use super::types::{Expr, Stmt, SwitchArm}; +use super::types::{Declaration, Expr, Program, RuleEntry, Stmt, SwitchArm}; pub(crate) fn contains_random(expression: &Expr) -> bool { struct RandomFinder(bool); @@ -57,6 +57,38 @@ pub(crate) trait Visitor { } } +pub(crate) fn walk_program(visitor: &mut V, program: &Program) { + for declaration in &program.declarations { + match declaration { + Declaration::GlobalVariable { + initializer: Some(initializer), + .. + } + | Declaration::PlayerVariable { + initializer: Some(initializer), + .. + } => visitor.visit_expr(initializer), + Declaration::Constant { value, .. } => visitor.visit_expr(value), + Declaration::Macro { body, .. } => walk_stmts(visitor, body), + _ => {} + } + } + for entry in &program.rules { + match entry { + RuleEntry::Rule(rule) => { + for argument in &rule.event.args { + visitor.visit_expr(argument); + } + for condition in &rule.conditions { + visitor.visit_expr(condition); + } + walk_stmts(visitor, &rule.actions); + } + RuleEntry::SubroutineDef { body, .. } => walk_stmts(visitor, body), + } + } +} + pub(crate) fn walk_expr(visitor: &mut V, expression: &Expr) { match expression { Expr::Array { elements, .. } => { diff --git a/crates/opy-rs/src/tooling.rs b/crates/opy-rs/src/tooling.rs index 91f67624..cdb21f65 100644 --- a/crates/opy-rs/src/tooling.rs +++ b/crates/opy-rs/src/tooling.rs @@ -483,39 +483,7 @@ impl SemanticModel { } let mut collector = ReferenceSiteCollector { sites: Vec::new() }; - for declaration in &self.hir.declarations { - match declaration { - Declaration::GlobalVariable { - initializer: Some(initializer), - .. - } - | Declaration::PlayerVariable { - initializer: Some(initializer), - .. - } => hir::visit::Visitor::visit_expr(&mut collector, initializer), - Declaration::Constant { value, .. } => { - hir::visit::Visitor::visit_expr(&mut collector, value) - } - Declaration::Macro { body, .. } => hir::visit::walk_stmts(&mut collector, body), - _ => {} - } - } - for entry in &self.hir.rules { - match entry { - RuleEntry::Rule(rule) => { - for argument in &rule.event.args { - hir::visit::Visitor::visit_expr(&mut collector, argument); - } - for condition in &rule.conditions { - hir::visit::Visitor::visit_expr(&mut collector, condition); - } - hir::visit::walk_stmts(&mut collector, &rule.actions); - } - RuleEntry::SubroutineDef { body, .. } => { - hir::visit::walk_stmts(&mut collector, body) - } - } - } + hir::visit::walk_program(&mut collector, &self.hir); for (kind, name, span) in collector.sites { self.attach_reference(kind, &name, span); } From 129c2097536c8694f2f664c80e100190443b146f Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 10:50:27 +0800 Subject: [PATCH 58/67] refactor(opy-rs): centralize HIR span accessors --- crates/opy-rs/src/compiler/backend.rs | 59 +----------- crates/opy-rs/src/hir/types.rs | 127 ++++++++++++++------------ 2 files changed, 74 insertions(+), 112 deletions(-) diff --git a/crates/opy-rs/src/compiler/backend.rs b/crates/opy-rs/src/compiler/backend.rs index 371d9046..e15c3f60 100644 --- a/crates/opy-rs/src/compiler/backend.rs +++ b/crates/opy-rs/src/compiler/backend.rs @@ -123,7 +123,7 @@ impl<'a> MacroExpander<'a> { } self.expand_stmt_inner(&mut statement, bindings, site)?; if let Some(site) = site { - set_stmt_span(&mut statement, site); + *statement.span_mut() = Some(site); } expanded.push(statement); } @@ -269,7 +269,7 @@ impl<'a> MacroExpander<'a> { if let Some(replacement) = replacement { *expression = replacement; if let Some(site) = site { - set_expr_span(expression, site); + *expression.span_mut() = Some(site); } return Ok(()); } @@ -379,7 +379,7 @@ impl<'a> MacroExpander<'a> { | Expr::MacroParam { .. } => {} } if let Some(site) = site { - set_expr_span(expression, site); + *expression.span_mut() = Some(site); } Ok(()) } @@ -428,56 +428,3 @@ impl<'a> MacroExpander<'a> { result } } - -fn set_stmt_span(statement: &mut Stmt, site: HirSpan) { - match statement { - Stmt::Expr { span, .. } - | Stmt::Assign { span, .. } - | Stmt::If { span, .. } - | Stmt::For { span, .. } - | Stmt::While { span, .. } - | Stmt::DoWhile { span, .. } - | Stmt::Switch { span, .. } - | Stmt::Delete { span, .. } - | Stmt::Goto { span, .. } - | Stmt::Break { span } - | Stmt::Return { span } - | Stmt::Continue { span } - | Stmt::Label { span, .. } - | Stmt::CallSubroutine { span, .. } - | Stmt::Pass { span } => *span = Some(site), - } -} - -fn set_expr_span(expression: &mut Expr, site: HirSpan) { - match expression { - Expr::Number { span, .. } - | Expr::String { span, .. } - | Expr::Bool { span, .. } - | Expr::Null { span } - | Expr::StringModifier { span, .. } - | Expr::Local { span, .. } - | Expr::Enum { span, .. } - | Expr::GlobalVar { span, .. } - | Expr::HostPlayer { span } - | Expr::EventPlayer { span } - | Expr::Constant { span, .. } - | Expr::MacroParam { span, .. } - | Expr::Array { span, .. } - | Expr::Dict { span, .. } - | Expr::Comprehension { span, .. } - | Expr::Lambda { span, .. } - | Expr::Type { span, .. } - | Expr::Vector { span, .. } - | Expr::PlayerVar { span, .. } - | Expr::Member { span, .. } - | Expr::Call { span, .. } - | Expr::ReceiverCall { span, .. } - | Expr::Binary { span, .. } - | Expr::Conditional { span, .. } - | Expr::Unary { span, .. } - | Expr::Index { span, .. } - | Expr::Format { span, .. } - | Expr::MacroCall { span, .. } => *span = Some(site), - } -} diff --git a/crates/opy-rs/src/hir/types.rs b/crates/opy-rs/src/hir/types.rs index ead495b7..c6c7ac81 100644 --- a/crates/opy-rs/src/hir/types.rs +++ b/crates/opy-rs/src/hir/types.rs @@ -10,6 +10,25 @@ use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; +macro_rules! impl_span_accessors { + ($ty:ident, $description:literal, $($variant:ident),+ $(,)?) => { + impl $ty { + #[doc = $description] + pub fn span(&self) -> Option<&Span> { + match self { + $(Self::$variant { span, .. } => span.as_ref(),)+ + } + } + + pub(crate) fn span_mut(&mut self) -> &mut Option { + match self { + $(Self::$variant { span, .. } => span,)+ + } + } + } + }; +} + /// The `wright/opy-hir` protocol name. pub const PROTOCOL_NAME: &str = "wright/opy-hir"; /// The protocol major version this consumer understands. @@ -537,28 +556,25 @@ pub struct IfBranch { pub body: Vec, } -impl Stmt { - /// The source span of this statement, if any. - pub fn span(&self) -> Option<&Span> { - match self { - Stmt::Expr { span, .. } - | Stmt::Assign { span, .. } - | Stmt::If { span, .. } - | Stmt::For { span, .. } - | Stmt::While { span, .. } - | Stmt::DoWhile { span, .. } - | Stmt::Switch { span, .. } - | Stmt::Delete { span, .. } - | Stmt::Break { span } - | Stmt::Return { span } - | Stmt::Continue { span } - | Stmt::Goto { span, .. } - | Stmt::Label { span, .. } - | Stmt::CallSubroutine { span, .. } - | Stmt::Pass { span } => span.as_ref(), - } - } -} +impl_span_accessors!( + Stmt, + "The source span of this statement, if any.", + Expr, + Assign, + If, + For, + While, + DoWhile, + Switch, + Delete, + Break, + Return, + Continue, + Goto, + Label, + CallSubroutine, + Pass, +); /// One source-ordered arm in the OPY HIR. Arms execute in source order and /// fall through to subsequent arms until a `break` statement is encountered. @@ -787,40 +803,6 @@ pub struct DictEntry { } impl Expr { - /// The source span of this expression, if any. - pub fn span(&self) -> Option<&Span> { - match self { - Expr::Number { span, .. } - | Expr::String { span, .. } - | Expr::Bool { span, .. } - | Expr::Null { span } - | Expr::Array { span, .. } - | Expr::Dict { span, .. } - | Expr::Comprehension { span, .. } - | Expr::Lambda { span, .. } - | Expr::StringModifier { span, .. } - | Expr::Local { span, .. } - | Expr::Vector { span, .. } - | Expr::Enum { span, .. } - | Expr::GlobalVar { span, .. } - | Expr::PlayerVar { span, .. } - | Expr::HostPlayer { span } - | Expr::Member { span, .. } - | Expr::EventPlayer { span } - | Expr::Constant { span, .. } - | Expr::Call { span, .. } - | Expr::ReceiverCall { span, .. } - | Expr::MacroCall { span, .. } - | Expr::MacroParam { span, .. } - | Expr::Type { span, .. } - | Expr::Binary { span, .. } - | Expr::Conditional { span, .. } - | Expr::Unary { span, .. } - | Expr::Index { span, .. } - | Expr::Format { span, .. } => span.as_ref(), - } - } - /// The protocol `kind` of this expression. pub fn kind_name(&self) -> &'static str { match self { @@ -855,3 +837,36 @@ impl Expr { } } } + +impl_span_accessors!( + Expr, + "The source span of this expression, if any.", + Number, + String, + Bool, + Null, + Array, + Dict, + Comprehension, + Lambda, + StringModifier, + Local, + Vector, + Enum, + GlobalVar, + PlayerVar, + HostPlayer, + Member, + EventPlayer, + Constant, + Call, + ReceiverCall, + MacroCall, + MacroParam, + Type, + Binary, + Conditional, + Unary, + Index, + Format, +); From 0ffdfea4fc1ae1d9c88d61d8d5331faf7dac2ce7 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 11:07:26 +0800 Subject: [PATCH 59/67] refactor(opy-rs): share delimited expression parsing --- crates/opy-rs/src/parser/expressions.rs | 156 +++++++++++------------- 1 file changed, 71 insertions(+), 85 deletions(-) diff --git a/crates/opy-rs/src/parser/expressions.rs b/crates/opy-rs/src/parser/expressions.rs index 7ff26eda..da1a3eb8 100644 --- a/crates/opy-rs/src/parser/expressions.rs +++ b/crates/opy-rs/src/parser/expressions.rs @@ -329,74 +329,69 @@ impl Parser<'_> { self.peek_kind() != closing } + fn parse_delimited_items( + &mut self, + items: &mut Vec, + first_item_parsed: bool, + closing: TokenKind, + closing_text: &str, + mut parse_item: impl FnMut(&mut Self) -> Result, + ) -> Result { + self.skip_newlines(); + if !first_item_parsed && self.peek_kind() == closing { + return Ok(self.advance()); + } + if !first_item_parsed { + items.push(parse_item(self)?); + } + while self.has_more_delimited_items(closing) { + items.push(parse_item(self)?); + } + self.expect(closing, closing_text) + } + /// `@Event name(args)`: positional expressions only (keyword arguments /// are a call-argument form, not an event form). pub(super) fn parse_event_args(&mut self, args: &mut Vec) -> Result<(), ()> { self.expect(TokenKind::LParen, "'('")?; - self.skip_newlines(); - if self.peek_kind() == TokenKind::RParen { - self.advance(); - return Ok(()); - } - loop { - let expr = self.parse_expr()?; - if self.peek_kind() == TokenKind::Assign { - self.error_at_current("keyword arguments are not valid in @Event".to_string()); + self.parse_delimited_items(args, false, TokenKind::RParen, "')'", |parser| { + let expr = parser.parse_expr()?; + if parser.peek_kind() == TokenKind::Assign { + parser.error_at_current("keyword arguments are not valid in @Event".to_string()); return Err(()); } - args.push(expr); - if !self.has_more_delimited_items(TokenKind::RParen) { - break; - } - } - self.expect(TokenKind::RParen, "')'")?; + Ok(expr) + })?; Ok(()) } pub(super) fn parse_call_args(&mut self, args: &mut Vec) -> Result<(), ()> { self.expect(TokenKind::LParen, "'('")?; - self.skip_newlines(); - if self.peek_kind() == TokenKind::RParen { - self.advance(); - return Ok(()); - } - loop { - match self.parse_expr() { - Ok(expr) => { - // A keyword argument is `name = expr` (issue #110): a - // bare identifier immediately followed by `=`. Anything - // else (`expr = ...`) is not a call argument form and is - // rejected like the pinned reference rejects it. - if self.peek_kind() == TokenKind::Assign { - let Expr::Name { name, span } = expr else { - self.error_at_current( - "expected a keyword name before '=' in this call".to_string(), - ); - return Err(()); - }; - self.advance(); - let value = match self.parse_expr() { - Ok(value) => value, - Err(()) => return Err(()), - }; - args.push(CallArg { - keyword: Some((name, span)), - value, - }); - } else { - args.push(CallArg { - keyword: None, - value: expr, - }); - } - } - Err(()) => return Err(()), - } - if !self.has_more_delimited_items(TokenKind::RParen) { - break; + self.parse_delimited_items(args, false, TokenKind::RParen, "')'", |parser| { + let expr = parser.parse_expr()?; + // A keyword argument is `name = expr` (issue #110): a bare + // identifier immediately followed by `=`. Anything else is + // rejected like the pinned reference rejects it. + if parser.peek_kind() == TokenKind::Assign { + let Expr::Name { name, span } = expr else { + parser.error_at_current( + "expected a keyword name before '=' in this call".to_string(), + ); + return Err(()); + }; + parser.advance(); + let value = parser.parse_expr()?; + Ok(CallArg { + keyword: Some((name, span)), + value, + }) + } else { + Ok(CallArg { + keyword: None, + value: expr, + }) } - } - self.expect(TokenKind::RParen, "')'")?; + })?; Ok(()) } @@ -522,16 +517,16 @@ impl Parser<'_> { }); } elements.push(first); - loop { - if !self.has_more_delimited_items(TokenKind::RBracket) { - break; - } - elements.push(self.parse_expr()?); - } - let end = match self.expect(TokenKind::RBracket, "']'") { - Ok(token) => token.span.end, - Err(()) => return Err(()), - }; + let end = self + .parse_delimited_items( + &mut elements, + true, + TokenKind::RBracket, + "']'", + Self::parse_expr, + )? + .span + .end; Ok(Expr::Array { elements, span: Span::new(open.span.file, open.span.start, end), @@ -595,25 +590,16 @@ impl Parser<'_> { pub(super) fn parse_dict(&mut self) -> Result { let open = self.advance(); let mut entries = Vec::new(); - self.skip_newlines(); - if self.peek_kind() == TokenKind::RBrace { - let end = self.advance().span.end; - return Ok(Expr::Dict { - entries, - span: Span::new(open.span.file, open.span.start, end), - }); - } - loop { - let key = self.parse_expr()?; - self.expect(TokenKind::Colon, "':' in a dictionary entry")?; - let value = self.parse_expr()?; - let span = Span::new(key.span().file, key.span().start, value.span().end); - entries.push(DictEntry { key, value, span }); - if !self.has_more_delimited_items(TokenKind::RBrace) { - break; - } - } - let end = self.expect(TokenKind::RBrace, "'}'")?.span.end; + let end = self + .parse_delimited_items(&mut entries, false, TokenKind::RBrace, "'}'", |parser| { + let key = parser.parse_expr()?; + parser.expect(TokenKind::Colon, "':' in a dictionary entry")?; + let value = parser.parse_expr()?; + let span = Span::new(key.span().file, key.span().start, value.span().end); + Ok(DictEntry { key, value, span }) + })? + .span + .end; Ok(Expr::Dict { entries, span: Span::new(open.span.file, open.span.start, end), From 60fd207e78d5907a854d9ad39cdd165d0dcc5275 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 11:19:46 +0800 Subject: [PATCH 60/67] refactor(opy-rs): keep HIR span accessors explicit --- crates/opy-rs/src/hir/types.rs | 180 ++++++++++++++++++++------------- 1 file changed, 109 insertions(+), 71 deletions(-) diff --git a/crates/opy-rs/src/hir/types.rs b/crates/opy-rs/src/hir/types.rs index c6c7ac81..35ba2b27 100644 --- a/crates/opy-rs/src/hir/types.rs +++ b/crates/opy-rs/src/hir/types.rs @@ -10,25 +10,6 @@ use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; -macro_rules! impl_span_accessors { - ($ty:ident, $description:literal, $($variant:ident),+ $(,)?) => { - impl $ty { - #[doc = $description] - pub fn span(&self) -> Option<&Span> { - match self { - $(Self::$variant { span, .. } => span.as_ref(),)+ - } - } - - pub(crate) fn span_mut(&mut self) -> &mut Option { - match self { - $(Self::$variant { span, .. } => span,)+ - } - } - } - }; -} - /// The `wright/opy-hir` protocol name. pub const PROTOCOL_NAME: &str = "wright/opy-hir"; /// The protocol major version this consumer understands. @@ -556,25 +537,48 @@ pub struct IfBranch { pub body: Vec, } -impl_span_accessors!( - Stmt, - "The source span of this statement, if any.", - Expr, - Assign, - If, - For, - While, - DoWhile, - Switch, - Delete, - Break, - Return, - Continue, - Goto, - Label, - CallSubroutine, - Pass, -); +impl Stmt { + /// The source span of this statement, if any. + pub fn span(&self) -> Option<&Span> { + match self { + Stmt::Expr { span, .. } + | Stmt::Assign { span, .. } + | Stmt::If { span, .. } + | Stmt::For { span, .. } + | Stmt::While { span, .. } + | Stmt::DoWhile { span, .. } + | Stmt::Switch { span, .. } + | Stmt::Delete { span, .. } + | Stmt::Break { span } + | Stmt::Return { span } + | Stmt::Continue { span } + | Stmt::Goto { span, .. } + | Stmt::Label { span, .. } + | Stmt::CallSubroutine { span, .. } + | Stmt::Pass { span } => span.as_ref(), + } + } + + pub(crate) fn span_mut(&mut self) -> &mut Option { + match self { + Stmt::Expr { span, .. } + | Stmt::Assign { span, .. } + | Stmt::If { span, .. } + | Stmt::For { span, .. } + | Stmt::While { span, .. } + | Stmt::DoWhile { span, .. } + | Stmt::Switch { span, .. } + | Stmt::Delete { span, .. } + | Stmt::Break { span } + | Stmt::Return { span } + | Stmt::Continue { span } + | Stmt::Goto { span, .. } + | Stmt::Label { span, .. } + | Stmt::CallSubroutine { span, .. } + | Stmt::Pass { span } => span, + } + } +} /// One source-ordered arm in the OPY HIR. Arms execute in source order and /// fall through to subsequent arms until a `break` statement is encountered. @@ -803,6 +807,73 @@ pub struct DictEntry { } impl Expr { + /// The source span of this expression, if any. + pub fn span(&self) -> Option<&Span> { + match self { + Expr::Number { span, .. } + | Expr::String { span, .. } + | Expr::Bool { span, .. } + | Expr::Null { span } + | Expr::Array { span, .. } + | Expr::Dict { span, .. } + | Expr::Comprehension { span, .. } + | Expr::Lambda { span, .. } + | Expr::StringModifier { span, .. } + | Expr::Local { span, .. } + | Expr::Vector { span, .. } + | Expr::Enum { span, .. } + | Expr::GlobalVar { span, .. } + | Expr::PlayerVar { span, .. } + | Expr::HostPlayer { span } + | Expr::Member { span, .. } + | Expr::EventPlayer { span } + | Expr::Constant { span, .. } + | Expr::Call { span, .. } + | Expr::ReceiverCall { span, .. } + | Expr::MacroCall { span, .. } + | Expr::MacroParam { span, .. } + | Expr::Type { span, .. } + | Expr::Binary { span, .. } + | Expr::Conditional { span, .. } + | Expr::Unary { span, .. } + | Expr::Index { span, .. } + | Expr::Format { span, .. } => span.as_ref(), + } + } + + pub(crate) fn span_mut(&mut self) -> &mut Option { + match self { + Expr::Number { span, .. } + | Expr::String { span, .. } + | Expr::Bool { span, .. } + | Expr::Null { span } + | Expr::Array { span, .. } + | Expr::Dict { span, .. } + | Expr::Comprehension { span, .. } + | Expr::Lambda { span, .. } + | Expr::StringModifier { span, .. } + | Expr::Local { span, .. } + | Expr::Vector { span, .. } + | Expr::Enum { span, .. } + | Expr::GlobalVar { span, .. } + | Expr::PlayerVar { span, .. } + | Expr::HostPlayer { span } + | Expr::Member { span, .. } + | Expr::EventPlayer { span } + | Expr::Constant { span, .. } + | Expr::Call { span, .. } + | Expr::ReceiverCall { span, .. } + | Expr::MacroCall { span, .. } + | Expr::MacroParam { span, .. } + | Expr::Type { span, .. } + | Expr::Binary { span, .. } + | Expr::Conditional { span, .. } + | Expr::Unary { span, .. } + | Expr::Index { span, .. } + | Expr::Format { span, .. } => span, + } + } + /// The protocol `kind` of this expression. pub fn kind_name(&self) -> &'static str { match self { @@ -837,36 +908,3 @@ impl Expr { } } } - -impl_span_accessors!( - Expr, - "The source span of this expression, if any.", - Number, - String, - Bool, - Null, - Array, - Dict, - Comprehension, - Lambda, - StringModifier, - Local, - Vector, - Enum, - GlobalVar, - PlayerVar, - HostPlayer, - Member, - EventPlayer, - Constant, - Call, - ReceiverCall, - MacroCall, - MacroParam, - Type, - Binary, - Conditional, - Unary, - Index, - Format, -); From e80ada80da76ea0f444ee4a455e44fbe12cc39f7 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 11:41:22 +0800 Subject: [PATCH 61/67] refactor(opy-rs): share indexed expression decomposition --- crates/opy-rs/src/compiler/lowering.rs | 14 -------------- crates/opy-rs/src/compiler/lowering/assignments.rs | 12 +++++------- crates/opy-rs/src/compiler/lowering/rules.rs | 4 +--- crates/opy-rs/src/hir/visit.rs | 11 +++++++++++ crates/opy-rs/src/lower.rs | 11 ----------- crates/opy-rs/src/lower/statements.rs | 3 ++- 6 files changed, 19 insertions(+), 36 deletions(-) diff --git a/crates/opy-rs/src/compiler/lowering.rs b/crates/opy-rs/src/compiler/lowering.rs index edb61e4a..8a873291 100644 --- a/crates/opy-rs/src/compiler/lowering.rs +++ b/crates/opy-rs/src/compiler/lowering.rs @@ -1235,20 +1235,6 @@ fn literal_key_matches(left: &hir::Expr, right: &hir::Expr) -> bool { } } -fn indexed_target_parts<'a>( - target: &'a hir::Expr, - indices: &mut Vec<&'a hir::Expr>, -) -> Option<&'a hir::Expr> { - match target { - hir::Expr::Index { array, index, .. } => { - indices.push(index); - indexed_target_parts(array, indices) - } - hir::Expr::GlobalVar { .. } | hir::Expr::PlayerVar { .. } => Some(target), - _ => None, - } -} - fn is_literal_key(expr: &hir::Expr) -> bool { matches!( expr, diff --git a/crates/opy-rs/src/compiler/lowering/assignments.rs b/crates/opy-rs/src/compiler/lowering/assignments.rs index 88819edc..6be8bc39 100644 --- a/crates/opy-rs/src/compiler/lowering/assignments.rs +++ b/crates/opy-rs/src/compiler/lowering/assignments.rs @@ -6,17 +6,16 @@ impl<'a> Lowering<'a> { target: &Expr, span: Option, ) -> Result { - let mut indices = Vec::new(); - let Some(root) = indexed_target_parts(target, &mut indices) else { + let (root, indices) = hir::visit::indexed_expr_parts(target); + if !matches!(root, Expr::GlobalVar { .. } | Expr::PlayerVar { .. }) { return Err(self.unsupported( "delete statements require an indexed global or player variable", span, )); - }; + } if indices.len() > 4 { return Err(self.unsupported("Cannot delete index of 4d array", span)); } - indices.reverse(); if hir::visit::has_random_nested_delete(root, &indices) { return Err(self.unsupported( "Cannot delete from nested array with a random outer or middle index", @@ -141,13 +140,12 @@ impl<'a> Lowering<'a> { value: &Expr, span: Option, ) -> Result { - let mut indices = Vec::new(); - if let Some(root) = indexed_target_parts(target, &mut indices) { + let (root, indices) = hir::visit::indexed_expr_parts(target); + if matches!(root, Expr::GlobalVar { .. } | Expr::PlayerVar { .. }) { if indices.len() > 3 { return Err(self.unsupported("Cannot assign to 4d array", target.span().copied())); } if indices.len() > 1 { - indices.reverse(); return self.lower_nested_indexed_assign(root, &indices, target, value); } } diff --git a/crates/opy-rs/src/compiler/lowering/rules.rs b/crates/opy-rs/src/compiler/lowering/rules.rs index 75925f15..dfb570fa 100644 --- a/crates/opy-rs/src/compiler/lowering/rules.rs +++ b/crates/opy-rs/src/compiler/lowering/rules.rs @@ -1056,9 +1056,7 @@ impl<'a> Lowering<'a> { } } Stmt::Delete { target, .. } => { - let mut indices = Vec::new(); - let _ = indexed_target_parts(target, &mut indices); - indices.reverse(); + let (_, indices) = hir::visit::indexed_expr_parts(target); for action in actions { let spans = match self.actions.get(*action) { Some(Action::SetGlobalVariable { .. }) diff --git a/crates/opy-rs/src/hir/visit.rs b/crates/opy-rs/src/hir/visit.rs index 2d06646b..2c54996f 100644 --- a/crates/opy-rs/src/hir/visit.rs +++ b/crates/opy-rs/src/hir/visit.rs @@ -25,6 +25,17 @@ pub(crate) fn contains_random(expression: &Expr) -> bool { finder.0 } +pub(crate) fn indexed_expr_parts(expression: &Expr) -> (&Expr, Vec<&Expr>) { + let mut indices = Vec::new(); + let mut root = expression; + while let Expr::Index { array, index, .. } = root { + indices.push(index.as_ref()); + root = array; + } + indices.reverse(); + (root, indices) +} + pub(crate) fn literal_number(expression: &Expr) -> Option { match expression { Expr::Null { .. } => Some(0.0), diff --git a/crates/opy-rs/src/lower.rs b/crates/opy-rs/src/lower.rs index 7230b0fc..b19b778f 100644 --- a/crates/opy-rs/src/lower.rs +++ b/crates/opy-rs/src/lower.rs @@ -923,17 +923,6 @@ fn indexed_expr_depth(expr: &Expr) -> usize { } } -fn has_random_nested_delete(target: &HirExpr) -> bool { - let mut indices = Vec::new(); - let mut root = target; - while let HirExpr::Index { array, index, .. } = root { - indices.push(index.as_ref()); - root = array.as_ref(); - } - indices.reverse(); - crate::hir::visit::has_random_nested_delete(root, &indices) -} - impl From for HirSpan { fn from(span: Span) -> HirSpan { HirSpan { diff --git a/crates/opy-rs/src/lower/statements.rs b/crates/opy-rs/src/lower/statements.rs index a3a2e6d6..6ea7ccf2 100644 --- a/crates/opy-rs/src/lower/statements.rs +++ b/crates/opy-rs/src/lower/statements.rs @@ -240,7 +240,8 @@ impl Lowerer { ); } let target = self.lower_expr(target, macro_params, CallPosition::Value); - if has_random_nested_delete(&target) { + let (root, indices) = crate::hir::visit::indexed_expr_parts(&target); + if crate::hir::visit::has_random_nested_delete(root, &indices) { self.error_at( "random-indexed-delete", "Cannot delete from nested array with a random outer or middle index" From 481be7ec215c4439a2d0c2d1f5445eb29160551d Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 11:53:00 +0800 Subject: [PATCH 62/67] refactor(opy-rs): simplify synthetic texture rule --- crates/opy-rs/src/lower.rs | 267 +++++++++++++++++-------------------- 1 file changed, 120 insertions(+), 147 deletions(-) diff --git a/crates/opy-rs/src/lower.rs b/crates/opy-rs/src/lower.rs index b19b778f..4b6ef633 100644 --- a/crates/opy-rs/src/lower.rs +++ b/crates/opy-rs/src/lower.rs @@ -270,174 +270,147 @@ fn texture_setup_rule() -> RuleEntry { }, conditions: Vec::new(), actions: vec![ - HirStmt::Expr { - expr: Box::new(HirExpr::Call { - name: "createDummy".to_string(), - args: vec![ - HirExpr::Call { - name: "getAllHeroes".to_string(), - args: Vec::new(), - debug_source: None, - span: None, - }, - texture_dummy_team(), - HirExpr::Bool { - value: false, - span: None, - }, - HirExpr::Null { span: None }, - HirExpr::Null { span: None }, - ], - debug_source: None, - span: None, - }), - span: None, - }, - HirStmt::Expr { - expr: Box::new(HirExpr::ReceiverCall { - receiver: Box::new(texture_dummy_player()), - name: "startForcingName".to_string(), - args: vec![texture_marker('\u{303c}')], - span: None, - }), - span: None, - }, - HirStmt::Assign { - target: Box::new(HirExpr::GlobalVar { - name: "__holygrail__".to_string(), - span: None, - }), - value: Box::new(HirExpr::ReceiverCall { - receiver: Box::new(texture_dummy_first_value()), - name: "split".to_string(), - args: vec![HirExpr::Array { - elements: Vec::new(), - span: None, - }], - span: None, - }), - span: None, - }, - HirStmt::Expr { - expr: Box::new(HirExpr::ReceiverCall { - receiver: Box::new(texture_dummy_player()), - name: "startForcingName".to_string(), - args: vec![texture_marker('\u{840}')], - span: None, - }), - span: None, - }, - HirStmt::Assign { - target: Box::new(HirExpr::GlobalVar { - name: "__holygrail__".to_string(), - span: None, - }), - value: Box::new(HirExpr::ReceiverCall { - receiver: Box::new(HirExpr::ReceiverCall { - receiver: Box::new(texture_marker('\u{303c}')), - name: "replace".to_string(), - args: vec![ - HirExpr::GlobalVar { - name: "__holygrail__".to_string(), - span: None, - }, - texture_dummy_first_value(), - ], - span: None, - }), - name: "substring".to_string(), - args: vec![ - HirExpr::Number { - value: 126.0, - text: "126".to_string(), - span: None, - }, - HirExpr::Bool { - value: true, - span: None, - }, - ], - span: None, - }), - span: None, - }, - HirStmt::Expr { - expr: Box::new(HirExpr::Call { - name: "destroyAllDummies".to_string(), - args: Vec::new(), - debug_source: None, + synthetic_statement(synthetic_call( + "createDummy", + vec![ + synthetic_call("getAllHeroes", Vec::new()), + texture_dummy_team(), + synthetic_boolean(false), + HirExpr::Null { span: None }, + HirExpr::Null { span: None }, + ], + )), + synthetic_statement(synthetic_method( + texture_dummy_player(), + "startForcingName", + vec![texture_marker('\u{303c}')], + )), + texture_state_assignment(synthetic_method( + texture_dummy_first_value(), + "split", + vec![HirExpr::Array { + elements: Vec::new(), span: None, - }), - span: None, - }, + }], + )), + synthetic_statement(synthetic_method( + texture_dummy_player(), + "startForcingName", + vec![texture_marker('\u{840}')], + )), + texture_state_assignment(synthetic_method( + synthetic_method( + texture_marker('\u{303c}'), + "replace", + vec![texture_state_variable(), texture_dummy_first_value()], + ), + "substring", + vec![synthetic_number(126.0), synthetic_boolean(true)], + )), + synthetic_statement(synthetic_call("destroyAllDummies", Vec::new())), ], }) } -fn texture_dummy_player() -> HirExpr { +fn synthetic_call(name: &str, args: Vec) -> HirExpr { HirExpr::Call { - name: "lastCreatedEntity".to_string(), - args: Vec::new(), + name: name.to_string(), + args, debug_source: None, span: None, } } -fn texture_dummy_first_value() -> HirExpr { +fn synthetic_method(receiver: HirExpr, name: &str, args: Vec) -> HirExpr { + HirExpr::ReceiverCall { + receiver: Box::new(receiver), + name: name.to_string(), + args, + span: None, + } +} + +fn synthetic_enum(value_type: &str, value: &str) -> HirExpr { + HirExpr::Enum { + value_type: value_type.to_string(), + value: value.to_string(), + span: None, + } +} + +fn synthetic_number(value: f64) -> HirExpr { + HirExpr::Number { + value, + text: value.to_string(), + span: None, + } +} + +fn synthetic_boolean(value: bool) -> HirExpr { + HirExpr::Bool { value, span: None } +} + +fn synthetic_index(array: HirExpr, index: HirExpr) -> HirExpr { HirExpr::Index { - array: Box::new(texture_dummy_player()), - index: Box::new(HirExpr::Number { - value: 0.0, - text: "0".to_string(), - span: None, - }), + array: Box::new(array), + index: Box::new(index), span: None, } } -fn texture_dummy_team() -> HirExpr { +fn synthetic_conditional(then_value: HirExpr, condition: HirExpr, else_value: HirExpr) -> HirExpr { HirExpr::Conditional { - then_value: Box::new(HirExpr::Enum { - value_type: "Team".to_string(), - value: "TEAM_1".to_string(), - span: None, - }), - condition: Box::new(HirExpr::Call { - name: "getNumberOfSlots".to_string(), - args: vec![HirExpr::Enum { - value_type: "Team".to_string(), - value: "TEAM_1".to_string(), - span: None, - }], - debug_source: None, - span: None, - }), - else_value: Box::new(HirExpr::Conditional { - then_value: Box::new(HirExpr::Enum { - value_type: "Team".to_string(), - value: "TEAM_2".to_string(), - span: None, - }), - condition: Box::new(HirExpr::Call { - name: "getNumberOfSlots".to_string(), - args: vec![HirExpr::Enum { - value_type: "Team".to_string(), - value: "TEAM_2".to_string(), - span: None, - }], - debug_source: None, - span: None, - }), - else_value: Box::new(HirExpr::Bool { - value: true, - span: None, - }), - span: None, - }), + then_value: Box::new(then_value), + condition: Box::new(condition), + else_value: Box::new(else_value), span: None, } } +fn synthetic_statement(expr: HirExpr) -> HirStmt { + HirStmt::Expr { + expr: Box::new(expr), + span: None, + } +} + +fn texture_state_variable() -> HirExpr { + HirExpr::GlobalVar { + name: "__holygrail__".to_string(), + span: None, + } +} + +fn texture_state_assignment(value: HirExpr) -> HirStmt { + HirStmt::Assign { + target: Box::new(texture_state_variable()), + value: Box::new(value), + span: None, + } +} + +fn texture_dummy_player() -> HirExpr { + synthetic_call("lastCreatedEntity", Vec::new()) +} + +fn texture_dummy_first_value() -> HirExpr { + synthetic_index(texture_dummy_player(), synthetic_number(0.0)) +} + +fn texture_dummy_team() -> HirExpr { + let team_1 = synthetic_enum("Team", "TEAM_1"); + let team_2 = synthetic_enum("Team", "TEAM_2"); + synthetic_conditional( + team_1.clone(), + synthetic_call("getNumberOfSlots", vec![team_1]), + synthetic_conditional( + team_2.clone(), + synthetic_call("getNumberOfSlots", vec![team_2]), + synthetic_boolean(true), + ), + ) +} + fn texture_marker(suffix: char) -> HirExpr { HirExpr::String { value: format!("{}{}", "_".repeat(126), suffix), From c9217c0813e6574442eba9d69028172c1da75466 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 12:02:17 +0800 Subject: [PATCH 63/67] refactor(opy-rs): remove unused JavaScript engine trait --- crates/opy-rs/src/macro_js/engine/mod.rs | 34 ------------------- .../opy-rs/src/macro_js/engine/quickjs_ng.rs | 26 +++++++------- crates/opy-rs/src/macro_js/mod.rs | 7 ++-- crates/opy-rs/src/macro_js/runtime.rs | 2 +- 4 files changed, 17 insertions(+), 52 deletions(-) diff --git a/crates/opy-rs/src/macro_js/engine/mod.rs b/crates/opy-rs/src/macro_js/engine/mod.rs index 35faebcc..b6668154 100644 --- a/crates/opy-rs/src/macro_js/engine/mod.rs +++ b/crates/opy-rs/src/macro_js/engine/mod.rs @@ -1,9 +1,6 @@ pub(crate) mod quickjs_ng; use std::fmt; -use std::time::Instant; - -use super::limits::Limits; /// Result of a script evaluation: either the string completion value or a /// non-string value described by its ECMAScript `typeof` name. @@ -32,34 +29,3 @@ impl fmt::Display for EngineError { } } } - -/// Minimal contract an embedded JavaScript engine must satisfy. -/// -/// Implementations must be single-threaded: an engine is created, used, and -/// dropped on one thread. -pub(crate) trait JsEngine { - /// Creates an engine with `limits` applied at the runtime level (memory - /// and stack limits must be enforced by the engine itself). - fn new(limits: &Limits) -> Result - where - Self: Sized; - - /// Installs a captured `console.log` on the global object. Subsequent - /// `console.log(...)` calls append rendered lines to the engine's output - /// buffer, visible through [`JsEngine::console_output`]. - fn install_console(&mut self) -> Result<(), EngineError>; - - /// Evaluates `source` as a global-scope script named `filename` and - /// returns its completion value. - /// - /// Exceptions are reported with the exception's message and stack; the - /// stack's `filename:line:column` frames refer to `source` as passed. - fn evaluate(&mut self, source: &str, filename: &str) -> Result; - - /// Arms (or disarms) the wall-clock interrupt deadline. Once the deadline - /// passes, the next interrupt poll inside the engine aborts execution. - fn set_interrupt_deadline(&mut self, deadline: Option); - - /// `console.log` lines captured since the engine was created. - fn console_output(&self) -> &[String]; -} diff --git a/crates/opy-rs/src/macro_js/engine/quickjs_ng.rs b/crates/opy-rs/src/macro_js/engine/quickjs_ng.rs index 78cc9b15..aef78c78 100644 --- a/crates/opy-rs/src/macro_js/engine/quickjs_ng.rs +++ b/crates/opy-rs/src/macro_js/engine/quickjs_ng.rs @@ -10,7 +10,7 @@ //! This module owns the raw `JSRuntime`/`JSContext` pointers and never shares //! them across threads. The interrupt handler and the `console.log` native //! function are armed only while the engine is at a stable address inside -//! [`evaluate`](JsEngine::evaluate), and they read engine-owned state through +//! [`QuickJsEngine::evaluate`], and they read engine-owned state through //! raw pointers that stay valid until the context is freed in `Drop`. use std::ffi::{CStr, CString}; @@ -21,7 +21,7 @@ use std::time::Instant; use libquickjs_ng_sys as q; use super::super::limits::Limits; -use super::{Completion, EngineError, JsEngine}; +use super::{Completion, EngineError}; pub(crate) struct QuickJsEngine { runtime: *mut q::JSRuntime, @@ -30,8 +30,8 @@ pub(crate) struct QuickJsEngine { console_lines: Vec, } -impl JsEngine for QuickJsEngine { - fn new(limits: &Limits) -> Result { +impl QuickJsEngine { + pub(crate) fn new(limits: &Limits) -> Result { #[cfg(test)] crate::resource_metrics::record_macro_engine_creation(); #[cfg(test)] @@ -63,7 +63,7 @@ impl JsEngine for QuickJsEngine { } } - fn install_console(&mut self) -> Result<(), EngineError> { + pub(crate) fn install_console(&mut self) -> Result<(), EngineError> { unsafe { let sink_value = q::JS_Ext_NewPointer( q::JS_TAG_UNDEFINED, @@ -102,7 +102,11 @@ impl JsEngine for QuickJsEngine { Ok(()) } - fn evaluate(&mut self, source: &str, filename: &str) -> Result { + pub(crate) fn evaluate( + &mut self, + source: &str, + filename: &str, + ) -> Result { let source = CString::new(source) .map_err(|_| EngineError::Internal("script contains a NUL byte".into()))?; let filename = CString::new(filename) @@ -154,7 +158,7 @@ impl JsEngine for QuickJsEngine { } } - fn set_interrupt_deadline(&mut self, deadline: Option) { + pub(crate) fn set_interrupt_deadline(&mut self, deadline: Option) { self.interrupt_deadline = deadline; unsafe { q::JS_SetInterruptHandler( @@ -165,12 +169,10 @@ impl JsEngine for QuickJsEngine { } } - fn console_output(&self) -> &[String] { + pub(crate) fn console_output(&self) -> &[String] { &self.console_lines } -} -impl QuickJsEngine { /// Reads the string property `name` of `obj`; when the property is not a /// string (missing, or a thrown non-Error value), returns `fallback`. /// @@ -285,7 +287,7 @@ impl Drop for QuickJsEngine { } /// Deadline-based interrupt handler: returns non-zero once the deadline set by -/// [`JsEngine::set_interrupt_deadline`] has passed, aborting the running +/// [`QuickJsEngine::set_interrupt_deadline`] has passed, aborting the running /// script with the QuickJS `"interrupted"` error. /// /// # Safety @@ -306,7 +308,7 @@ unsafe extern "C" fn interrupt_handler(_runtime: *mut q::JSRuntime, opaque: *mut /// # Safety /// /// The captured data value must be the pointer value created in -/// [`JsEngine::install_console`], pointing at the engine's `console_lines` +/// [`QuickJsEngine::install_console`], pointing at the engine's `console_lines` /// field; the engine is alive for the whole call. unsafe extern "C" fn console_log( context: *mut q::JSContext, diff --git a/crates/opy-rs/src/macro_js/mod.rs b/crates/opy-rs/src/macro_js/mod.rs index 1151af03..c7aabc71 100644 --- a/crates/opy-rs/src/macro_js/mod.rs +++ b/crates/opy-rs/src/macro_js/mod.rs @@ -29,16 +29,14 @@ //! that could provide one. `console.log` is captured into //! [`MacroResult::console_output`] instead of reaching the host. //! -//! # Engine choice and replaceability +//! # Engine choice //! //! Scripts run on [QuickJS-NG] embedded through the `libquickjs-ng-sys` crate //! (the QuickJS-NG FFI layer maintained behind `quickjs-rusty`; the upstream //! `quick-js-ng` crate name is not published on crates.io). QuickJS-NG is the //! engine family the OverPy reference uses (quickjs-ng wasm), which keeps //! observable language behavior aligned (completion values, `typeof`, error -//! messages such as `"interrupted"`). The binding is isolated behind the -//! crate-private [`JsEngine`] trait, so the concrete engine crate can be -//! swapped without touching the runtime logic. +//! messages such as `"interrupted"`). //! //! Building `libquickjs-ng-sys` compiles the QuickJS-NG C sources, which //! requires a C compiler toolchain (`cc`/`clang`); on macOS the Xcode Command @@ -90,7 +88,6 @@ //! hosts only (the OverPy reference restricts script execution to Node, too). //! //! [QuickJS-NG]: https://github.com/quickjs-ng/quickjs -//! [`JsEngine`]: engine::JsEngine mod engine; mod error; diff --git a/crates/opy-rs/src/macro_js/runtime.rs b/crates/opy-rs/src/macro_js/runtime.rs index 203edbbd..6dae68b7 100644 --- a/crates/opy-rs/src/macro_js/runtime.rs +++ b/crates/opy-rs/src/macro_js/runtime.rs @@ -1,7 +1,7 @@ use std::time::Instant; use super::engine::quickjs_ng::QuickJsEngine; -use super::engine::{Completion, EngineError, JsEngine}; +use super::engine::{Completion, EngineError}; use super::error::{MacroError, ScriptError}; use super::helpers::Helpers; use super::limits::Limits; From 135a6332663c9c4a4227374d2dca245ab9609785 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 12:10:53 +0800 Subject: [PATCH 64/67] refactor(opy-rs): borrow self-modification operands --- crates/opy-rs/src/compiler/operator_optimization.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/opy-rs/src/compiler/operator_optimization.rs b/crates/opy-rs/src/compiler/operator_optimization.rs index 639bd607..68e0b065 100644 --- a/crates/opy-rs/src/compiler/operator_optimization.rs +++ b/crates/opy-rs/src/compiler/operator_optimization.rs @@ -1239,14 +1239,14 @@ fn modify_operation(name: &str) -> Option { /// The operand of `target = target operand`. fn self_operand( name: &str, - args: &mut Vec, - is_target: impl Fn(&Value) -> bool, + args: &[Value], + is_target: impl FnOnce(&Value) -> bool, ) -> Option<(ModifyOp, Value)> { let op = modify_operation(name)?; if args.len() != 2 || !is_target(&args[0]) { return None; } - Some((op, args.pop().expect("two arguments"))) + Some((op, args[1].clone())) } /// `x = x y` is the modification `x = y`. @@ -1258,7 +1258,7 @@ pub(super) fn self_modification(action: &Action) -> Option { } => { let (op, value) = self_operand( name, - &mut args.clone(), + args, |target| matches!(target, Value::GlobalVariable(other) if other == variable), )?; Some(Action::ModifyGlobalVariable { @@ -1272,7 +1272,7 @@ pub(super) fn self_modification(action: &Action) -> Option { variable, value: Value::Call { name, args }, } => { - let (op, value) = self_operand(name, &mut args.clone(), |target| { + let (op, value) = self_operand(name, args, |target| { matches!(target, Value::PlayerVariable { player: other, variable: other_variable } if other_variable == variable && same(player, other)) })?; @@ -1297,7 +1297,7 @@ pub(super) fn self_modification(action: &Action) -> Option { return None; }; let (variable, index) = (&args[0], &args[1]); - let (_, value) = self_operand(operation, &mut operands.clone(), |read| match read { + let (_, value) = self_operand(operation, operands, |read| match read { Value::Call { name, args } if name == "valueInArray" && args.len() == 2 => { same(&args[0], variable) && same(&args[1], index) } From 8d5b8e8fd26d4a4246e64e0b0f739c9ea2ab3a6e Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 12:22:57 +0800 Subject: [PATCH 65/67] refactor(opy-rs): reuse reconstruction subroutine set --- crates/opy-rs/src/compiler/reconstruct.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/opy-rs/src/compiler/reconstruct.rs b/crates/opy-rs/src/compiler/reconstruct.rs index 593f577e..b2561d7f 100644 --- a/crates/opy-rs/src/compiler/reconstruct.rs +++ b/crates/opy-rs/src/compiler/reconstruct.rs @@ -1087,12 +1087,7 @@ impl<'a> Emitter<'a> { ); } Action::CallSubroutine { subroutine } => { - if !self - .program - .subroutines - .iter() - .any(|definition| definition.name == *subroutine) - { + if !self.subroutine_names.contains(subroutine) { self.issue( "unsupported-dangling", format!("unknown subroutine '{subroutine}'"), From 27732256ddda8c6737f171eb53a16a3fb3f1b980 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 12:22:58 +0800 Subject: [PATCH 66/67] refactor(opy-rs): avoid cloning compile report output --- crates/opy-rs/src/compiler/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/opy-rs/src/compiler/mod.rs b/crates/opy-rs/src/compiler/mod.rs index 6f055207..863c27f9 100644 --- a/crates/opy-rs/src/compiler/mod.rs +++ b/crates/opy-rs/src/compiler/mod.rs @@ -853,6 +853,7 @@ impl CompileReport { artifact: CompilationArtifact, diagnostics: Vec, ) -> Self { + let workshop = normalize_workshop(&artifact.final_output); Self { schema_version: COMPILE_SCHEMA_VERSION, compiler, @@ -863,8 +864,8 @@ impl CompileReport { failure_class: None, diagnostics, stdout: String::new(), - workshop_exact: artifact.final_output.clone(), - workshop: normalize_workshop(&artifact.final_output), + workshop_exact: artifact.final_output, + workshop, }, } } From dc7d63570829d501c08a6c7d21ec441e5562d6c5 Mon Sep 17 00:00:00 2001 From: Teakowa Date: Mon, 28 Sep 2026 17:34:57 +0800 Subject: [PATCH 67/67] fix(opy-rs): preserve HIR validation error precedence --- crates/opy-rs/src/hir/mod.rs | 28 ++++++++++++++++++++++++++++ crates/opy-rs/src/hir/validate.rs | 5 +++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/opy-rs/src/hir/mod.rs b/crates/opy-rs/src/hir/mod.rs index 27df3b44..0c3ddd1d 100644 --- a/crates/opy-rs/src/hir/mod.rs +++ b/crates/opy-rs/src/hir/mod.rs @@ -81,6 +81,34 @@ mod tests { assert_eq!(error.code(), "incompatible-protocol"); } + #[test] + fn expression_errors_precede_invalid_statement_structure() { + let payload = json!({ + "protocol": { "name": "wright/opy-hir", "version": "2.0.0" }, + "generator": { "name": "test", "version": "0", "frontend": "test" }, + "files": [], + "rules": [{ + "name": "test", + "event": { "name": "Ongoing - Global", "args": [] }, + "conditions": [], + "actions": [{ + "kind": "delete", + "target": { "kind": "globalVar", "name": "missing" } + }] + }] + }); + + let program: super::Program = serde_json::from_value(payload.clone()).unwrap(); + assert_eq!( + program.validate().unwrap_err().code(), + "unresolved-reference" + ); + assert_eq!( + parse_value(payload).unwrap_err().code(), + "unresolved-reference" + ); + } + #[test] fn unknown_conditional_condition_kind_preserves_unsupported_node_span() { let error = parse_value(json!({ diff --git a/crates/opy-rs/src/hir/validate.rs b/crates/opy-rs/src/hir/validate.rs index 3ef56c5d..fb73063a 100644 --- a/crates/opy-rs/src/hir/validate.rs +++ b/crates/opy-rs/src/hir/validate.rs @@ -384,9 +384,10 @@ fn validate_stmts( expressions, errors, } = validator; - errors + expressions + .errors .into_iter() - .chain(expressions.errors) + .chain(errors) .next() .map_or(Ok(()), Err) }