1use std::collections::{BTreeMap, HashMap, HashSet};
9
10use crate::hir::{self, Expr, RuleEntry, Span as HirSpan, Stmt, SwitchArm, default_var_index};
11use crate::manifest::{FunctionKind, Manifest};
12use serde::Serialize;
13use workshop_rs::catalog::{Catalog, CatalogIdentity, Kind, Locale, ParamCoercions};
14use workshop_rs::source::{Position as WorkshopPosition, SourceFile, Span as WorkshopSpan};
15use workshop_rs::wir::{self, Action, Event, PlayerEventKind, Program, Value, ValueNode};
16
17pub mod reconstruct;
18
19#[cfg(test)]
20mod integration_tests;
21
22pub const WORKSHOP_RS_VERSION: &str = "0.1.18";
24
25const TRANSLATION_HELPER_NAME: &str = "__overpyTranslationHelper__";
26
27pub const COMPILE_SCHEMA_VERSION: u32 = 1;
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
32pub struct CompilerIdentity {
33 pub name: &'static str,
34 pub version: &'static str,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
39#[serde(rename_all = "lowercase")]
40pub enum CompileStatus {
41 Success,
42 Failure,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
47#[serde(rename_all = "kebab-case")]
48pub enum CompileFailureClass {
49 Frontend,
50 Integration,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
55#[serde(rename_all = "camelCase")]
56pub struct CompileDiagnostic {
57 pub severity: crate::tooling::DiagnosticSeverity,
58 pub code: String,
59 pub message: String,
60 pub span: Option<crate::tooling::SourceLocation>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub script: Option<ScriptDiagnostic>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
67#[serde(rename_all = "camelCase")]
68pub struct CompileResult {
69 pub status: CompileStatus,
70 pub exit_code: u8,
71 pub failure_class: Option<CompileFailureClass>,
72 pub diagnostics: Vec<CompileDiagnostic>,
73 pub stdout: String,
74 pub workshop_exact: String,
75 pub workshop: String,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
80#[serde(rename_all = "camelCase")]
81pub struct CompileReport {
82 pub schema_version: u32,
83 pub compiler: CompilerIdentity,
84 pub catalog: CatalogIdentity,
85 pub compile: CompileResult,
86}
87
88impl CompilerIdentity {
89 fn current() -> Self {
90 Self {
91 name: "opy-rs",
92 version: env!("CARGO_PKG_VERSION"),
93 }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct IntegrationDiagnostic {
100 pub code: String,
101 pub message: String,
102 pub span: Option<HirSpan>,
103 pub script: Option<Box<ScriptDiagnostic>>,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
108#[serde(rename_all = "camelCase")]
109pub struct ScriptDiagnostic {
110 pub source_name: Option<String>,
111 pub line: Option<u32>,
112 pub column: Option<u32>,
113 pub stack: Option<String>,
114}
115
116impl IntegrationDiagnostic {
117 fn new(code: impl Into<String>, message: impl Into<String>, span: Option<HirSpan>) -> Self {
118 Self {
119 code: code.into(),
120 message: message.into(),
121 span,
122 script: None,
123 }
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct IntegrationError {
130 pub diagnostic: IntegrationDiagnostic,
131}
132
133impl IntegrationError {
134 fn new(code: impl Into<String>, message: impl Into<String>, span: Option<HirSpan>) -> Self {
135 Self {
136 diagnostic: IntegrationDiagnostic::new(code, message, span),
137 }
138 }
139
140 fn post_compile_hook(error: crate::macro_js::MacroError, span: Option<HirSpan>) -> Self {
141 let message = error.to_string();
142 let script = match error {
143 crate::macro_js::MacroError::Script(error) => Some(Box::new(ScriptDiagnostic {
144 source_name: error.source_name,
145 line: error.line,
146 column: error.column,
147 stack: error.stack,
148 })),
149 crate::macro_js::MacroError::InvalidResult { .. }
150 | crate::macro_js::MacroError::Internal(_) => None,
151 };
152 Self {
153 diagnostic: IntegrationDiagnostic {
154 code: "post-compile-hook".to_string(),
155 message,
156 span,
157 script,
158 },
159 }
160 }
161}
162
163impl std::fmt::Display for IntegrationError {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 write!(f, "{}: {}", self.diagnostic.code, self.diagnostic.message)
166 }
167}
168
169impl std::error::Error for IntegrationError {}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub struct LinkReport {
174 pub catalog_ids_checked: usize,
175 pub domains_checked: usize,
176}
177
178pub(crate) fn cross_check_manifest(
182 manifest: &Manifest,
183 catalog: &Catalog,
184) -> Result<LinkReport, IntegrationError> {
185 let mut catalog_ids_checked = 0;
186 let mut domains_checked = 0;
187
188 for function in &manifest.functions {
189 if let Some(catalog_id) = &function.catalog_id {
190 let kind = match function.kind {
191 FunctionKind::Action | FunctionKind::MemberAction => Kind::Action,
192 FunctionKind::Value | FunctionKind::MemberValue => Kind::Value,
193 };
194 catalog_ids_checked += 1;
195 if catalog.entry(kind, catalog_id).is_none() {
196 return Err(IntegrationError::new(
197 "catalog-link-missing",
198 format!(
199 "manifest function '{}' links to missing {:?} catalog id '{}'",
200 function.id, kind, catalog_id
201 ),
202 None,
203 ));
204 }
205 }
206
207 for parameter in &function.params {
208 let Some(domain) = ¶meter.domain else {
209 continue;
210 };
211 let contextual = function
212 .contextual_domain
213 .as_ref()
214 .is_some_and(|context| context.domain == *domain);
215 if contextual {
216 continue;
217 }
218 domains_checked += 1;
219 if catalog.enum_domain(domain).is_none() {
220 return Err(IntegrationError::new(
221 "domain-link-missing",
222 format!(
223 "manifest function '{}' parameter '{}' links to missing enum domain '{}',",
224 function.id, parameter.name, domain
225 ),
226 None,
227 ));
228 }
229 }
230
231 if let Some(contextual) = &function.contextual_domain {
232 for option in contextual.options.values() {
233 domains_checked += 1;
234 if catalog.enum_domain(&option.domain).is_none() {
235 return Err(IntegrationError::new(
236 "domain-link-missing",
237 format!(
238 "manifest function '{}' contextual option links to missing enum domain '{}'",
239 function.id, option.domain
240 ),
241 None,
242 ));
243 }
244 }
245 }
246 }
247
248 Ok(LinkReport {
249 catalog_ids_checked,
250 domains_checked,
251 })
252}
253
254pub struct Compiler {
257 catalog: Catalog,
258 manifest: &'static Manifest,
259 links: LinkReport,
260}
261
262impl Compiler {
263 pub fn new() -> Result<Self, IntegrationError> {
264 let catalog = Catalog::builtin()
265 .map_err(|error| IntegrationError::new("catalog-load", error.to_string(), None))?;
266 let manifest = Manifest::builtin()
267 .map_err(|error| IntegrationError::new("manifest-load", error.to_string(), None))?;
268 let links = cross_check_manifest(manifest, &catalog)?;
269 let identity = catalog.identity();
270 if identity.implementation_version != WORKSHOP_RS_VERSION {
271 return Err(IntegrationError::new(
272 "workshop-contract-version",
273 format!(
274 "expected workshop-rs {}, loaded {}",
275 WORKSHOP_RS_VERSION, identity.implementation_version
276 ),
277 None,
278 ));
279 }
280 Ok(Self {
281 catalog,
282 manifest,
283 links,
284 })
285 }
286
287 pub fn catalog_identity(&self) -> CatalogIdentity {
288 self.catalog.identity()
289 }
290
291 pub fn link_report(&self) -> LinkReport {
292 self.links
293 }
294
295 pub fn compile_hir(&self, hir: &hir::Program) -> Result<CompilationArtifact, IntegrationError> {
298 self.compile_hir_with_locale(hir, &Locale::new("en-US"))
299 }
300
301 pub fn compile_hir_with_locale(
303 &self,
304 hir: &hir::Program,
305 locale: &Locale,
306 ) -> Result<CompilationArtifact, IntegrationError> {
307 if !self.catalog.supports(locale) {
308 return Err(IntegrationError::new(
309 "locale-unsupported",
310 format!("workshop catalog does not declare locale '{locale}'"),
311 None,
312 ));
313 }
314 reject_unlowered_directives(hir)?;
315 let expanded_hir = expand_macros(hir)?;
316 let mut lowering = Lowering::new(self, &expanded_hir)?;
317 lowering.copy_files()?;
318 lowering.lower_declarations()?;
319 lowering.lower_rules()?;
320
321 lowering.wir.validate().map_err(|error| {
322 let span = error
323 .span()
324 .and_then(|span| lowering.hir_span_from_workshop(span));
325 IntegrationError::new(error.code(), error.message(), span)
326 })?;
327 workshop_rs::validate::validate_canonical_ids(&lowering.wir, &self.catalog).map_err(
328 |error| {
329 let span = workshop_error_span(&error)
330 .and_then(|span| lowering.hir_span_from_workshop(span));
331 IntegrationError::new("catalog-validation", error.to_string(), span)
332 },
333 )?;
334 let emitted =
335 workshop_rs::emitter::emit(&lowering.wir, &self.catalog, locale).map_err(|error| {
336 let span = workshop_error_span(&error)
337 .and_then(|span| lowering.hir_span_from_workshop(span));
338 IntegrationError::new("workshop-emission", error.to_string(), span)
339 })?;
340
341 Ok(CompilationArtifact {
342 wir: lowering.wir,
343 final_output: emitted.clone(),
344 emitted,
345 catalog_identity: self.catalog.identity(),
346 hook_console_output: Vec::new(),
347 })
348 }
349
350 pub fn compile_source(
356 &self,
357 source: &str,
358 main_path: &str,
359 root: &std::path::Path,
360 ) -> Result<CompileOutput, IntegrationError> {
361 self.compile_source_with_language(source, main_path, root, "en-US")
362 }
363
364 pub fn compile_source_with_language(
367 &self,
368 source: &str,
369 main_path: &str,
370 root: &std::path::Path,
371 language: &str,
372 ) -> Result<CompileOutput, IntegrationError> {
373 self.compile_source_with_locale(source, main_path, root, &Locale::new(language))
374 .map(CompilationArtifact::into_output)
375 }
376
377 pub fn compile_source_with_locale(
382 &self,
383 source: &str,
384 main_path: &str,
385 root: &std::path::Path,
386 locale: &Locale,
387 ) -> Result<CompilationArtifact, IntegrationError> {
388 self.compile_source_internal(source, main_path, root, locale)
389 }
390
391 pub fn compile_source_artifact(
394 &self,
395 source: &str,
396 main_path: &str,
397 root: &std::path::Path,
398 ) -> Result<CompilationArtifact, IntegrationError> {
399 self.compile_source_with_locale(source, main_path, root, &Locale::new("en-US"))
400 }
401
402 pub fn compile_source_report(
405 &self,
406 source: &str,
407 main_path: &str,
408 root: &std::path::Path,
409 ) -> CompileReport {
410 self.compile_source_report_with_language(source, main_path, root, "en-US")
411 }
412
413 pub fn compile_source_report_with_language(
416 &self,
417 source: &str,
418 main_path: &str,
419 root: &std::path::Path,
420 language: &str,
421 ) -> CompileReport {
422 self.compile_source_report_with_locale(source, main_path, root, &Locale::new(language))
423 }
424
425 pub fn compile_source_report_with_locale(
428 &self,
429 source: &str,
430 main_path: &str,
431 root: &std::path::Path,
432 locale: &Locale,
433 ) -> CompileReport {
434 let outcome = crate::compile_with_overlay_outcome(
435 source,
436 main_path,
437 root,
438 &std::collections::BTreeMap::new(),
439 );
440 let catalog = self.catalog.identity();
441 let compiler = CompilerIdentity::current();
442 let frontend_diagnostics = outcome
443 .diagnostics
444 .iter()
445 .map(compile_frontend_diagnostic)
446 .collect::<Vec<_>>();
447 let Some(hir) = outcome.hir else {
448 return CompileReport::failure(
449 compiler,
450 catalog,
451 CompileFailureClass::Frontend,
452 frontend_diagnostics,
453 );
454 };
455
456 match self.compile_hir_with_locale_and_hook(&hir, outcome.post_compile_hook, locale) {
457 Ok(artifact) => {
458 CompileReport::success(compiler, catalog, artifact, frontend_diagnostics)
459 }
460 Err(error) => {
461 let mut diagnostics = frontend_diagnostics;
462 diagnostics.push(compile_diagnostic(error, &hir.files));
463 CompileReport::failure(
464 compiler,
465 catalog,
466 CompileFailureClass::Integration,
467 diagnostics,
468 )
469 }
470 }
471 }
472
473 fn compile_source_internal(
474 &self,
475 source: &str,
476 main_path: &str,
477 root: &std::path::Path,
478 locale: &Locale,
479 ) -> Result<CompilationArtifact, IntegrationError> {
480 let outcome = crate::compile_with_overlay_outcome(
481 source,
482 main_path,
483 root,
484 &std::collections::BTreeMap::new(),
485 );
486 let hir = outcome.hir.ok_or_else(|| {
487 let error = outcome
488 .error
489 .expect("failed frontend compile has diagnostic");
490 IntegrationError::new(
491 error.code,
492 error.message,
493 error.span.map(hir_span_from_diag),
494 )
495 })?;
496 self.compile_hir_with_locale_and_hook(&hir, outcome.post_compile_hook, locale)
497 }
498
499 fn compile_hir_with_locale_and_hook(
500 &self,
501 hir: &hir::Program,
502 hook: Option<crate::PostCompileHookRecord>,
503 locale: &Locale,
504 ) -> Result<CompilationArtifact, IntegrationError> {
505 let mut artifact = self.compile_hir_with_locale(hir, locale)?;
506 if let Some(hook) = hook {
507 let runtime = crate::macro_js::MacroRuntime::new(crate::macro_js::Limits::default());
508 let result = runtime
509 .run_hook(&hook.source, &artifact.emitted, &hook.script)
510 .map_err(|error| {
511 IntegrationError::post_compile_hook(error, hook.span.map(hir_span_from_diag))
512 })?;
513 artifact.final_output = result.text;
514 artifact.hook_console_output = result.console_output;
515 }
516 Ok(artifact)
517 }
518}
519
520impl CompileReport {
521 fn success(
522 compiler: CompilerIdentity,
523 catalog: CatalogIdentity,
524 artifact: CompilationArtifact,
525 diagnostics: Vec<CompileDiagnostic>,
526 ) -> Self {
527 Self {
528 schema_version: COMPILE_SCHEMA_VERSION,
529 compiler,
530 catalog,
531 compile: CompileResult {
532 status: CompileStatus::Success,
533 exit_code: 0,
534 failure_class: None,
535 diagnostics,
536 stdout: String::new(),
537 workshop_exact: artifact.final_output.clone(),
538 workshop: normalize_workshop(&artifact.final_output),
539 },
540 }
541 }
542
543 fn failure(
544 compiler: CompilerIdentity,
545 catalog: CatalogIdentity,
546 failure_class: CompileFailureClass,
547 diagnostics: Vec<CompileDiagnostic>,
548 ) -> Self {
549 Self {
550 schema_version: COMPILE_SCHEMA_VERSION,
551 compiler,
552 catalog,
553 compile: CompileResult {
554 status: CompileStatus::Failure,
555 exit_code: 1,
556 failure_class: Some(failure_class),
557 diagnostics,
558 stdout: String::new(),
559 workshop_exact: String::new(),
560 workshop: String::new(),
561 },
562 }
563 }
564}
565
566fn compile_diagnostic(error: IntegrationError, files: &[hir::SourceFile]) -> CompileDiagnostic {
567 let diagnostic = error.diagnostic;
568 CompileDiagnostic {
569 severity: crate::tooling::DiagnosticSeverity::Error,
570 code: diagnostic.code,
571 message: diagnostic.message,
572 span: diagnostic
573 .span
574 .and_then(|span| source_location_from_hir(span, files)),
575 script: diagnostic.script.map(|script| *script),
576 }
577}
578
579fn compile_frontend_diagnostic(diagnostic: &crate::tooling::Diagnostic) -> CompileDiagnostic {
580 CompileDiagnostic {
581 severity: diagnostic.severity,
582 code: diagnostic.code.clone(),
583 message: diagnostic.message.clone(),
584 span: diagnostic.span.clone(),
585 script: None,
586 }
587}
588
589fn source_location_from_hir(
590 span: HirSpan,
591 files: &[hir::SourceFile],
592) -> Option<crate::tooling::SourceLocation> {
593 let path = files.iter().find(|file| file.id == span.file)?.path.clone();
594 Some(crate::tooling::SourceLocation {
595 file_id: span.file,
596 path,
597 start: crate::diag::Position::new(span.start.line, span.start.col),
598 end: crate::diag::Position::new(span.end.line, span.end.col),
599 })
600}
601
602fn normalize_workshop(text: &str) -> String {
603 if text.is_empty() {
604 return String::new();
605 }
606 let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
607 let mut lines = normalized
608 .split('\n')
609 .map(|line| line.trim_end_matches([' ', '\t']).to_owned())
610 .collect::<Vec<_>>();
611 while lines.last().is_some_and(|line| line.is_empty()) {
612 lines.pop();
613 }
614 if lines.is_empty() {
615 String::new()
616 } else {
617 lines.join("\n") + "\n"
618 }
619}
620
621fn reject_unlowered_directives(hir: &hir::Program) -> Result<(), IntegrationError> {
622 if let Some(replacement) = hir.preprocessing.replacements.first() {
623 let span = hir
624 .preprocessing
625 .directives
626 .iter()
627 .find(|directive| directive.name.starts_with("replace"))
628 .and_then(|directive| directive.span)
629 .or(replacement.span);
630 return Err(IntegrationError::new(
631 "backend-directive-unsupported",
632 format!(
633 "replacement directive '{}' has no canonical workshop-rs lowering",
634 replacement.value
635 ),
636 span,
637 ));
638 }
639 if let Some(replacement) = hir
640 .preprocessing
641 .directives
642 .iter()
643 .find(|directive| directive.name.starts_with("replace"))
644 {
645 return Err(IntegrationError::new(
646 "backend-directive-unsupported",
647 format!(
648 "replacement directive '{}' has no canonical workshop-rs lowering",
649 replacement.name
650 ),
651 replacement.span,
652 ));
653 }
654 Ok(())
655}
656
657type MacroBindings = HashMap<String, Expr>;
658
659struct MacroExpander {
660 macros: HashMap<String, (Vec<String>, Vec<Stmt>)>,
661 stack: Vec<String>,
662}
663
664fn expand_macros(program: &hir::Program) -> Result<hir::Program, IntegrationError> {
665 let macros = program
666 .declarations
667 .iter()
668 .filter_map(|declaration| match declaration {
669 hir::Declaration::Macro {
670 name, args, body, ..
671 } => Some((name.clone(), (args.clone(), body.clone()))),
672 _ => None,
673 })
674 .collect();
675 let mut expander = MacroExpander {
676 macros,
677 stack: Vec::new(),
678 };
679 let mut expanded = program.clone();
680 let bindings = MacroBindings::new();
681
682 for declaration in &mut expanded.declarations {
683 match declaration {
684 hir::Declaration::GlobalVariable { initializer, .. }
685 | hir::Declaration::PlayerVariable { initializer, .. } => {
686 if let Some(initializer) = initializer {
687 **initializer = expander.expand_expr(initializer, &bindings)?;
688 }
689 }
690 _ => {}
691 }
692 }
693 for entry in &mut expanded.rules {
694 match entry {
695 RuleEntry::Rule(rule) => {
696 for argument in &mut rule.event.args {
697 *argument = expander.expand_expr(argument, &bindings)?;
698 }
699 for condition in &mut rule.conditions {
700 *condition = expander.expand_expr(condition, &bindings)?;
701 }
702 rule.actions = expander.expand_stmts(&rule.actions, &bindings)?;
703 }
704 RuleEntry::SubroutineDef { body, .. } => {
705 *body = expander.expand_stmts(body, &bindings)?;
706 }
707 }
708 }
709 Ok(expanded)
710}
711
712impl MacroExpander {
713 fn expand_stmts(
714 &mut self,
715 statements: &[Stmt],
716 bindings: &MacroBindings,
717 ) -> Result<Vec<Stmt>, IntegrationError> {
718 let mut expanded = Vec::new();
719 for statement in statements {
720 if let Stmt::Expr { expr, .. } = statement {
721 if let Expr::MacroCall { name, args, span } = expr.as_ref() {
722 let args = args
723 .iter()
724 .map(|arg| self.expand_expr(arg, bindings))
725 .collect::<Result<Vec<_>, _>>()?;
726 expanded.extend(self.expand_macro_body(name, &args, *span)?);
727 continue;
728 }
729 }
730 expanded.push(self.expand_stmt(statement, bindings)?);
731 }
732 Ok(expanded)
733 }
734
735 fn expand_stmt(
736 &mut self,
737 statement: &Stmt,
738 bindings: &MacroBindings,
739 ) -> Result<Stmt, IntegrationError> {
740 Ok(match statement {
741 Stmt::Expr { expr, span } => Stmt::Expr {
742 expr: Box::new(self.expand_expr(expr, bindings)?),
743 span: *span,
744 },
745 Stmt::Assign {
746 target,
747 value,
748 span,
749 } => Stmt::Assign {
750 target: Box::new(self.expand_expr(target, bindings)?),
751 value: Box::new(self.expand_expr(value, bindings)?),
752 span: *span,
753 },
754 Stmt::If {
755 branches,
756 r#else,
757 span,
758 } => Stmt::If {
759 branches: branches
760 .iter()
761 .map(|branch| {
762 Ok(hir::types::IfBranch {
763 condition: Box::new(self.expand_expr(&branch.condition, bindings)?),
764 body: self.expand_stmts(&branch.body, bindings)?,
765 })
766 })
767 .collect::<Result<Vec<_>, IntegrationError>>()?,
768 r#else: r#else
769 .as_ref()
770 .map(|body| self.expand_stmts(body, bindings))
771 .transpose()?,
772 span: *span,
773 },
774 Stmt::For {
775 variable,
776 iterable,
777 body,
778 span,
779 } => Stmt::For {
780 variable: Box::new(self.expand_expr(variable, bindings)?),
781 iterable: Box::new(self.expand_expr(iterable, bindings)?),
782 body: self.expand_stmts(body, bindings)?,
783 span: *span,
784 },
785 Stmt::While {
786 condition,
787 body,
788 span,
789 } => Stmt::While {
790 condition: Box::new(self.expand_expr(condition, bindings)?),
791 body: self.expand_stmts(body, bindings)?,
792 span: *span,
793 },
794 Stmt::DoWhile {
795 condition,
796 body,
797 span,
798 } => Stmt::DoWhile {
799 condition: Box::new(self.expand_expr(condition, bindings)?),
800 body: self.expand_stmts(body, bindings)?,
801 span: *span,
802 },
803 Stmt::Switch { value, arms, span } => Stmt::Switch {
804 value: Box::new(self.expand_expr(value, bindings)?),
805 arms: arms
806 .iter()
807 .map(|arm| match arm {
808 SwitchArm::Case { value, body, span } => Ok(SwitchArm::Case {
809 value: Box::new(self.expand_expr(value, bindings)?),
810 body: self.expand_stmts(body, bindings)?,
811 span: *span,
812 }),
813 SwitchArm::Default { body, span } => Ok(SwitchArm::Default {
814 body: self.expand_stmts(body, bindings)?,
815 span: *span,
816 }),
817 })
818 .collect::<Result<Vec<_>, IntegrationError>>()?,
819 span: *span,
820 },
821 Stmt::Delete { target, span } => Stmt::Delete {
822 target: Box::new(self.expand_expr(target, bindings)?),
823 span: *span,
824 },
825 Stmt::Goto {
826 label,
827 offset,
828 rule_start,
829 span,
830 } => Stmt::Goto {
831 label: label.clone(),
832 offset: offset
833 .as_ref()
834 .map(|offset| self.expand_expr(offset, bindings).map(Box::new))
835 .transpose()?,
836 rule_start: *rule_start,
837 span: *span,
838 },
839 Stmt::Break { .. }
840 | Stmt::Return { .. }
841 | Stmt::CallSubroutine { .. }
842 | Stmt::Pass { .. } => statement.clone(),
843 Stmt::Continue { .. } | Stmt::Label { .. } => statement.clone(),
844 })
845 }
846
847 fn expand_expr(
848 &mut self,
849 expression: &Expr,
850 bindings: &MacroBindings,
851 ) -> Result<Expr, IntegrationError> {
852 match expression {
853 Expr::MacroParam { name, span } => bindings.get(name).cloned().ok_or_else(|| {
854 IntegrationError::new(
855 "unsupported-integration-surface",
856 format!("macro parameter '{name}' has no expansion binding"),
857 *span,
858 )
859 }),
860 Expr::MacroCall { name, args, span } => {
861 let args = args
862 .iter()
863 .map(|arg| self.expand_expr(arg, bindings))
864 .collect::<Result<Vec<_>, _>>()?;
865 let body = self.expand_macro_body(name, &args, *span)?;
866 if body.len() != 1 {
867 return Err(IntegrationError::new(
868 "macro-invalid",
869 format!("macro '{name}' must produce one expression in value position"),
870 *span,
871 ));
872 }
873 match body.into_iter().next().expect("one macro body statement") {
874 Stmt::Expr { expr, .. } => Ok(*expr),
875 _ => Err(IntegrationError::new(
876 "macro-invalid",
877 format!("macro '{name}' must produce an expression in value position"),
878 *span,
879 )),
880 }
881 }
882 Expr::Array { elements, span } => Ok(Expr::Array {
883 elements: elements
884 .iter()
885 .map(|element| self.expand_expr(element, bindings))
886 .collect::<Result<Vec<_>, _>>()?,
887 span: *span,
888 }),
889 Expr::Dict { entries, span } => Ok(Expr::Dict {
890 entries: entries
891 .iter()
892 .map(|entry| {
893 Ok(hir::DictEntry {
894 key: Box::new(self.expand_expr(&entry.key, bindings)?),
895 value: Box::new(self.expand_expr(&entry.value, bindings)?),
896 span: entry.span,
897 })
898 })
899 .collect::<Result<Vec<_>, IntegrationError>>()?,
900 span: *span,
901 }),
902 Expr::Comprehension {
903 element,
904 variable,
905 variable_span,
906 index,
907 index_span,
908 iterable,
909 condition,
910 span,
911 } => Ok(Expr::Comprehension {
912 element: Box::new(self.expand_expr(element, bindings)?),
913 variable: variable.clone(),
914 variable_span: *variable_span,
915 index: index.clone(),
916 index_span: *index_span,
917 iterable: Box::new(self.expand_expr(iterable, bindings)?),
918 condition: condition
919 .as_ref()
920 .map(|condition| self.expand_expr(condition, bindings).map(Box::new))
921 .transpose()?,
922 span: *span,
923 }),
924 Expr::Lambda {
925 params,
926 param_spans,
927 body,
928 span,
929 } => Ok(Expr::Lambda {
930 params: params.clone(),
931 param_spans: param_spans.clone(),
932 body: Box::new(self.expand_expr(body, bindings)?),
933 span: *span,
934 }),
935 Expr::Type { name, args, span } => Ok(Expr::Type {
936 name: name.clone(),
937 args: args
938 .iter()
939 .map(|arg| self.expand_expr(arg, bindings))
940 .collect::<Result<Vec<_>, _>>()?,
941 span: *span,
942 }),
943 Expr::Vector { x, y, z, span } => Ok(Expr::Vector {
944 x: Box::new(self.expand_expr(x, bindings)?),
945 y: Box::new(self.expand_expr(y, bindings)?),
946 z: Box::new(self.expand_expr(z, bindings)?),
947 span: *span,
948 }),
949 Expr::PlayerVar {
950 player,
951 name,
952 member_span,
953 span,
954 } => Ok(Expr::PlayerVar {
955 player: Box::new(self.expand_expr(player, bindings)?),
956 name: name.clone(),
957 member_span: *member_span,
958 span: *span,
959 }),
960 Expr::Member {
961 receiver,
962 member,
963 member_span,
964 span,
965 } => Ok(Expr::Member {
966 receiver: Box::new(self.expand_expr(receiver, bindings)?),
967 member: member.clone(),
968 member_span: *member_span,
969 span: *span,
970 }),
971 Expr::Call { name, args, span } => Ok(Expr::Call {
972 name: name.clone(),
973 args: args
974 .iter()
975 .map(|arg| self.expand_expr(arg, bindings))
976 .collect::<Result<Vec<_>, _>>()?,
977 span: *span,
978 }),
979 Expr::ReceiverCall {
980 receiver,
981 name,
982 args,
983 span,
984 } => Ok(Expr::ReceiverCall {
985 receiver: Box::new(self.expand_expr(receiver, bindings)?),
986 name: name.clone(),
987 args: args
988 .iter()
989 .map(|arg| self.expand_expr(arg, bindings))
990 .collect::<Result<Vec<_>, _>>()?,
991 span: *span,
992 }),
993 Expr::Binary {
994 op,
995 left,
996 right,
997 span,
998 } => Ok(Expr::Binary {
999 op: op.clone(),
1000 left: Box::new(self.expand_expr(left, bindings)?),
1001 right: Box::new(self.expand_expr(right, bindings)?),
1002 span: *span,
1003 }),
1004 Expr::Conditional {
1005 then_value,
1006 condition,
1007 else_value,
1008 span,
1009 } => Ok(Expr::Conditional {
1010 then_value: Box::new(self.expand_expr(then_value, bindings)?),
1011 condition: Box::new(self.expand_expr(condition, bindings)?),
1012 else_value: Box::new(self.expand_expr(else_value, bindings)?),
1013 span: *span,
1014 }),
1015 Expr::Unary { op, operand, span } => Ok(Expr::Unary {
1016 op: op.clone(),
1017 operand: Box::new(self.expand_expr(operand, bindings)?),
1018 span: *span,
1019 }),
1020 Expr::Index { array, index, span } => Ok(Expr::Index {
1021 array: Box::new(self.expand_expr(array, bindings)?),
1022 index: Box::new(self.expand_expr(index, bindings)?),
1023 span: *span,
1024 }),
1025 Expr::Format { text, args, span } => Ok(Expr::Format {
1026 text: text.clone(),
1027 args: args
1028 .iter()
1029 .map(|arg| self.expand_expr(arg, bindings))
1030 .collect::<Result<Vec<_>, _>>()?,
1031 span: *span,
1032 }),
1033 _ => Ok(expression.clone()),
1034 }
1035 }
1036
1037 fn expand_macro_body(
1038 &mut self,
1039 name: &str,
1040 args: &[Expr],
1041 span: Option<HirSpan>,
1042 ) -> Result<Vec<Stmt>, IntegrationError> {
1043 let Some((params, body)) = self.macros.get(name).cloned() else {
1044 return Err(IntegrationError::new(
1045 "unsupported-integration-surface",
1046 format!("macro '{name}' has no declaration"),
1047 span,
1048 ));
1049 };
1050 if params.len() != args.len() {
1051 return Err(IntegrationError::new(
1052 "macro-arity",
1053 format!(
1054 "macro '{name}' expects {} argument(s) but got {}",
1055 params.len(),
1056 args.len()
1057 ),
1058 span,
1059 ));
1060 }
1061 if self.stack.iter().any(|active| active == name) {
1062 return Err(IntegrationError::new(
1063 "macro-recursion",
1064 format!("recursive macro expansion detected for '{name}'"),
1065 span,
1066 ));
1067 }
1068 let mut bindings = MacroBindings::new();
1069 for (param, arg) in params.into_iter().zip(args.iter()) {
1070 bindings.insert(param, arg.clone());
1071 }
1072 self.stack.push(name.to_string());
1073 let result = self.expand_stmts(&body, &bindings);
1074 self.stack.pop();
1075 result
1076 }
1077}
1078
1079#[derive(Debug, Clone, PartialEq, Eq)]
1084pub struct CompileOutput {
1085 pub workshop: String,
1087 pub emitted_workshop: String,
1089 pub hook_console_output: Vec<String>,
1091}
1092
1093pub struct CompilationArtifact {
1096 pub wir: Program,
1097 pub emitted: String,
1098 pub catalog_identity: CatalogIdentity,
1099 pub final_output: String,
1100 pub hook_console_output: Vec<String>,
1101}
1102
1103impl CompilationArtifact {
1104 fn into_output(self) -> CompileOutput {
1105 CompileOutput {
1106 workshop: self.final_output,
1107 emitted_workshop: self.emitted,
1108 hook_console_output: self.hook_console_output,
1109 }
1110 }
1111}
1112
1113fn expand_settings_constants(
1114 settings: crate::hir::Settings,
1115 constants: &HashMap<String, &Expr>,
1116) -> crate::hir::Settings {
1117 crate::hir::Settings {
1118 span: settings.span,
1119 children: settings
1120 .children
1121 .into_iter()
1122 .map(|node| expand_settings_node(node, constants))
1123 .collect(),
1124 }
1125}
1126
1127fn expand_settings_node(
1128 node: crate::hir::SettingsNode,
1129 constants: &HashMap<String, &Expr>,
1130) -> crate::hir::SettingsNode {
1131 use crate::hir::SettingsNode;
1132 match node {
1133 SettingsNode::Group {
1134 name,
1135 children,
1136 span,
1137 } => {
1138 let children = children
1139 .into_iter()
1140 .map(|child| expand_settings_node(child, constants))
1141 .collect::<Vec<_>>();
1142 let children = if matches!(name.as_str(), "team1" | "team2" | "allTeams") {
1143 children
1144 .into_iter()
1145 .flat_map(|child| match child {
1146 SettingsNode::Group { name, children, .. } if name == "general" => children,
1147 child => vec![child],
1148 })
1149 .collect()
1150 } else {
1151 children
1152 };
1153 SettingsNode::Group {
1154 name,
1155 children,
1156 span,
1157 }
1158 }
1159 SettingsNode::Raw { name, value, span } => constants
1160 .get(&value)
1161 .and_then(|expr| settings_node_from_expr(name.clone(), expr, constants, span))
1162 .unwrap_or(SettingsNode::Raw { name, value, span }),
1163 node => node,
1164 }
1165}
1166
1167fn settings_node_from_expr(
1168 name: String,
1169 expr: &Expr,
1170 constants: &HashMap<String, &Expr>,
1171 span: Option<HirSpan>,
1172) -> Option<crate::hir::SettingsNode> {
1173 use crate::hir::SettingsNode;
1174 match expr {
1175 Expr::Constant { name: value, .. } => constants
1176 .get(value)
1177 .and_then(|expr| settings_node_from_expr(name, expr, constants, span)),
1178 Expr::Dict { entries, .. } => Some(SettingsNode::Group {
1179 name,
1180 children: entries
1181 .iter()
1182 .filter_map(|entry| {
1183 let Expr::String {
1184 value: child_name, ..
1185 } = entry.key.as_ref()
1186 else {
1187 return None;
1188 };
1189 settings_node_from_expr(child_name.clone(), &entry.value, constants, entry.span)
1190 })
1191 .flat_map(|child| match child {
1192 SettingsNode::Group { name, children, .. } if name == "general" => children,
1193 child => vec![child],
1194 })
1195 .collect(),
1196 span,
1197 }),
1198 Expr::Number { value, .. } => Some(SettingsNode::Number {
1199 name,
1200 value: *value,
1201 span,
1202 }),
1203 Expr::Bool { value, .. } => Some(SettingsNode::Bool {
1204 name,
1205 value: *value,
1206 span,
1207 }),
1208 Expr::String { value, .. } => Some(SettingsNode::String {
1209 name,
1210 value: value.clone(),
1211 span,
1212 }),
1213 _ => None,
1214 }
1215}
1216
1217fn convert_settings(settings: crate::hir::Settings) -> workshop_rs::settings::Settings {
1218 workshop_rs::settings::Settings {
1219 span: settings.span.map(convert_settings_span),
1220 children: settings
1221 .children
1222 .into_iter()
1223 .map(convert_settings_node)
1224 .collect(),
1225 }
1226}
1227
1228fn convert_settings_node(node: crate::hir::SettingsNode) -> workshop_rs::settings::SettingsNode {
1229 use crate::hir::SettingsNode as SourceNode;
1230 use workshop_rs::settings::{SettingsListElement, SettingsNode as TargetNode};
1231
1232 match node {
1233 SourceNode::Group {
1234 name,
1235 children,
1236 span,
1237 } if name == "workshop" => TargetNode::Workshop {
1238 children: children.into_iter().map(convert_workshop_node).collect(),
1239 span: span.map(convert_settings_span),
1240 },
1241 SourceNode::Group {
1242 name,
1243 children,
1244 span,
1245 } => TargetNode::Group {
1246 name,
1247 children: children.into_iter().map(convert_settings_node).collect(),
1248 span: span.map(convert_settings_span),
1249 },
1250 SourceNode::Number { name, value, span } => TargetNode::Number {
1251 name,
1252 value,
1253 span: span.map(convert_settings_span),
1254 },
1255 SourceNode::Bool { name, value, span } => TargetNode::Bool {
1256 name,
1257 value,
1258 span: span.map(convert_settings_span),
1259 },
1260 SourceNode::String { name, value, span } => TargetNode::String {
1261 name: name.clone(),
1262 value: if name == "mapRotation" && value == "afterGame" {
1263 "afterAGame".to_string()
1264 } else {
1265 value
1266 },
1267 span: span.map(convert_settings_span),
1268 },
1269 SourceNode::Raw { name, value, span } => TargetNode::Raw {
1270 name,
1271 value,
1272 span: span.map(convert_settings_span),
1273 },
1274 SourceNode::List {
1275 name,
1276 elements,
1277 span,
1278 } => TargetNode::List {
1279 name,
1280 elements: elements
1281 .into_iter()
1282 .map(|element| SettingsListElement {
1283 value: element.value,
1284 span: element.span.map(convert_settings_span),
1285 })
1286 .collect(),
1287 span: span.map(convert_settings_span),
1288 },
1289 }
1290}
1291
1292fn convert_workshop_node(node: crate::hir::SettingsNode) -> workshop_rs::settings::SettingsNode {
1293 use crate::hir::SettingsNode as SourceNode;
1294 use workshop_rs::settings::SettingsNode as TargetNode;
1295
1296 match node {
1297 SourceNode::Group {
1298 name,
1299 children,
1300 span,
1301 } => TargetNode::Group {
1302 name,
1303 children: children.into_iter().map(convert_workshop_node).collect(),
1304 span: span.map(convert_settings_span),
1305 },
1306 SourceNode::Number { name, value, span } => TargetNode::Raw {
1307 name,
1308 value: value.to_string(),
1309 span: span.map(convert_settings_span),
1310 },
1311 SourceNode::Bool { name, value, span } => TargetNode::Raw {
1312 name,
1313 value: value.to_string(),
1314 span: span.map(convert_settings_span),
1315 },
1316 SourceNode::String { name, value, span } | SourceNode::Raw { name, value, span } => {
1317 TargetNode::Raw {
1318 name,
1319 value,
1320 span: span.map(convert_settings_span),
1321 }
1322 }
1323 SourceNode::List {
1324 name,
1325 elements,
1326 span,
1327 } => TargetNode::Raw {
1328 name,
1329 value: format!(
1330 "[{}]",
1331 elements
1332 .into_iter()
1333 .map(|element| element.value)
1334 .collect::<Vec<_>>()
1335 .join(", ")
1336 ),
1337 span: span.map(convert_settings_span),
1338 },
1339 }
1340}
1341
1342fn convert_settings_span(span: HirSpan) -> WorkshopSpan {
1343 WorkshopSpan::new(
1344 workshop_rs::source::FileId::from_index(span.file as usize),
1345 WorkshopPosition::new(span.start.line, span.start.col),
1346 WorkshopPosition::new(span.end.line, span.end.col),
1347 )
1348}
1349
1350struct Lowering<'a> {
1351 compiler: &'a Compiler,
1352 hir: &'a hir::Program,
1353 wir: Program,
1354 files: HashMap<u32, workshop_rs::source::FileId>,
1355 wir_to_hir_files: Vec<u32>,
1356 globals: HashMap<String, wir::GlobalVarId>,
1357 players: HashMap<String, wir::PlayerVarId>,
1358 subroutines: HashMap<String, wir::SubroutineId>,
1359 constants: HashMap<String, &'a Expr>,
1360 defined_subroutines: HashSet<wir::SubroutineId>,
1361 array_bindings: Vec<ArrayBinding>,
1362 current_rule_conditions: Option<Vec<wir::ValueId>>,
1363 visible_labels: Vec<HashSet<String>>,
1364 deferred_gotos: Vec<(wir::ActionId, String, Option<HirSpan>)>,
1365 outer_goto_targets: Vec<String>,
1366}
1367
1368#[derive(Debug, Clone)]
1369struct ArrayBinding {
1370 element: String,
1371 index: Option<String>,
1372}
1373
1374#[derive(Debug, Clone, Copy)]
1375enum BreakTarget {
1376 Loop,
1377 DoWhile,
1378 Switch,
1379}
1380
1381enum DeleteAssignment {
1382 Global(wir::GlobalVarId),
1383 Player {
1384 player: wir::ValueId,
1385 variable: wir::PlayerVarId,
1386 },
1387}
1388
1389type SwitchBreak = (usize, HirSpan);
1390type LoweredSwitchBody = (Vec<wir::ActionId>, Option<SwitchBreak>);
1391type LoweredSwitchArm<'a> = (Option<&'a Expr>, Vec<wir::ActionId>, Option<SwitchBreak>);
1392
1393fn pure_continue_conditions(statement: &Stmt) -> Option<Vec<&Expr>> {
1397 match statement {
1398 Stmt::Continue { .. } => Some(Vec::new()),
1399 Stmt::If {
1400 branches,
1401 r#else: None,
1402 ..
1403 } if branches.len() == 1 && branches[0].body.len() == 1 => {
1404 let mut conditions = pure_continue_conditions(&branches[0].body[0])?;
1405 conditions.insert(0, &branches[0].condition);
1406 Some(conditions)
1407 }
1408 _ => None,
1409 }
1410}
1411
1412fn pure_goto_conditions(statement: &Stmt) -> Option<(Vec<&Expr>, &str)> {
1416 match statement {
1417 Stmt::Goto {
1418 label: Some(label),
1419 offset: None,
1420 rule_start: false,
1421 ..
1422 } => Some((Vec::new(), label.as_str())),
1423 Stmt::If {
1424 branches,
1425 r#else: None,
1426 ..
1427 } if branches.len() == 1 && branches[0].body.len() == 1 => {
1428 let (mut conditions, label) = pure_goto_conditions(&branches[0].body[0])?;
1429 conditions.insert(0, &branches[0].condition);
1430 Some((conditions, label))
1431 }
1432 _ => None,
1433 }
1434}
1435
1436fn contains_loop_continue(statement: &Stmt) -> bool {
1439 match statement {
1440 Stmt::Continue { .. } => true,
1441 Stmt::If {
1442 branches, r#else, ..
1443 } => {
1444 branches
1445 .iter()
1446 .any(|branch| branch.body.iter().any(contains_loop_continue))
1447 || r#else
1448 .as_ref()
1449 .is_some_and(|body| body.iter().any(contains_loop_continue))
1450 }
1451 Stmt::For { .. } | Stmt::While { .. } | Stmt::DoWhile { .. } => false,
1452 _ => false,
1453 }
1454}
1455
1456impl<'a> Lowering<'a> {
1457 fn new(compiler: &'a Compiler, hir: &'a hir::Program) -> Result<Self, IntegrationError> {
1458 Ok(Self {
1459 compiler,
1460 hir,
1461 wir: Program::default(),
1462 files: HashMap::new(),
1463 wir_to_hir_files: Vec::new(),
1464 globals: HashMap::new(),
1465 players: HashMap::new(),
1466 subroutines: HashMap::new(),
1467 constants: HashMap::new(),
1468 defined_subroutines: HashSet::new(),
1469 array_bindings: Vec::new(),
1470 current_rule_conditions: None,
1471 visible_labels: Vec::new(),
1472 deferred_gotos: Vec::new(),
1473 outer_goto_targets: Vec::new(),
1474 })
1475 }
1476
1477 fn copy_files(&mut self) -> Result<(), IntegrationError> {
1478 let settings_constants = self
1479 .hir
1480 .declarations
1481 .iter()
1482 .filter_map(|declaration| match declaration {
1483 hir::Declaration::Constant { name, value, .. } => {
1484 Some((name.clone(), value.as_ref()))
1485 }
1486 _ => None,
1487 })
1488 .collect();
1489 self.wir.settings = self.hir.settings.clone().map(|settings| {
1490 convert_settings(expand_settings_constants(settings, &settings_constants))
1491 });
1492 for file in &self.hir.files {
1493 if self.files.contains_key(&file.id) {
1494 return Err(IntegrationError::new(
1495 "source-file",
1496 format!("duplicate HIR source file id {}", file.id),
1497 None,
1498 ));
1499 }
1500 let id = self.wir.files.push(SourceFile::new(file.path.clone()));
1501 self.files.insert(file.id, id);
1502 self.wir_to_hir_files.push(file.id);
1503 }
1504 Ok(())
1505 }
1506
1507 fn translation_helper_index(
1508 &self,
1509 implicit_reserved: &HashSet<u32>,
1510 ) -> Result<Option<u32>, IntegrationError> {
1511 if self.hir.preprocessing.translations.is_none() {
1512 return Ok(None);
1513 }
1514 let mut reserved = implicit_reserved.clone();
1515 reserved.extend(
1516 self.hir
1517 .declarations
1518 .iter()
1519 .filter_map(|declaration| match declaration {
1520 hir::Declaration::GlobalVariable {
1521 index: Some(index), ..
1522 } => Some(*index),
1523 _ => None,
1524 }),
1525 );
1526 (0..=127)
1527 .rev()
1528 .find(|index| !reserved.contains(index))
1529 .map(Some)
1530 .ok_or_else(|| {
1531 IntegrationError::new(
1532 "index-exhausted",
1533 "no available global variable index remains for translations",
1534 self.hir
1535 .preprocessing
1536 .translations
1537 .as_ref()
1538 .and_then(|value| value.span),
1539 )
1540 })
1541 }
1542
1543 fn lower_declarations(&mut self) -> Result<(), IntegrationError> {
1544 let (implicit_globals, implicit_players) = implicit_default_variables(self.hir);
1545 for declaration in &self.hir.declarations {
1546 if let hir::Declaration::GlobalVariable {
1547 name,
1548 index: Some(index),
1549 span,
1550 ..
1551 } = declaration
1552 {
1553 for (implicit_name, implicit_span) in &implicit_globals {
1554 if default_var_index(implicit_name) == Some(*index) {
1555 return Err(IntegrationError::new(
1556 "index-collision",
1557 format!(
1558 "duplicate use of index {index} for global variables '{implicit_name}' and '{name}'"
1559 ),
1560 implicit_span.or(*span),
1561 ));
1562 }
1563 }
1564 }
1565 if let hir::Declaration::PlayerVariable {
1566 name,
1567 index: Some(index),
1568 span,
1569 ..
1570 } = declaration
1571 {
1572 for (implicit_name, implicit_span) in &implicit_players {
1573 if default_var_index(implicit_name) == Some(*index) {
1574 return Err(IntegrationError::new(
1575 "index-collision",
1576 format!(
1577 "duplicate use of index {index} for player variables '{implicit_name}' and '{name}'"
1578 ),
1579 implicit_span.or(*span),
1580 ));
1581 }
1582 }
1583 }
1584 }
1585
1586 let globals = self
1587 .hir
1588 .declarations
1589 .iter()
1590 .filter_map(|declaration| match declaration {
1591 hir::Declaration::GlobalVariable { index, span, .. } => Some((*index, *span)),
1592 _ => None,
1593 })
1594 .collect::<Vec<_>>();
1595 let players = self
1596 .hir
1597 .declarations
1598 .iter()
1599 .filter_map(|declaration| match declaration {
1600 hir::Declaration::PlayerVariable { index, span, .. } => Some((*index, *span)),
1601 _ => None,
1602 })
1603 .collect::<Vec<_>>();
1604 let subroutines = self
1605 .hir
1606 .declarations
1607 .iter()
1608 .filter_map(|declaration| match declaration {
1609 hir::Declaration::Subroutine { index, span, .. } => Some((*index, *span)),
1610 _ => None,
1611 })
1612 .collect::<Vec<_>>();
1613 let implicit_reserved = implicit_globals
1614 .keys()
1615 .map(|name| default_var_index(name).expect("implicit default variable names resolve"))
1616 .collect::<HashSet<_>>();
1617 let implicit_player_reserved = implicit_players
1618 .keys()
1619 .map(|name| default_var_index(name).expect("implicit default player names resolve"))
1620 .collect::<HashSet<_>>();
1621 let translation_helper_index = self.translation_helper_index(&implicit_reserved)?;
1622 let mut global_reserved = implicit_reserved.clone();
1623 if let Some(index) = translation_helper_index {
1624 global_reserved.insert(index);
1625 }
1626 let empty = HashSet::new();
1627 let global_indices = allocate_indices(&globals, &global_reserved, "global variable")?;
1628 let player_indices =
1629 allocate_indices(&players, &implicit_player_reserved, "player variable")?;
1630 let subroutine_indices = allocate_indices(&subroutines, &empty, "subroutine")?;
1631 let mut global_index = 0;
1632 let mut player_index = 0;
1633 let mut subroutine_index = 0;
1634
1635 let mut declared_globals: Vec<(&str, u32, Option<HirSpan>, Option<HirSpan>)> = Vec::new();
1640 let mut global_initializers = Vec::new();
1641 let mut declared_players: Vec<(&str, u32, Option<HirSpan>, Option<HirSpan>)> = Vec::new();
1642 let mut player_initializers = Vec::new();
1643 let mut declared_subroutines: Vec<(&str, u32, Option<HirSpan>, Option<HirSpan>)> =
1644 Vec::new();
1645
1646 for declaration in &self.hir.declarations {
1647 match declaration {
1648 hir::Declaration::GlobalVariable {
1649 name,
1650 index: _,
1651 span,
1652 name_span,
1653 initializer,
1654 } => {
1655 let assigned = global_indices[global_index];
1656 global_index += 1;
1657 if declared_globals
1658 .iter()
1659 .any(|(existing, ..)| *existing == name)
1660 {
1661 return Err(IntegrationError::new(
1662 "symbol-collision",
1663 format!("duplicate global variable '{name}'"),
1664 *span,
1665 ));
1666 }
1667 declared_globals.push((name, assigned, *span, *name_span));
1668 if let Some(init) = initializer {
1669 if !is_zero_initializer(init) {
1670 global_initializers.push((name, init, *span, *name_span));
1671 }
1672 }
1673 }
1674 hir::Declaration::PlayerVariable {
1675 name,
1676 index: _,
1677 span,
1678 name_span,
1679 initializer,
1680 } => {
1681 let assigned = player_indices[player_index];
1682 player_index += 1;
1683 if declared_players
1684 .iter()
1685 .any(|(existing, ..)| *existing == name)
1686 {
1687 return Err(IntegrationError::new(
1688 "symbol-collision",
1689 format!("duplicate player variable '{name}'"),
1690 *span,
1691 ));
1692 }
1693 declared_players.push((name, assigned, *span, *name_span));
1694 if let Some(init) = initializer {
1695 if !is_zero_initializer(init) {
1696 player_initializers.push((name, init, *span, *name_span));
1697 }
1698 }
1699 }
1700 hir::Declaration::Subroutine {
1701 name,
1702 span,
1703 name_span,
1704 ..
1705 } => {
1706 let assigned = subroutine_indices[subroutine_index];
1707 subroutine_index += 1;
1708 if declared_subroutines
1709 .iter()
1710 .any(|(existing, ..)| *existing == name)
1711 {
1712 return Err(IntegrationError::new(
1713 "symbol-collision",
1714 format!("duplicate subroutine '{name}'"),
1715 *span,
1716 ));
1717 }
1718 declared_subroutines.push((name, assigned, *span, *name_span));
1719 }
1720 hir::Declaration::Constant { name, value, span } => {
1721 if self.constants.insert(name.clone(), value).is_some() {
1722 return Err(IntegrationError::new(
1723 "symbol-collision",
1724 format!("duplicate constant '{name}'"),
1725 *span,
1726 ));
1727 }
1728 }
1729 hir::Declaration::Macro { .. } => {
1730 }
1733 }
1734 }
1735
1736 let mut planned_globals: Vec<(String, u32, Option<HirSpan>, Option<HirSpan>)> =
1737 declared_globals
1738 .into_iter()
1739 .map(|(name, index, span, name_span)| (name.to_string(), index, span, name_span))
1740 .collect();
1741 planned_globals.extend(implicit_globals.iter().map(|(name, span)| {
1742 (
1743 name.clone(),
1744 default_var_index(name).expect("implicit default variable names resolve"),
1745 *span,
1746 None,
1747 )
1748 }));
1749 if let Some(index) = translation_helper_index {
1750 planned_globals.push((TRANSLATION_HELPER_NAME.to_string(), index, None, None));
1751 }
1752 planned_globals.sort_by_key(|(_, index, ..)| *index);
1753 for (name, assigned, span, name_span) in planned_globals {
1754 let id = self.wir.global_variables.push(wir::WorkshopVariable {
1755 name: name.clone(),
1756 index: assigned,
1757 span: self.wir_span(span)?,
1758 name_span: self.wir_span(name_span)?,
1759 });
1760 self.globals.insert(name.clone(), id);
1761 }
1762
1763 let mut planned_players: Vec<(String, u32, Option<HirSpan>, Option<HirSpan>)> =
1764 declared_players
1765 .into_iter()
1766 .map(|(name, index, span, name_span)| (name.to_string(), index, span, name_span))
1767 .collect();
1768 planned_players.extend(implicit_players.iter().map(|(name, span)| {
1769 (
1770 name.clone(),
1771 default_var_index(name).expect("implicit default player names resolve"),
1772 *span,
1773 None,
1774 )
1775 }));
1776 planned_players.sort_by_key(|(_, index, ..)| *index);
1777 for (name, assigned, span, name_span) in planned_players {
1778 let id = self.wir.player_variables.push(wir::WorkshopVariable {
1779 name: name.clone(),
1780 index: assigned,
1781 span: self.wir_span(span)?,
1782 name_span: self.wir_span(name_span)?,
1783 });
1784 self.players.insert(name, id);
1785 }
1786
1787 declared_subroutines.sort_by_key(|(_, index, ..)| *index);
1788 for (name, assigned, span, name_span) in declared_subroutines {
1789 let id = self.wir.subroutines.push(wir::WorkshopSubroutine {
1790 name: name.to_string(),
1791 index: assigned,
1792 span: self.wir_span(span)?,
1793 name_span: self.wir_span(name_span)?,
1794 });
1795 self.subroutines.insert(name.to_string(), id);
1796 }
1797
1798 let translation_initializer = self
1799 .hir
1800 .preprocessing
1801 .translations
1802 .as_ref()
1803 .map(|translations| {
1804 let variable = *self
1805 .globals
1806 .get(TRANSLATION_HELPER_NAME)
1807 .expect("translation helper variable is created");
1808 let value = self.lower_translation_helper(translations)?;
1809 Ok(self.wir.actions.push(Action::SetGlobalVariable {
1810 variable,
1811 value,
1812 span: self.wir_span(translations.span)?,
1813 target_span: None,
1814 }))
1815 })
1816 .transpose()?;
1817
1818 if translation_initializer.is_some() || !global_initializers.is_empty() {
1819 let mut actions = Vec::with_capacity(
1820 global_initializers.len() + usize::from(translation_initializer.is_some()),
1821 );
1822 if let Some(action) = translation_initializer {
1823 actions.push(action);
1824 }
1825 for (name, init_expr, span, target_span) in global_initializers {
1826 let variable = *self.globals.get(name).expect("declared global is created");
1827 let value = self.lower_value(init_expr)?;
1828 actions.push(self.wir.actions.push(Action::SetGlobalVariable {
1829 variable,
1830 value,
1831 span: self.wir_span(span)?,
1832 target_span: self.wir_span(target_span)?,
1833 }));
1834 }
1835 self.wir.rules.push(wir::Rule {
1836 name: self.global_initializer_rule_name(),
1837 span: None,
1838 name_span: None,
1839 disabled: false,
1840 event: Event::Global,
1841 conditions: Vec::new(),
1842 actions,
1843 });
1844 }
1845
1846 if !player_initializers.is_empty() {
1847 let mut actions = Vec::with_capacity(player_initializers.len());
1848 for (name, init_expr, span, target_span) in player_initializers {
1849 let variable = *self
1850 .players
1851 .get(name)
1852 .expect("declared player variable is created");
1853 let player = self
1854 .wir
1855 .values
1856 .push(ValueNode::new(Value::EventPlayer, None));
1857 let value = self.lower_value(init_expr)?;
1858 actions.push(self.wir.actions.push(Action::SetPlayerVariable {
1859 player,
1860 variable,
1861 value,
1862 span: self.wir_span(span)?,
1863 target_span: self.wir_span(target_span)?,
1864 }));
1865 }
1866 self.wir.rules.push(wir::Rule {
1867 name: "Initialize player variables".to_string(),
1868 span: None,
1869 name_span: None,
1870 disabled: false,
1871 event: Event::EachPlayer,
1872 conditions: Vec::new(),
1873 actions,
1874 });
1875 }
1876
1877 Ok(())
1878 }
1879
1880 fn lower_rules(&mut self) -> Result<(), IntegrationError> {
1881 for entry in &self.hir.rules {
1882 match entry {
1883 RuleEntry::Rule(rule) => self.lower_rule(rule)?,
1884 RuleEntry::SubroutineDef {
1885 name,
1886 source_name,
1887 span,
1888 name_span,
1889 body,
1890 annotations,
1891 ..
1892 } => {
1893 self.lower_subroutine(name, source_name, *span, *name_span, body, annotations)?
1894 }
1895 }
1896 }
1897 Ok(())
1898 }
1899
1900 fn lower_rule(&mut self, rule: &hir::Rule) -> Result<(), IntegrationError> {
1901 if rule.disabled {
1902 return Ok(());
1903 }
1904 self.reject_rule_metadata(rule)?;
1905 let event = self.lower_event(&rule.event, &rule.annotations)?;
1906 let conditions = rule
1907 .conditions
1908 .iter()
1909 .map(|expr| self.lower_condition(expr))
1910 .collect::<Result<Vec<_>, _>>()?;
1911 let previous_conditions = self.current_rule_conditions.replace(conditions.clone());
1912 let lowered_actions = self.lower_actions(&rule.actions, None);
1913 self.current_rule_conditions = previous_conditions;
1914 let mut actions = Vec::new();
1915 actions.extend(lowered_actions?);
1916 self.wir.rules.push(wir::Rule {
1917 name: rule.name.clone(),
1918 span: self.wir_span(rule.span)?,
1919 name_span: self.wir_span(rule.name_span)?,
1920 disabled: rule.disabled,
1921 event,
1922 conditions,
1923 actions,
1924 });
1925 Ok(())
1926 }
1927
1928 fn lower_subroutine(
1929 &mut self,
1930 name: &str,
1931 source_name: &str,
1932 span: Option<HirSpan>,
1933 name_span: Option<HirSpan>,
1934 body: &[Stmt],
1935 annotations: &[hir::Annotation],
1936 ) -> Result<(), IntegrationError> {
1937 self.reject_subroutine_metadata(annotations)?;
1938 let source_name = if source_name.is_empty() {
1939 name
1940 } else {
1941 source_name
1942 };
1943 let subroutine = *self.subroutines.get(source_name).ok_or_else(|| {
1944 self.unsupported(
1945 format!("subroutine definition '{source_name}' has no declaration"),
1946 name_span.or(span),
1947 )
1948 })?;
1949 if !self.defined_subroutines.insert(subroutine) {
1950 return Err(self.unsupported(
1951 format!("subroutine '{source_name}' has multiple definitions"),
1952 name_span.or(span),
1953 ));
1954 }
1955 let mut actions = Vec::new();
1956 actions.extend(self.lower_actions(body, None)?);
1957 self.wir.rules.push(wir::Rule {
1958 name: self.subroutine_rule_name(name),
1959 span: self.wir_span(span)?,
1960 name_span: self.wir_span(name_span)?,
1961 disabled: false,
1962 event: Event::Subroutine(subroutine),
1963 conditions: Vec::new(),
1964 actions,
1965 });
1966 Ok(())
1967 }
1968
1969 fn reject_rule_metadata(&self, rule: &hir::Rule) -> Result<(), IntegrationError> {
1970 if rule.delimiter {
1971 let span = rule
1972 .annotations
1973 .iter()
1974 .find(|annotation| annotation.name == "Delimiter")
1975 .and_then(|annotation| annotation.span)
1976 .or(rule.span);
1977 return Err(self.unsupported(
1978 "rule delimiter metadata is not representable in canonical WIR",
1979 span,
1980 ));
1981 }
1982 if rule.new_page.is_some() {
1983 let span = rule
1984 .annotations
1985 .iter()
1986 .find(|annotation| annotation.name == "NewPage")
1987 .and_then(|annotation| annotation.span)
1988 .or(rule.span);
1989 return Err(self.unsupported(
1990 "rule new-page metadata is not representable in canonical WIR",
1991 span,
1992 ));
1993 }
1994 for annotation in &rule.annotations {
1995 match annotation.name.as_str() {
1996 "Event" | "Condition" | "Team" | "Slot" | "Hero" | "Disabled"
1997 | "SuppressWarnings" => {}
1998 _ => {
1999 return Err(self.unsupported(
2000 format!(
2001 "rule annotation '{}' is not representable in canonical WIR",
2002 annotation.name
2003 ),
2004 annotation.span.or(rule.span),
2005 ));
2006 }
2007 }
2008 }
2009 Ok(())
2010 }
2011
2012 fn reject_subroutine_metadata(
2013 &self,
2014 annotations: &[hir::Annotation],
2015 ) -> Result<(), IntegrationError> {
2016 for annotation in annotations {
2017 match annotation.name.as_str() {
2018 "Name" | "SuppressWarnings" => {}
2019 _ => {
2020 return Err(self.unsupported(
2021 format!(
2022 "subroutine annotation '{}' is not representable in canonical WIR",
2023 annotation.name
2024 ),
2025 annotation.span,
2026 ));
2027 }
2028 }
2029 }
2030 Ok(())
2031 }
2032
2033 fn subroutine_rule_name(&self, generated_name: &str) -> String {
2034 if self.hir.preprocessing.rule_prefix_template.is_some() {
2035 generated_name.to_string()
2036 } else {
2037 format!("Subroutine {generated_name}")
2038 }
2039 }
2040
2041 fn global_initializer_rule_name(&self) -> String {
2042 if self.hir.preprocessing.rule_prefix_template.is_some() {
2043 "[] Initialize global variables".to_string()
2044 } else {
2045 "Initialize global variables".to_string()
2046 }
2047 }
2048
2049 fn lower_event(
2050 &self,
2051 event: &hir::Event,
2052 annotations: &[hir::Annotation],
2053 ) -> Result<Event, IntegrationError> {
2054 if !event.args.is_empty() {
2055 return Err(self.unsupported(
2056 "event arguments are not representable in canonical WIR; use structural event filters",
2057 event.span,
2058 ));
2059 }
2060 let team = self.lower_event_team(annotations)?;
2061 let target = self.lower_event_target(annotations)?;
2062 let has_filters =
2063 !matches!(team, wir::EventTeam::All) || !matches!(target, wir::EventTarget::All);
2064 match event.name.as_str() {
2065 "global" => {
2066 if has_filters {
2067 return Err(
2068 self.unsupported("global events cannot have player filters", event.span)
2069 );
2070 }
2071 Ok(Event::Global)
2072 }
2073 "eachPlayer" => {
2074 if has_filters {
2075 Ok(Event::EachPlayerWithFilters { team, target })
2076 } else {
2077 Ok(Event::EachPlayer)
2078 }
2079 }
2080 name => player_event_kind(name).map_or_else(
2081 || {
2082 Err(self.unsupported(
2083 format!("event '{name}' is not supported by canonical WIR"),
2084 event.span,
2085 ))
2086 },
2087 |kind| Ok(Event::Player { kind, team, target }),
2088 ),
2089 }
2090 }
2091
2092 fn lower_event_team(
2093 &self,
2094 annotations: &[hir::Annotation],
2095 ) -> Result<wir::EventTeam, IntegrationError> {
2096 let team_annotations = annotations
2097 .iter()
2098 .filter(|annotation| annotation.name == "Team")
2099 .collect::<Vec<_>>();
2100 if team_annotations.len() > 1 {
2101 return Err(self.unsupported(
2102 "an event cannot have multiple @Team filters",
2103 team_annotations[1].span.or(team_annotations[0].span),
2104 ));
2105 }
2106 let Some(annotation) = team_annotations.first() else {
2107 return Ok(wir::EventTeam::All);
2108 };
2109 let argument = annotation
2110 .args
2111 .first()
2112 .ok_or_else(|| self.unsupported("@Team requires one filter value", annotation.span))?;
2113 if annotation.args.len() != 1 {
2114 return Err(
2115 self.unsupported("@Team requires exactly one filter value", annotation.span)
2116 );
2117 }
2118 let spelling = match argument.text.as_str() {
2119 "1" => "Team 1",
2120 "2" => "Team 2",
2121 value => value,
2122 };
2123 let (_, member) = self
2124 .compiler
2125 .catalog
2126 .resolve_enum_member("EventTeam", &Locale::new("en-US"), spelling)
2127 .ok_or_else(|| {
2128 self.unsupported(
2129 format!("unknown EventTeam filter '{spelling}'"),
2130 argument.span.or(annotation.span),
2131 )
2132 })?;
2133 match member.as_str() {
2134 "ALL" => Ok(wir::EventTeam::All),
2135 "TEAM_1" => Ok(wir::EventTeam::Team1),
2136 "TEAM_2" => Ok(wir::EventTeam::Team2),
2137 _ => Err(self.unsupported(
2138 format!("catalog EventTeam member '{member}' is not supported by canonical WIR"),
2139 argument.span.or(annotation.span),
2140 )),
2141 }
2142 }
2143
2144 fn lower_event_target(
2145 &self,
2146 annotations: &[hir::Annotation],
2147 ) -> Result<wir::EventTarget, IntegrationError> {
2148 let mut filters = Vec::new();
2149 for name in ["Slot", "Hero"] {
2150 let matches = annotations
2151 .iter()
2152 .filter(|annotation| annotation.name == name)
2153 .collect::<Vec<_>>();
2154 if matches.len() > 1 {
2155 return Err(self.unsupported(
2156 format!("an event cannot have multiple @{name} filters"),
2157 matches[1].span.or(matches[0].span),
2158 ));
2159 }
2160 filters.extend(matches);
2161 }
2162 if filters.len() > 1 {
2163 return Err(self.unsupported(
2164 "an event cannot combine @Slot and @Hero filters",
2165 filters[1].span.or(filters[0].span),
2166 ));
2167 }
2168 let Some(annotation) = filters.first() else {
2169 return Ok(wir::EventTarget::All);
2170 };
2171 let argument = annotation.args.first().ok_or_else(|| {
2172 self.unsupported(
2173 format!("@{} requires one filter value", annotation.name),
2174 annotation.span,
2175 )
2176 })?;
2177 if annotation.args.len() != 1 {
2178 return Err(self.unsupported(
2179 format!("@{} requires exactly one filter value", annotation.name),
2180 annotation.span,
2181 ));
2182 }
2183 let spelling = if annotation.name == "Slot" {
2184 match argument.text.as_str() {
2185 value if value.parse::<u8>().is_ok() => {
2186 format!("Slot {}", value.parse::<u8>().unwrap_or_default())
2187 }
2188 value => value.to_string(),
2189 }
2190 } else {
2191 argument.text.clone()
2192 };
2193 let domain = if annotation.name == "Slot" {
2194 "EventPlayer"
2195 } else {
2196 "Hero"
2197 };
2198 let locale = Locale::new("en-US");
2199 let catalog_spelling = match (domain, spelling.as_str()) {
2200 ("Hero", "mccree") => "CASSIDY",
2201 ("Hero", "hammond") => "WRECKING_BALL",
2202 ("Hero", "soldier") => "SOLDIER_76",
2203 ("Hero", "domina") => "JINYU",
2204 ("Hero", "dmon") => "D_MON",
2205 _ => spelling.as_str(),
2206 };
2207 let member = self
2208 .compiler
2209 .catalog
2210 .resolve_enum_member(domain, &locale, catalog_spelling)
2211 .map(|(_, member)| member)
2212 .or_else(|| {
2213 (domain == "Hero")
2214 .then(|| {
2215 self.compiler
2216 .catalog
2217 .enum_domain(domain)
2218 .and_then(|domain| {
2219 domain
2220 .members
2221 .iter()
2222 .find(|member| {
2223 member.member.eq_ignore_ascii_case(catalog_spelling)
2224 || member.spellings(&locale).iter().any(|candidate| {
2225 candidate.eq_ignore_ascii_case(catalog_spelling)
2226 })
2227 || member
2228 .member
2229 .chars()
2230 .filter(|c| c.is_ascii_alphanumeric())
2231 .collect::<String>()
2232 .eq_ignore_ascii_case(
2233 &catalog_spelling
2234 .chars()
2235 .filter(|c| c.is_ascii_alphanumeric())
2236 .collect::<String>(),
2237 )
2238 })
2239 .map(|member| member.member.clone())
2240 })
2241 })
2242 .flatten()
2243 })
2244 .ok_or_else(|| {
2245 self.unsupported(
2246 format!("unknown {domain} filter '{spelling}'"),
2247 argument.span.or(annotation.span),
2248 )
2249 })?;
2250 if domain == "EventPlayer" {
2251 if member == "ALL" {
2252 Ok(wir::EventTarget::All)
2253 } else if let Some(slot) = member.strip_prefix("SLOT_") {
2254 let slot = slot.parse::<u8>().map_err(|_| {
2255 self.unsupported(
2256 format!("catalog EventPlayer member '{member}' is not a slot"),
2257 argument.span.or(annotation.span),
2258 )
2259 })?;
2260 Ok(wir::EventTarget::Slot(slot))
2261 } else {
2262 Err(self.unsupported(
2263 format!(
2264 "catalog EventPlayer member '{member}' is not supported by canonical WIR"
2265 ),
2266 argument.span.or(annotation.span),
2267 ))
2268 }
2269 } else {
2270 Ok(wir::EventTarget::Hero(member))
2271 }
2272 }
2273
2274 fn lower_actions(
2275 &mut self,
2276 statements: &[Stmt],
2277 break_target: Option<BreakTarget>,
2278 ) -> Result<Vec<wir::ActionId>, IntegrationError> {
2279 self.visible_labels.push(
2280 statements
2281 .iter()
2282 .filter_map(|statement| match statement {
2283 Stmt::Label { name, .. } => Some(name.clone()),
2284 _ => None,
2285 })
2286 .collect(),
2287 );
2288 let mut actions = Vec::new();
2289 let mut labels = HashMap::new();
2290 let mut gotos = Vec::new();
2291 let mut index = 0;
2292 while index < statements.len() {
2293 let statement = &statements[index];
2294 if let Stmt::If {
2295 branches,
2296 r#else,
2297 span,
2298 } = statement
2299 {
2300 if branches.len() == 1 && r#else.is_none() {
2301 let branch = &branches[0];
2302 if let Some((conditions, label)) =
2303 branch.body.first().and_then(pure_goto_conditions)
2304 {
2305 let target = statements[index + 1..]
2306 .iter()
2307 .position(|candidate| {
2308 matches!(candidate, Stmt::Label { name, .. } if name == label)
2309 });
2310 if target.is_some() && conditions.is_empty() {
2311 let condition = self.lower_value(&branch.condition)?;
2312 let condition = self.push_call("not", vec![condition]);
2313 let body = self.lower_actions(&branch.body[1..], break_target)?;
2314 actions.push(self.wir.actions.push(Action::If {
2315 branches: vec![wir::IfBranch { condition, body }],
2316 else_body: None,
2317 span: self.wir_span(*span)?,
2318 }));
2319 index += 1;
2320 continue;
2321 }
2322 }
2323 }
2324 let external_jump =
2325 branches
2326 .iter()
2327 .enumerate()
2328 .find_map(|(branch_index, branch)| {
2329 let (conditions, label) = pure_goto_conditions(branch.body.last()?)?;
2330 let target = statements[index + 1..]
2331 .iter()
2332 .position(|candidate| {
2333 matches!(candidate, Stmt::Label { name, .. } if name == label)
2334 })
2335 .map(|offset| index + 1 + offset)
2336 .or_else(|| {
2337 self.outer_goto_targets
2338 .last()
2339 .is_some_and(|target| target == label)
2340 .then_some(statements.len())
2341 })?;
2342 Some((
2343 branch_index,
2344 conditions.into_iter().cloned().collect::<Vec<_>>(),
2345 label.to_string(),
2346 target,
2347 ))
2348 });
2349 if let Some((exit_branch, exit_conditions, label, target)) = external_jump {
2350 self.outer_goto_targets.push(label.clone());
2351 let middle_result =
2352 self.lower_actions(&statements[index + 1..target], break_target);
2353 let middle = middle_result?;
2354 self.resolve_deferred_gotos(&middle, label.as_str(), middle.len())?;
2355 let distance = self.canonical_action_width(&middle, *span)?;
2356 let mut lowered_branches = Vec::with_capacity(branches.len());
2357 for (branch_index, branch) in branches.iter().enumerate() {
2358 let body = if branch_index == exit_branch {
2359 &branch.body[..branch.body.len() - 1]
2360 } else {
2361 &branch.body[..]
2362 };
2363 lowered_branches.push(wir::IfBranch {
2364 condition: self.lower_value(&branch.condition)?,
2365 body: self.lower_actions(body, break_target)?,
2366 });
2367 }
2368 let else_body = r#else
2369 .as_ref()
2370 .map(|body| self.lower_actions(body, break_target))
2371 .transpose()?;
2372 self.outer_goto_targets.pop();
2373 actions.push(self.wir.actions.push(Action::If {
2374 branches: lowered_branches,
2375 else_body,
2376 span: self.wir_span(*span)?,
2377 }));
2378 let mut condition = self.lower_value(&branches[exit_branch].condition)?;
2379 for expression in exit_conditions {
2380 let right = self.lower_value(&expression)?;
2381 condition = self.push_call("and", vec![condition, right]);
2382 }
2383 let distance_value = self.push_number(distance as f64, &distance.to_string());
2384 actions.push(self.wir.actions.push(Action::Call {
2385 name: "skipIf".to_string(),
2386 args: vec![condition, distance_value],
2387 span: self.wir_span(*span)?,
2388 }));
2389 actions.extend(middle);
2390 index = target + 1;
2391 continue;
2392 }
2393 }
2394 if let Some((conditions, label)) = pure_goto_conditions(statement) {
2395 let target = statements[index + 1..]
2396 .iter()
2397 .position(
2398 |candidate| matches!(candidate, Stmt::Label { name, .. } if name == label),
2399 )
2400 .map(|offset| index + 1 + offset)
2401 .or_else(|| {
2402 self.outer_goto_targets
2403 .last()
2404 .is_some_and(|target| target == label)
2405 .then_some(statements.len())
2406 });
2407 if let Some(target) = target {
2408 self.outer_goto_targets.push(label.to_string());
2409 let middle_result =
2410 self.lower_actions(&statements[index + 1..target], break_target);
2411 self.outer_goto_targets.pop();
2412 let middle = middle_result?;
2413 let span = statement.span().copied();
2414 self.resolve_deferred_gotos(&middle, label, middle.len())?;
2415 let distance = self.canonical_action_width(&middle, span)?;
2416 let mut args = Vec::with_capacity(conditions.len() + 1);
2417 if let Some((first, rest)) = conditions.split_first() {
2418 let mut condition = self.lower_value(first)?;
2419 for expression in rest {
2420 let right = self.lower_value(expression)?;
2421 condition = self.push_call("and", vec![condition, right]);
2422 }
2423 args.push(condition);
2424 }
2425 args.push(self.push_number(distance as f64, &distance.to_string()));
2426 actions.push(
2427 self.wir.actions.push(Action::Call {
2428 name: if conditions.is_empty() {
2429 "skip"
2430 } else {
2431 "skipIf"
2432 }
2433 .to_string(),
2434 args,
2435 span: self.wir_span(span)?,
2436 }),
2437 );
2438 actions.extend(middle);
2439 index = target + 1;
2440 continue;
2441 }
2442 }
2443 match statement {
2444 Stmt::Label { name, .. } => {
2445 labels.insert(name.clone(), actions.len());
2446 }
2447 Stmt::Goto {
2448 label,
2449 offset,
2450 rule_start,
2451 span,
2452 } => {
2453 if *rule_start {
2454 return Err(self.unsupported(
2455 "goto RULE_START is not representable in canonical WIR",
2456 *span,
2457 ));
2458 }
2459 let placeholder = self.push_number(0.0, "0");
2460 let action = self.wir.actions.push(Action::Call {
2461 name: "skip".to_string(),
2462 args: vec![placeholder],
2463 span: self.wir_span(*span)?,
2464 });
2465 let position = actions.len();
2466 actions.push(action);
2467 gotos.push((
2468 action,
2469 position,
2470 label.clone(),
2471 offset.clone(),
2472 span.map(Into::into),
2473 ));
2474 }
2475 _ => actions.extend(self.lower_action(statement, break_target)?),
2476 }
2477 index += 1;
2478 }
2479 for (action, position, label, offset, span) in gotos {
2480 let distance = if let Some(offset) = offset {
2481 self.lower_value(&offset)?
2482 } else {
2483 let Some(label) = label else {
2484 return Err(self.unsupported("goto is missing a label or offset", span));
2485 };
2486 let Some(&target) = labels.get(&label) else {
2487 if self
2488 .visible_labels
2489 .iter()
2490 .any(|labels| labels.contains(&label))
2491 {
2492 self.deferred_gotos.push((action, label, span));
2493 continue;
2494 }
2495 return Err(self.unsupported(format!("unknown goto label '{label}'"), span));
2496 };
2497 if target < position {
2498 return Err(self
2499 .unsupported("backward goto is not representable in canonical WIR", span));
2500 }
2501 let width = self.canonical_action_width(&actions[position + 1..target], span)?;
2502 self.push_number(width as f64, &width.to_string())
2503 };
2504 let Some(Action::Call { args, .. }) = self.wir.actions.get_mut(action) else {
2505 unreachable!("goto placeholder must be a call action")
2506 };
2507 args[0] = distance;
2508 }
2509 if self.visible_labels.len() == 1 && !self.deferred_gotos.is_empty() {
2510 let (_, label, span) = self.deferred_gotos.remove(0);
2511 return Err(self.unsupported(format!("unknown goto label '{label}'"), span));
2512 }
2513 self.visible_labels.pop();
2514 Ok(actions)
2515 }
2516
2517 fn resolve_deferred_gotos(
2518 &mut self,
2519 actions: &[wir::ActionId],
2520 label: &str,
2521 target: usize,
2522 ) -> Result<(), IntegrationError> {
2523 let deferred = std::mem::take(&mut self.deferred_gotos);
2524 let mut remaining = Vec::new();
2525 for (action, deferred_label, span) in deferred {
2526 if deferred_label != label {
2527 remaining.push((action, deferred_label, span));
2528 continue;
2529 }
2530 let Some(position) = actions.iter().position(|candidate| *candidate == action) else {
2531 remaining.push((action, deferred_label, span));
2532 continue;
2533 };
2534 if target < position {
2535 return Err(
2536 self.unsupported("backward goto is not representable in canonical WIR", span)
2537 );
2538 }
2539 let width = self.canonical_action_width(&actions[position + 1..target], span)?;
2540 let distance = self.push_number(width as f64, &width.to_string());
2541 let Some(Action::Call { args, .. }) = self.wir.actions.get_mut(action) else {
2542 unreachable!("deferred goto placeholder must be a call action")
2543 };
2544 args[0] = distance;
2545 }
2546 self.deferred_gotos = remaining;
2547 Ok(())
2548 }
2549
2550 fn lower_action(
2551 &mut self,
2552 stmt: &Stmt,
2553 break_target: Option<BreakTarget>,
2554 ) -> Result<Vec<wir::ActionId>, IntegrationError> {
2555 match stmt {
2556 Stmt::Pass { .. } => Ok(Vec::new()),
2557 Stmt::Assign {
2558 target,
2559 value,
2560 span,
2561 } => self.lower_assign(target, value, *span).map(|action| vec![action]),
2562 Stmt::If {
2563 branches,
2564 r#else,
2565 span,
2566 } => {
2567 let branches = branches
2568 .iter()
2569 .map(|branch| {
2570 Ok(wir::IfBranch {
2571 condition: self.lower_value(&branch.condition)?,
2572 body: self.lower_actions(&branch.body, break_target)?,
2573 })
2574 })
2575 .collect::<Result<Vec<_>, IntegrationError>>()?;
2576 let else_body = r#else
2577 .as_ref()
2578 .map(|body| self.lower_actions(body, break_target))
2579 .transpose()?;
2580 Ok(vec![self.wir.actions.push(Action::If {
2581 branches,
2582 else_body,
2583 span: self.wir_span(*span)?,
2584 })])
2585 }
2586 Stmt::For {
2587 variable,
2588 iterable,
2589 body,
2590 span,
2591 } => {
2592 let (start, stop, step) = self.lower_range(iterable)?;
2593 let body = self.lower_loop_body(body)?;
2594 match variable.as_ref() {
2595 Expr::GlobalVar {
2596 name,
2597 span: target_span,
2598 } => {
2599 let variable_id = *self.globals.get(name).ok_or_else(|| {
2600 self.unsupported(
2601 format!("unknown global variable '{name}'"),
2602 *target_span,
2603 )
2604 })?;
2605 Ok(vec![self.wir.actions.push(Action::ForGlobalVariable {
2606 variable: variable_id,
2607 start,
2608 stop,
2609 step,
2610 body,
2611 span: self.wir_span(*span)?,
2612 target_span: self.wir_span(*target_span)?,
2613 })])
2614 }
2615 Expr::PlayerVar {
2616 player,
2617 name,
2618 span: target_span,
2619 ..
2620 } => {
2621 let variable_id = *self.players.get(name).ok_or_else(|| {
2622 self.unsupported(
2623 format!("unknown player variable '{name}'"),
2624 *target_span,
2625 )
2626 })?;
2627 let player = self.lower_value(player)?;
2628 Ok(vec![self.wir.actions.push(Action::ForPlayerVariable {
2629 player,
2630 variable: variable_id,
2631 start,
2632 stop,
2633 step,
2634 body,
2635 span: self.wir_span(*span)?,
2636 })])
2637 }
2638 _ => Err(self.unsupported(
2639 "range loops require a global- or player-variable binder in canonical WIR",
2640 variable.span().copied(),
2641 )),
2642 }
2643 }
2644 Stmt::While {
2645 condition,
2646 body,
2647 span,
2648 } => {
2649 let condition = self.lower_value(condition)?;
2650 let body = self.lower_loop_body(body)?;
2651 Ok(vec![self.wir.actions.push(Action::While {
2652 condition,
2653 body,
2654 span: self.wir_span(*span)?,
2655 })])
2656 }
2657 Stmt::DoWhile {
2658 condition,
2659 body,
2660 span,
2661 } => {
2662 let body = self.lower_do_while_body(body)?;
2663 let condition = self.lower_value(condition)?;
2664 let loop_if = self.wir.actions.push(Action::Call {
2665 name: "loopIf".to_string(),
2666 args: vec![condition],
2667 span: self.wir_span(*span)?,
2668 });
2669 let mut actions = body;
2672 actions.push(loop_if);
2673 Ok(actions)
2674 }
2675 Stmt::Switch {
2676 value,
2677 arms,
2678 span,
2679 } => self.lower_switch(value, arms, *span).map(|action| vec![action]),
2680 Stmt::Delete { target, span } => self.lower_delete(target, *span).map(|action| vec![action]),
2681 Stmt::Continue { span } => Err(self.unsupported(
2682 "continue statements are only lowered while constructing a loop body",
2683 *span,
2684 )),
2685 Stmt::Goto { span, .. } => Err(self.unsupported(
2686 "goto statements are not representable in canonical WIR",
2687 *span,
2688 )),
2689 Stmt::Label { span, .. } => Err(self.unsupported(
2690 "labels are not representable in canonical WIR",
2691 *span,
2692 )),
2693 Stmt::Break { span } => match break_target {
2694 Some(BreakTarget::Loop) => Ok(vec![self.wir.actions.push(Action::Call {
2695 name: "break".to_string(),
2696 args: Vec::new(),
2697 span: self.wir_span(*span)?,
2698 })]),
2699 Some(BreakTarget::DoWhile) => Err(self.unsupported(
2700 "break inside a do-while must be a direct statement or a single conditional break",
2701 *span,
2702 )),
2703 Some(BreakTarget::Switch) => Err(self.unsupported(
2704 "break inside a nested conditional cannot be normalized into canonical switch control flow",
2705 *span,
2706 )),
2707 None => Err(self.unsupported(
2708 "break has no enclosing canonical loop or switch",
2709 *span,
2710 )),
2711 },
2712 Stmt::Return { span } => {
2713 let true_value = self.wir.values.push(ValueNode::new(
2714 Value::Bool(true),
2715 self.wir_span(*span)?,
2716 ));
2717 Ok(vec![self.wir.actions.push(Action::Call {
2718 name: "abortIf".to_string(),
2719 args: vec![true_value],
2720 span: self.wir_span(*span)?,
2721 })])
2722 }
2723 Stmt::Expr { expr, span } => match expr.as_ref() {
2724 Expr::Call { name, args, .. } => {
2725 if name == "disableInspector" && args.is_empty() {
2726 Ok(vec![self.wir.actions.push(Action::Call {
2727 name: "disableInspector".to_string(),
2728 args: Vec::new(),
2729 span: self.wir_span(*span)?,
2730 })])
2731 } else if name == "debug" && args.len() == 1 {
2732 Ok(vec![self.lower_debug(&args[0], *span)?])
2733 } else if name == "print" && args.len() == 1 {
2734 Ok(vec![self.lower_print(&args[0], *span)?])
2735 } else {
2736 self.lower_action_call(name, args, *span).map(|action| vec![action])
2737 }
2738 }
2739 Expr::ReceiverCall {
2740 receiver,
2741 name,
2742 args,
2743 span: call_span,
2744 } => self
2745 .lower_receiver_action_call(receiver, name, args, *call_span)
2746 .map(|action| vec![action]),
2747 _ => Err(self.unsupported(
2748 "only action calls are currently representable as expression statements in canonical WIR",
2749 *span,
2750 )),
2751 },
2752 Stmt::CallSubroutine { name, span } => {
2753 let subroutine = *self.subroutines.get(name).ok_or_else(|| {
2754 self.unsupported(format!("unknown subroutine '{name}'"), *span)
2755 })?;
2756 let span = self.wir_span(*span)?;
2757 Ok(vec![self.wir.actions.push(Action::CallSubroutine {
2758 subroutine,
2759 span,
2760 callee_span: span,
2761 })])
2762 }
2763 }
2764 }
2765
2766 fn lower_loop_body(
2772 &mut self,
2773 statements: &[Stmt],
2774 ) -> Result<Vec<wir::ActionId>, IntegrationError> {
2775 self.lower_loop_sequence(statements, &[], 0)
2776 }
2777
2778 fn lower_loop_sequence(
2784 &mut self,
2785 statements: &[Stmt],
2786 after: &[wir::ActionId],
2787 structural_after: usize,
2788 ) -> Result<Vec<wir::ActionId>, IntegrationError> {
2789 let mut actions = Vec::new();
2790 let mut index = 0;
2791 while index < statements.len() {
2792 let statement = &statements[index];
2793 let tail = &statements[index + 1..];
2794 if let Some(conditions) = pure_continue_conditions(statement) {
2795 let tail = self.lower_loop_sequence(tail, after, structural_after)?;
2796 let distance = self.canonical_action_width(&tail, statement.span().copied())?
2797 + structural_after
2798 + self.canonical_action_width(after, statement.span().copied())?;
2799 if distance > 0 {
2800 let mut args = Vec::with_capacity(conditions.len() + 1);
2801 if let Some((first, rest)) = conditions.split_first() {
2802 let mut condition = self.lower_value(first)?;
2803 for expression in rest {
2804 let right = self.lower_value(expression)?;
2805 condition = self.push_call("and", vec![condition, right]);
2806 }
2807 args.push(condition);
2808 }
2809 let distance = self.wir.values.push(ValueNode::new(
2810 Value::Number {
2811 value: distance as f64,
2812 text: distance.to_string(),
2813 },
2814 self.wir_span(statement.span().copied())?,
2815 ));
2816 args.push(distance);
2817 actions.push(
2818 self.wir.actions.push(Action::Call {
2819 name: if conditions.is_empty() {
2820 "skip"
2821 } else {
2822 "skipIf"
2823 }
2824 .to_string(),
2825 args,
2826 span: self.wir_span(statement.span().copied())?,
2827 }),
2828 );
2829 }
2830 actions.extend(tail);
2831 return Ok(actions);
2832 }
2833 if contains_loop_continue(statement) {
2834 let tail = self.lower_loop_sequence(tail, after, structural_after)?;
2835 let mut continuation_after = tail.clone();
2836 continuation_after.extend_from_slice(after);
2837 let lowered = self.lower_if_with_loop_continue(
2838 statement,
2839 &continuation_after,
2840 structural_after,
2841 )?;
2842 actions.push(lowered);
2843 actions.extend(tail);
2844 return Ok(actions);
2845 }
2846 if let Some((conditions, label)) = pure_goto_conditions(statement) {
2847 if let Some(target) = statements[index + 1..]
2848 .iter()
2849 .position(
2850 |candidate| matches!(candidate, Stmt::Label { name, .. } if name == label),
2851 )
2852 .map(|offset| index + 1 + offset)
2853 {
2854 let middle = self.lower_loop_sequence(
2855 &statements[index + 1..target],
2856 after,
2857 structural_after,
2858 )?;
2859 let suffix = self.lower_loop_sequence(
2860 &statements[target + 1..],
2861 after,
2862 structural_after,
2863 )?;
2864 let distance =
2865 self.canonical_action_width(&middle, statement.span().copied())?;
2866 let mut args = Vec::with_capacity(conditions.len() + 1);
2867 if let Some((first, rest)) = conditions.split_first() {
2868 let mut condition = self.lower_value(first)?;
2869 for expression in rest {
2870 let right = self.lower_value(expression)?;
2871 condition = self.push_call("and", vec![condition, right]);
2872 }
2873 args.push(condition);
2874 }
2875 args.push(self.push_number(distance as f64, &distance.to_string()));
2876 actions.push(
2877 self.wir.actions.push(Action::Call {
2878 name: if conditions.is_empty() {
2879 "skip"
2880 } else {
2881 "skipIf"
2882 }
2883 .to_string(),
2884 args,
2885 span: self.wir_span(statement.span().copied())?,
2886 }),
2887 );
2888 actions.extend(middle);
2889 actions.extend(suffix);
2890 return Ok(actions);
2891 }
2892 }
2893 if let Some((conditions, label)) = pure_goto_conditions(statement) {
2894 let local_label = statements.iter().any(
2895 |candidate| matches!(candidate, Stmt::Label { name, .. } if name == label),
2896 );
2897 let outer_label = self
2898 .visible_labels
2899 .last()
2900 .is_some_and(|labels| labels.contains(label));
2901 if !local_label && outer_label {
2902 let span = statement.span().copied();
2903 let mut condition = None;
2904 for expression in conditions {
2905 let value = self.lower_value(expression)?;
2906 condition = Some(match condition {
2907 Some(left) => self.push_call("and", vec![left, value]),
2908 None => value,
2909 });
2910 }
2911 let break_action = self.wir.actions.push(Action::Call {
2912 name: "break".to_string(),
2913 args: Vec::new(),
2914 span: self.wir_span(span)?,
2915 });
2916 actions.push(if let Some(condition) = condition {
2917 self.wir.actions.push(Action::If {
2918 branches: vec![wir::IfBranch {
2919 condition,
2920 body: vec![break_action],
2921 }],
2922 else_body: None,
2923 span: self.wir_span(span)?,
2924 })
2925 } else {
2926 break_action
2927 });
2928 index += 1;
2929 continue;
2930 }
2931 }
2932 actions.extend(self.lower_action(statement, Some(BreakTarget::Loop))?);
2933 index += 1;
2934 }
2935 Ok(actions)
2936 }
2937
2938 fn lower_if_with_loop_continue(
2939 &mut self,
2940 statement: &Stmt,
2941 after: &[wir::ActionId],
2942 structural_after: usize,
2943 ) -> Result<wir::ActionId, IntegrationError> {
2944 let Stmt::If {
2945 branches,
2946 r#else,
2947 span,
2948 } = statement
2949 else {
2950 unreachable!("continue-containing loop statement must be an if")
2951 };
2952 let mut lowered_branches = Vec::with_capacity(branches.len());
2953 let mut suffix = after.to_vec();
2954 let mut suffix_structural = structural_after + 1;
2955 let mut lowered_else = None;
2956 if let Some(body) = r#else {
2957 let body = self.lower_loop_sequence(body, after, suffix_structural)?;
2958 suffix.splice(0..0, body.iter().copied());
2959 suffix_structural += 1;
2960 lowered_else = Some(body);
2961 }
2962 for index in (0..branches.len()).rev() {
2963 let body =
2964 self.lower_loop_sequence(&branches[index].body, &suffix, suffix_structural)?;
2965 suffix_structural += 1;
2966 suffix.splice(0..0, body.iter().copied());
2967 lowered_branches.push(body);
2968 }
2969 lowered_branches.reverse();
2970 let mut branch_actions = Vec::with_capacity(branches.len());
2971 for (branch, body) in branches.iter().zip(lowered_branches) {
2972 branch_actions.push(wir::IfBranch {
2973 condition: self.lower_value(&branch.condition)?,
2974 body,
2975 });
2976 }
2977 Ok(self.wir.actions.push(Action::If {
2978 branches: branch_actions,
2979 else_body: lowered_else,
2980 span: self.wir_span(*span)?,
2981 }))
2982 }
2983
2984 fn lower_do_while_body(
2985 &mut self,
2986 statements: &[Stmt],
2987 ) -> Result<Vec<wir::ActionId>, IntegrationError> {
2988 let mut actions = Vec::new();
2989 for (index, statement) in statements.iter().enumerate() {
2990 let direct_break = matches!(statement, Stmt::Break { .. });
2991 let conditional_break = match statement {
2992 Stmt::If {
2993 branches,
2994 r#else: None,
2995 ..
2996 } if branches.len() == 1 => {
2997 matches!(branches[0].body.as_slice(), [Stmt::Break { .. }])
2998 }
2999 _ => false,
3000 };
3001
3002 if direct_break || conditional_break {
3003 let tail = self.lower_do_while_body(&statements[index + 1..])?;
3004 let distance = self.canonical_action_width(&tail, statement.span().copied())? + 1;
3005 let (name, args, span) = if let Stmt::Break { span } = statement {
3006 ("skip", Vec::new(), *span)
3007 } else if let Stmt::If { branches, span, .. } = statement {
3008 (
3009 "skipIf",
3010 vec![self.lower_value(&branches[0].condition)?],
3011 *span,
3012 )
3013 } else {
3014 unreachable!("break shape was checked above")
3015 };
3016 let distance = self.wir.values.push(ValueNode::new(
3017 Value::Number {
3018 value: distance as f64,
3019 text: distance.to_string(),
3020 },
3021 self.wir_span(span)?,
3022 ));
3023 let mut args = args;
3024 args.push(distance);
3025 actions.push(self.wir.actions.push(Action::Call {
3026 name: name.to_string(),
3027 args,
3028 span: self.wir_span(span)?,
3029 }));
3030 actions.extend(tail);
3031 return Ok(actions);
3032 }
3033
3034 actions.extend(self.lower_action(statement, Some(BreakTarget::DoWhile))?);
3035 }
3036 Ok(actions)
3037 }
3038
3039 fn lower_range(
3040 &mut self,
3041 iterable: &Expr,
3042 ) -> Result<(wir::ValueId, wir::ValueId, wir::ValueId), IntegrationError> {
3043 let Expr::Call { name, args, .. } = iterable else {
3044 return Err(self.unsupported(
3045 "range loop iterable must be a range(...) call",
3046 iterable.span().copied(),
3047 ));
3048 };
3049 if name != "range" || !(1..=3).contains(&args.len()) {
3050 return Err(self.unsupported(
3051 "range loop requires one to three arguments",
3052 iterable.span().copied(),
3053 ));
3054 }
3055 let span = iterable.span().copied();
3056 let number = |this: &mut Self, value: f64| -> Result<wir::ValueId, IntegrationError> {
3057 Ok(this.wir.values.push(ValueNode::new(
3058 Value::Number {
3059 value,
3060 text: value.to_string(),
3061 },
3062 this.wir_span(span)?,
3063 )))
3064 };
3065 match args.as_slice() {
3066 [stop] => Ok((
3067 number(self, 0.0)?,
3068 self.lower_value(stop)?,
3069 number(self, 1.0)?,
3070 )),
3071 [start, stop] => Ok((
3072 self.lower_value(start)?,
3073 self.lower_value(stop)?,
3074 number(self, 1.0)?,
3075 )),
3076 [start, stop, step] => Ok((
3077 self.lower_value(start)?,
3078 self.lower_value(stop)?,
3079 self.lower_value(step)?,
3080 )),
3081 _ => unreachable!("range arity checked above"),
3082 }
3083 }
3084
3085 fn lower_switch(
3086 &mut self,
3087 value: &Expr,
3088 arms: &[SwitchArm],
3089 span: Option<HirSpan>,
3090 ) -> Result<wir::ActionId, IntegrationError> {
3091 let selector = self.lower_value(value)?;
3092 let mut case_values = Vec::new();
3093 let mut lowered_arms = Vec::with_capacity(arms.len());
3094 let mut has_default = false;
3095 let mut legacy_case_offsets = Vec::new();
3096 let mut legacy_offset = 0;
3097 let mut legacy_default_offset = None;
3098
3099 for arm in arms {
3100 let (value, (body, break_at)) = match arm {
3101 SwitchArm::Case { value, body, .. } => {
3102 case_values.push(self.lower_value(value)?);
3103 (Some(value), self.lower_switch_body(body)?)
3104 }
3105 SwitchArm::Default { body, span } => {
3106 if has_default {
3107 return Err(
3108 self.unsupported("a switch may contain at most one default arm", *span)
3109 );
3110 }
3111 has_default = true;
3112 legacy_default_offset = Some(legacy_offset);
3113 (None, self.lower_switch_body(body)?)
3114 }
3115 };
3116 if value.is_some() {
3117 legacy_case_offsets.push(legacy_offset);
3118 }
3119 legacy_offset +=
3120 self.canonical_action_width(&body, span)? + usize::from(break_at.is_some());
3121 lowered_arms.push((value.map(Box::as_ref), body, break_at));
3122 }
3123
3124 let break_arms: Vec<_> = lowered_arms
3125 .iter()
3126 .enumerate()
3127 .filter_map(|(index, (_, _, break_at))| break_at.map(|break_at| (index, break_at)))
3128 .collect();
3129 let first_break = break_arms.first().copied();
3130 let has_later_reachable_actions =
3131 first_break.is_some_and(|(break_index, (break_at, _))| {
3132 lowered_arms[break_index].1.len() > break_at
3133 || lowered_arms
3134 .iter()
3135 .skip(break_index + 1)
3136 .any(|(_, body, _)| !body.is_empty())
3137 });
3138 let use_shared_exit = break_arms.len() > 1 && has_later_reachable_actions;
3139
3140 let case_values = self.lower_array(case_values, span)?;
3141 let value_span = self.wir_span(span)?;
3142 if !use_shared_exit {
3143 let default_offset = legacy_default_offset.unwrap_or(legacy_offset);
3144 let offset_values = std::iter::once(default_offset)
3145 .chain(legacy_case_offsets)
3146 .map(|value| {
3147 self.wir.values.push(ValueNode::new(
3148 Value::Number {
3149 value: value as f64,
3150 text: value.to_string(),
3151 },
3152 value_span,
3153 ))
3154 })
3155 .collect();
3156 let offsets = self.lower_array(offset_values, span)?;
3157 let skip = self.lower_switch_selector(selector, case_values, offsets, span)?;
3158 let true_value = self
3159 .wir
3160 .values
3161 .push(ValueNode::new(Value::Bool(true), self.wir_span(span)?));
3162 let mut branch_body = vec![skip];
3163 let else_body = if let Some((break_index, (break_at, _))) = first_break {
3164 for (index, (_, body, _)) in lowered_arms.iter().enumerate() {
3165 if index < break_index {
3166 branch_body.extend(body.iter().copied());
3167 } else if index == break_index {
3168 branch_body.extend(body[..break_at].iter().copied());
3169 }
3170 }
3171 let mut tail = Vec::new();
3172 tail.extend(lowered_arms[break_index].1[break_at..].iter().copied());
3173 for (_, body, _) in lowered_arms.iter().skip(break_index + 1) {
3174 tail.extend(body.iter().copied());
3175 }
3176 Some(tail)
3177 } else {
3178 for (_, body, _) in &lowered_arms {
3179 branch_body.extend(body.iter().copied());
3180 }
3181 None
3182 };
3183 return Ok(self.wir.actions.push(Action::If {
3184 branches: vec![wir::IfBranch {
3185 condition: true_value,
3186 body: branch_body,
3187 }],
3188 else_body,
3189 span: self.wir_span(span)?,
3190 }));
3191 }
3192
3193 let offsets = self
3194 .wir
3195 .values
3196 .push(ValueNode::new(Value::Array(Vec::new()), value_span));
3197 let skip = self.lower_switch_selector(selector, case_values, offsets, span)?;
3198 let mut arm_offsets = vec![None; lowered_arms.len()];
3199 let (switch, switch_end) =
3200 self.lower_switch_level(&lowered_arms, 0, Some(skip), 0, &mut arm_offsets, span)?;
3201
3202 let default_offset = lowered_arms
3203 .iter()
3204 .enumerate()
3205 .find_map(|(index, (value, _, _))| value.is_none().then(|| arm_offsets[index].unwrap()))
3206 .unwrap_or(switch_end);
3207 let offset_values = std::iter::once(default_offset)
3208 .chain(
3209 lowered_arms
3210 .iter()
3211 .enumerate()
3212 .filter(|(_, (value, _, _))| value.is_some())
3213 .map(|(index, _)| arm_offsets[index].unwrap()),
3214 )
3215 .map(|value| {
3216 self.wir.values.push(ValueNode::new(
3217 Value::Number {
3218 value: value as f64,
3219 text: value.to_string(),
3220 },
3221 value_span,
3222 ))
3223 })
3224 .collect();
3225 let offset_values = self.lower_array(offset_values, span)?;
3226 let offset_value = self
3227 .wir
3228 .values
3229 .get(offset_values)
3230 .expect("switch offset array must exist")
3231 .value
3232 .clone();
3233 let Some(node) = self.wir.values.get_mut(offsets) else {
3234 unreachable!("switch offset placeholder must exist")
3235 };
3236 node.value = offset_value;
3237
3238 Ok(switch)
3239 }
3240
3241 fn lower_switch_selector(
3242 &mut self,
3243 selector: wir::ValueId,
3244 case_values: wir::ValueId,
3245 offsets: wir::ValueId,
3246 span: Option<HirSpan>,
3247 ) -> Result<wir::ActionId, IntegrationError> {
3248 let one = self.wir.values.push(ValueNode::new(
3249 Value::Number {
3250 value: 1.0,
3251 text: "1".to_string(),
3252 },
3253 self.wir_span(span)?,
3254 ));
3255 let index = self.wir.values.push(ValueNode::new(
3256 Value::Call {
3257 name: "indexOfArrayValue".to_string(),
3258 args: vec![case_values, selector],
3259 },
3260 self.wir_span(span)?,
3261 ));
3262 let case_offset = self.wir.values.push(ValueNode::new(
3263 Value::Call {
3264 name: "add".to_string(),
3265 args: vec![one, index],
3266 },
3267 self.wir_span(span)?,
3268 ));
3269 let skip_condition = self.wir.values.push(ValueNode::new(
3270 Value::Call {
3271 name: "valueInArray".to_string(),
3272 args: vec![offsets, case_offset],
3273 },
3274 self.wir_span(span)?,
3275 ));
3276 Ok(self.wir.actions.push(Action::Call {
3277 name: "skip".to_string(),
3278 args: vec![skip_condition],
3279 span: self.wir_span(span)?,
3280 }))
3281 }
3282
3283 fn lower_switch_level(
3284 &mut self,
3285 arms: &[LoweredSwitchArm<'_>],
3286 start: usize,
3287 selector_skip: Option<wir::ActionId>,
3288 level_offset: usize,
3289 arm_offsets: &mut [Option<usize>],
3290 span: Option<HirSpan>,
3291 ) -> Result<(wir::ActionId, usize), IntegrationError> {
3292 let break_index = (start..arms.len())
3293 .find(|index| arms[*index].2.is_some())
3294 .expect("switch level must contain a break");
3295 let mut branch_body = Vec::new();
3296 if let Some(selector_skip) = selector_skip {
3297 branch_body.push(selector_skip);
3298 }
3299 let mut branch_offset = 0;
3300 for index in start..=break_index {
3301 arm_offsets[index] = Some(if selector_skip.is_some() {
3302 level_offset + branch_offset
3303 } else if index == start {
3304 level_offset
3305 } else {
3306 level_offset + 1 + branch_offset
3307 });
3308 let (_, body, break_at) = &arms[index];
3309 let body = if index == break_index {
3310 &body[..break_at.as_ref().unwrap().0]
3311 } else {
3312 body.as_slice()
3313 };
3314 branch_offset += self.canonical_action_width(body, span)?;
3315 branch_body.extend(body.iter().copied());
3316 }
3317 let branch_width = self.canonical_action_width(&branch_body, span)?;
3318 let (_, break_body, Some((break_at, _))) = &arms[break_index] else {
3319 unreachable!("break index must point to a switch break")
3320 };
3321 let mut else_body = break_body[*break_at..].to_vec();
3322 let tail_width = self.canonical_action_width(&else_body, span)?;
3323 let else_content_start = if selector_skip.is_some() {
3324 level_offset + branch_width + tail_width
3325 } else {
3326 level_offset + branch_width + tail_width + 2
3327 };
3328 let has_next_break = (break_index + 1..arms.len()).any(|index| arms[index].2.is_some());
3329 let end_offset = if has_next_break {
3330 let (child, child_end) = self.lower_switch_level(
3331 arms,
3332 break_index + 1,
3333 None,
3334 else_content_start,
3335 arm_offsets,
3336 span,
3337 )?;
3338 else_body.push(child);
3339 child_end
3340 } else {
3341 let mut offset = else_content_start;
3342 for index in break_index + 1..arms.len() {
3343 arm_offsets[index] = Some(offset);
3344 let (_, body, _) = &arms[index];
3345 offset += self.canonical_action_width(body, span)?;
3346 else_body.extend(body.iter().copied());
3347 }
3348 offset
3349 };
3350 let true_value = self
3351 .wir
3352 .values
3353 .push(ValueNode::new(Value::Bool(true), self.wir_span(span)?));
3354 let switch = self.wir.actions.push(Action::If {
3355 branches: vec![wir::IfBranch {
3356 condition: true_value,
3357 body: branch_body,
3358 }],
3359 else_body: Some(else_body),
3360 span: self.wir_span(span)?,
3361 });
3362 Ok((switch, end_offset))
3363 }
3364
3365 fn lower_switch_body(
3366 &mut self,
3367 statements: &[Stmt],
3368 ) -> Result<LoweredSwitchBody, IntegrationError> {
3369 let mut actions = Vec::new();
3370 let mut break_at = None;
3371 for statement in statements {
3372 if let Stmt::Break { span } = statement {
3373 if break_at.is_some() {
3374 return Err(self.unsupported(
3375 "multiple switch breaks in one arm require canonical switch targets",
3376 *span,
3377 ));
3378 }
3379 break_at = Some((
3380 actions.len(),
3381 span.ok_or_else(|| {
3382 self.unsupported("switch break is missing source provenance", None)
3383 })?,
3384 ));
3385 continue;
3386 }
3387 actions.extend(self.lower_action(statement, Some(BreakTarget::Switch))?);
3388 }
3389 Ok((actions, break_at))
3390 }
3391
3392 fn canonical_action_width(
3394 &self,
3395 actions: &[wir::ActionId],
3396 fallback_span: Option<HirSpan>,
3397 ) -> Result<usize, IntegrationError> {
3398 workshop_rs::emitter::action_width(
3399 &self.wir,
3400 &self.compiler.catalog,
3401 &Locale::new("en-US"),
3402 actions,
3403 )
3404 .map(|layout| layout.width)
3405 .map_err(|error| {
3406 let workshop_span = match &error {
3407 workshop_rs::emitter::ActionLayoutError::InvalidWIR(error) => error.span(),
3408 workshop_rs::emitter::ActionLayoutError::Emission(error) => {
3409 workshop_error_span(error)
3410 }
3411 };
3412 let span = workshop_span
3413 .and_then(|span| self.hir_span_from_workshop(span))
3414 .or(fallback_span);
3415 IntegrationError::new("workshop-action-layout", error.to_string(), span)
3416 })
3417 }
3418
3419 fn lower_array(
3420 &mut self,
3421 elements: Vec<wir::ValueId>,
3422 span: Option<HirSpan>,
3423 ) -> Result<wir::ValueId, IntegrationError> {
3424 let name = if elements.is_empty() {
3425 "emptyArray"
3426 } else {
3427 "array"
3428 };
3429 let elements = self.normalize_contextual_arguments(name, elements);
3430 Ok(self.wir.values.push(ValueNode::new(
3431 Value::Call {
3432 name: name.to_string(),
3433 args: elements,
3434 },
3435 self.wir_span(span)?,
3436 )))
3437 }
3438
3439 fn lower_translation_helper(
3440 &mut self,
3441 translations: &hir::TranslationState,
3442 ) -> Result<wir::ValueId, IntegrationError> {
3443 let translated_white = translations
3444 .languages
3445 .iter()
3446 .map(|language| {
3447 let locale = translation_locale(language).ok_or_else(|| {
3448 IntegrationError::new(
3449 "translations-invalid",
3450 format!("unsupported translation language '{language}'"),
3451 translations.span,
3452 )
3453 })?;
3454 self.compiler
3455 .catalog
3456 .enum_spelling("Color", &Locale::new(locale), "WHITE")
3457 .map(str::to_string)
3458 .ok_or_else(|| {
3459 IntegrationError::new(
3460 "translations-invalid",
3461 format!("catalog has no Color.WHITE spelling for locale '{locale}'"),
3462 translations.span,
3463 )
3464 })
3465 })
3466 .collect::<Result<Vec<_>, _>>()?
3467 .join("0");
3468 let text = self.push_value(Value::String(format!("\u{ec48}0{translated_white}")));
3469 let custom_string = self.push_call("customString", vec![text]);
3470 let null = self.push_value(Value::Null);
3471 let separator = self.push_call("firstOf", vec![null]);
3472 Ok(self.push_call("stringSplit", vec![custom_string, separator]))
3473 }
3474
3475 fn lower_debug(
3476 &mut self,
3477 expr: &Expr,
3478 span: Option<HirSpan>,
3479 ) -> Result<wir::ActionId, IntegrationError> {
3480 macro_rules! call {
3481 ($name:literal $(, $arg:expr)* $(,)?) => {{
3482 let args = vec![$($arg),*];
3483 self.push_call($name, args)
3484 }};
3485 }
3486
3487 let value = self.lower_text_value(expr)?;
3488 let array_text = if self.debug_value_is_array(value) {
3489 self.lower_debug_array_text(value)
3490 } else {
3491 value
3492 };
3493 let debug_label = canonical_debug_text(&debug_expr_text(expr));
3494 let debug_prefix = format!("{debug_label}\u{2028}= {{0}}");
3495 let inline_padding = 128 - debug_prefix.chars().count() - "{1}".chars().count();
3496 let padding_text = self.push_value(Value::String(" ".repeat(170 - inline_padding)));
3497 let padding = self.push_call("customString", vec![padding_text]);
3498 let debug_label = self.push_value(Value::String(format!(
3499 "{debug_prefix}{}{{1}}",
3500 " ".repeat(inline_padding)
3501 )));
3502 let text = self.push_call("customString", vec![debug_label, array_text, padding]);
3503 let all_teams = self.push_value(Value::Enum {
3504 value_type: "Team".to_string(),
3505 value: "ALL".to_string(),
3506 });
3507 let all_players = call!("allPlayers", all_teams);
3508 let null_value = self.push_value(Value::Null);
3509 let null_value_2 = self.push_value(Value::Null);
3510 let null_value_3 = self.push_value(Value::Null);
3511 let null_value_4 = self.push_value(Value::Null);
3512 let hud_position = self.push_value(Value::Enum {
3513 value_type: "HudPosition".to_string(),
3514 value: "LEFT".to_string(),
3515 });
3516 let sort_order = self.push_value(Value::Number {
3517 value: -9999.0,
3518 text: "-9999".to_string(),
3519 });
3520 let color = self.push_value(Value::Enum {
3521 value_type: "Color".to_string(),
3522 value: "WHITE".to_string(),
3523 });
3524 let reevaluation = self.push_value(Value::Enum {
3525 value_type: "HudReeval".to_string(),
3526 value: "VISIBILITY_SORT_ORDER_STRING_AND_COLOR".to_string(),
3527 });
3528 let visibility = self.push_value(Value::Enum {
3529 value_type: "SpecVisibility".to_string(),
3530 value: "DEFAULT".to_string(),
3531 });
3532 let args = self.normalize_contextual_arguments(
3533 "createHudText",
3534 vec![
3535 all_players,
3536 null_value,
3537 text,
3538 null_value_2,
3539 hud_position,
3540 sort_order,
3541 null_value_3,
3542 color,
3543 null_value_4,
3544 reevaluation,
3545 visibility,
3546 ],
3547 );
3548 Ok(self.wir.actions.push(Action::Call {
3549 name: "createHudText".to_string(),
3550 args,
3551 span: self.wir_span(span)?,
3552 }))
3553 }
3554
3555 fn lower_print(
3556 &mut self,
3557 expr: &Expr,
3558 span: Option<HirSpan>,
3559 ) -> Result<wir::ActionId, IntegrationError> {
3560 macro_rules! call {
3561 ($name:literal $(, $arg:expr)* $(,)?) => {{
3562 let args = vec![$($arg),*];
3563 self.push_call($name, args)
3564 }};
3565 }
3566
3567 let message = self.lower_text_value(expr)?;
3568 let padding_text = self.push_value(Value::String(" ".repeat(45)));
3569 let padding = self.push_call("customString", vec![padding_text]);
3570 let body_text = self.push_value(Value::String(format!("{}{{0}}", " ".repeat(125))));
3571 let body = self.push_call("customString", vec![body_text, padding]);
3572 let all_teams = self.push_value(Value::Enum {
3573 value_type: "Team".to_string(),
3574 value: "ALL".to_string(),
3575 });
3576 let all_players = call!("allPlayers", all_teams);
3577 let null_value = self.push_value(Value::Null);
3578 let null_value_2 = self.push_value(Value::Null);
3579 let null_value_3 = self.push_value(Value::Null);
3580 let hud_position = self.push_value(Value::Enum {
3581 value_type: "HudPosition".to_string(),
3582 value: "LEFT".to_string(),
3583 });
3584 let sort_order = self.push_value(Value::Number {
3585 value: -9999.0,
3586 text: "-9999".to_string(),
3587 });
3588 let color = self.push_value(Value::Enum {
3589 value_type: "Color".to_string(),
3590 value: "ORANGE".to_string(),
3591 });
3592 let reevaluation = self.push_value(Value::Enum {
3593 value_type: "HudReeval".to_string(),
3594 value: "VISIBILITY_AND_STRING".to_string(),
3595 });
3596 let visibility = self.push_value(Value::Enum {
3597 value_type: "SpecVisibility".to_string(),
3598 value: "DEFAULT".to_string(),
3599 });
3600 let args = self.normalize_contextual_arguments(
3601 "createHudText",
3602 vec![
3603 all_players,
3604 message,
3605 body,
3606 null_value,
3607 hud_position,
3608 sort_order,
3609 color,
3610 null_value_2,
3611 null_value_3,
3612 reevaluation,
3613 visibility,
3614 ],
3615 );
3616 Ok(self.wir.actions.push(Action::Call {
3617 name: "createHudText".to_string(),
3618 args,
3619 span: self.wir_span(span)?,
3620 }))
3621 }
3622
3623 fn lower_debug_array_text(&mut self, value: wir::ValueId) -> wir::ValueId {
3624 macro_rules! call {
3625 ($name:literal $(, $arg:expr)* $(,)?) => {{
3626 let args = vec![$($arg),*];
3627 self.push_call($name, args)
3628 }};
3629 }
3630
3631 let current_count = call!("countOf", call!("currentArrayElement"));
3632 let is_single = call!(
3633 "==",
3634 call!("countOf", call!("currentArrayElement")),
3635 self.push_number(1.0, "1")
3636 );
3637 let is_empty = call!("==", call!("currentArrayElement"), call!("emptyArray"));
3638 let not_null = call!(
3639 "!=",
3640 call!("currentArrayElement"),
3641 self.push_value(Value::Null)
3642 );
3643 let has_empty_array = call!("and", is_empty, not_null);
3644 let brackets = call!("or", is_single, has_empty_array);
3645 let first_element = call!(
3646 "customString",
3647 self.push_value(Value::String("[{0}]".to_string())),
3648 call!("currentArrayElement"),
3649 );
3650 let many_elements = call!(
3651 "customString",
3652 self.push_value(Value::String("[{0}, …+{1}]".to_string())),
3653 call!("currentArrayElement"),
3654 call!(
3655 "subtract",
3656 call!("countOf", call!("currentArrayElement")),
3657 self.push_number(1.0, "1"),
3658 ),
3659 );
3660 let element_text = call!(
3661 "ifThenElse",
3662 brackets,
3663 first_element,
3664 call!(
3665 "ifThenElse",
3666 current_count,
3667 many_elements,
3668 call!("currentArrayElement"),
3669 ),
3670 );
3671 let mapped_elements = call!("mappedArray", value, element_text,);
3672 let mapped_input = call!("array", mapped_elements);
3673 let current_array = call!("currentArrayElement");
3674 let actual_array = call!(
3675 "or",
3676 call!("countOf", current_array),
3677 call!(
3678 "and",
3679 call!("==", call!("currentArrayElement"), call!("emptyArray")),
3680 call!(
3681 "!=",
3682 call!("currentArrayElement"),
3683 self.push_value(Value::Null)
3684 ),
3685 ),
3686 );
3687 let empty_length = call!(
3688 "ifThenElse",
3689 call!(
3690 "and",
3691 call!("not", call!("countOf", call!("currentArrayElement"))),
3692 call!("!=", call!("currentArrayElement"), call!("emptyArray"),),
3693 ),
3694 self.push_number(3.0, "3"),
3695 call!(
3696 "multiply",
3697 call!("countOf", call!("currentArrayElement")),
3698 self.push_number(3.0, "3"),
3699 ),
3700 );
3701 let x = call!(
3702 "appendToArray",
3703 call!("appendToArray", actual_array, empty_length),
3704 current_array,
3705 );
3706 let x_input = call!("mappedArray", mapped_input, x);
3707 let x_length = |this: &mut Self| {
3708 let current = this.push_call("currentArrayElement", Vec::new());
3709 let index = this.push_number(1.0, "1");
3710 this.push_call("valueInArray", vec![current, index])
3711 };
3712 let x_value = |this: &mut Self, index: f64| {
3713 let current = this.push_call("currentArrayElement", Vec::new());
3714 let index_value = this.push_number(index, &index.to_string());
3715 this.push_call("valueInArray", vec![current, index_value])
3716 };
3717 let first = call!("firstOf", call!("currentArrayElement"));
3718 let array_tail = call!(
3719 "customString",
3720 self.push_value(Value::String("{0}, {1}, {2}".to_string())),
3721 x_value(self, 4.0),
3722 x_value(self, 5.0),
3723 call!(
3724 "customString",
3725 self.push_value(Value::String("{0}, {1}, …\u{0001}".to_string())),
3726 x_value(self, 6.0),
3727 x_value(self, 7.0),
3728 ),
3729 );
3730 let array_head = call!(
3731 "customString",
3732 self.push_value(Value::String("{0}, {1}, {2}".to_string())),
3733 x_value(self, 2.0),
3734 x_value(self, 3.0),
3735 array_tail,
3736 );
3737 let placeholder = call!(
3738 "customString",
3739 self.push_value(Value::String("0, 0, 0, 0, 0, 0, …\u{0001}".to_string())),
3740 );
3741 let length_for_slice = x_length(self);
3742 let end_length_for_slice = x_length(self);
3743 let slice = call!(
3744 "stringSlice",
3745 placeholder,
3746 call!("add", self.push_number(-2.0, "-2"), length_for_slice),
3747 call!(
3748 "subtract",
3749 self.push_number(22.0, "22"),
3750 end_length_for_slice,
3751 ),
3752 );
3753 let replaced = call!("stringReplace", array_head, slice, call!("emptyArray"),);
3754 let length_for_compare = x_length(self);
3755 let length_for_divide = x_length(self);
3756 let plus = call!(
3757 "ifThenElse",
3758 call!(">", length_for_compare, self.push_number(18.0, "18")),
3759 call!(
3760 "customString",
3761 self.push_value(Value::String("+{0}".to_string())),
3762 call!(
3763 "subtract",
3764 call!("divide", length_for_divide, self.push_number(3.0, "3")),
3765 self.push_number(6.0, "6"),
3766 ),
3767 ),
3768 call!("emptyArray"),
3769 );
3770 let formatted_array = call!(
3771 "customString",
3772 self.push_value(Value::String("[{0}{1}]".to_string())),
3773 replaced,
3774 plus,
3775 );
3776 let current_for_split = call!("currentArrayElement");
3777 let rendered = call!(
3778 "ifThenElse",
3779 first,
3780 formatted_array,
3781 call!(
3782 "stringSplit",
3783 call!(
3784 "valueInArray",
3785 current_for_split,
3786 self.push_number(2.0, "2")
3787 ),
3788 call!("emptyArray"),
3789 ),
3790 );
3791 call!("mappedArray", x_input, rendered)
3792 }
3793
3794 fn lower_text_value(&mut self, expr: &Expr) -> Result<wir::ValueId, IntegrationError> {
3795 let value = self.lower_value(expr)?;
3796 let Value::Call { name, args } = &self
3797 .wir
3798 .values
3799 .get(value)
3800 .expect("lowered text value must exist")
3801 .value
3802 else {
3803 return Ok(value);
3804 };
3805 if name == "customString" && args.len() == 1 {
3806 Ok(args[0])
3807 } else {
3808 Ok(value)
3809 }
3810 }
3811
3812 fn debug_value_is_array(&self, value: wir::ValueId) -> bool {
3813 match &self
3814 .wir
3815 .values
3816 .get(value)
3817 .expect("lowered value must exist")
3818 .value
3819 {
3820 Value::GlobalVariable(_) | Value::Array(_) => true,
3821 Value::Call { name, .. } if matches!(name.as_str(), "array" | "emptyArray") => true,
3822 Value::Call { name, .. } => self
3823 .compiler
3824 .catalog
3825 .entry(Kind::Value, name)
3826 .and_then(|entry| entry.return_type())
3827 .is_some_and(|return_type| {
3828 return_type.split('|').any(|part| part.trim() == "Array")
3829 }),
3830 _ => false,
3831 }
3832 }
3833
3834 fn value_is_known_player(&self, value: wir::ValueId) -> bool {
3835 match &self
3836 .wir
3837 .values
3838 .get(value)
3839 .expect("lowered value must exist")
3840 .value
3841 {
3842 Value::EventPlayer => true,
3843 Value::Call { name, .. } => self
3844 .compiler
3845 .catalog
3846 .entry(Kind::Value, name)
3847 .and_then(|entry| entry.return_type())
3848 .is_some_and(|return_type| {
3849 return_type.split('|').any(|part| part.trim() == "Player")
3850 }),
3851 _ => false,
3852 }
3853 }
3854
3855 fn push_value(&mut self, value: Value) -> wir::ValueId {
3856 self.wir.values.push(ValueNode::new(value, None))
3857 }
3858
3859 fn push_call(&mut self, name: &str, args: Vec<wir::ValueId>) -> wir::ValueId {
3860 let args = self.normalize_contextual_arguments(name, args);
3861 self.push_value(Value::Call {
3862 name: name.to_string(),
3863 args,
3864 })
3865 }
3866
3867 fn contextual_coercions(&self, call_id: &str, arg_index: usize) -> Option<ParamCoercions> {
3868 [Kind::Action, Kind::Value].into_iter().find_map(|kind| {
3869 self.compiler
3870 .catalog
3871 .entry(kind, call_id)
3872 .and_then(|entry| entry.param_coercions(arg_index))
3873 .copied()
3874 })
3875 }
3876
3877 fn is_zero_number(&self, value_id: wir::ValueId) -> bool {
3878 matches!(
3879 self.wir.values.get(value_id),
3880 Some(ValueNode {
3881 value: Value::Number { value, .. },
3882 ..
3883 }) if *value == 0.0
3884 )
3885 }
3886
3887 fn is_empty_string(&self, value_id: wir::ValueId) -> bool {
3888 matches!(
3889 self.wir.values.get(value_id),
3890 Some(ValueNode {
3891 value: Value::String(value),
3892 ..
3893 }) if value.is_empty()
3894 )
3895 }
3896
3897 fn normalize_value_with_coercions(
3898 &mut self,
3899 coercions: ParamCoercions,
3900 value_id: wir::ValueId,
3901 ) -> wir::ValueId {
3902 let Some(node) = self.wir.values.get(value_id) else {
3903 return value_id;
3904 };
3905 let span = node.span;
3906 let replacement = match &node.value {
3907 Value::Bool(false) if coercions.false_as_number => Some(Value::Number {
3908 value: 0.0,
3909 text: "0".to_string(),
3910 }),
3911 Value::Bool(true) if coercions.true_as_number => Some(Value::Number {
3912 value: 1.0,
3913 text: "1".to_string(),
3914 }),
3915 Value::Number { value, .. } if coercions.zero_as_null && *value == 0.0 => {
3916 Some(Value::Null)
3917 }
3918 Value::Vector { x, y, z }
3919 if coercions.null_vector_as_null
3920 && self.is_zero_number(*x)
3921 && self.is_zero_number(*y)
3922 && self.is_zero_number(*z) =>
3923 {
3924 Some(Value::Null)
3925 }
3926 Value::Call { name, args }
3927 if coercions.null_vector_as_null
3928 && name == "vector"
3929 && args.len() == 3
3930 && args.iter().all(|value_id| self.is_zero_number(*value_id)) =>
3931 {
3932 Some(Value::Null)
3933 }
3934 Value::Call { name, args }
3935 if coercions.empty_array_as_string && name == "emptyArray" && args.is_empty() =>
3936 {
3937 Some(Value::String(String::new()))
3938 }
3939 Value::Call { name, args }
3940 if coercions.empty_array_as_string
3941 && name == "customString"
3942 && args.len() == 1
3943 && self.is_empty_string(args[0]) =>
3944 {
3945 Some(Value::String(String::new()))
3946 }
3947 Value::Array(elements) if coercions.empty_array_as_string && elements.is_empty() => {
3948 Some(Value::String(String::new()))
3949 }
3950 _ => None,
3951 };
3952 let Some(value) = replacement else {
3953 return value_id;
3954 };
3955 self.wir.values.push(ValueNode::new(value, span))
3956 }
3957
3958 fn normalize_contextual_argument(
3959 &mut self,
3960 call_id: &str,
3961 arg_index: usize,
3962 value_id: wir::ValueId,
3963 ) -> wir::ValueId {
3964 let Some(coercions) = self.contextual_coercions(call_id, arg_index) else {
3965 return value_id;
3966 };
3967 self.normalize_value_with_coercions(coercions, value_id)
3968 }
3969
3970 fn normalize_modify_value(
3971 &mut self,
3972 op: wir::ModifyOp,
3973 value_id: wir::ValueId,
3974 ) -> wir::ValueId {
3975 let coercions = match op {
3976 wir::ModifyOp::Add
3977 | wir::ModifyOp::Subtract
3978 | wir::ModifyOp::Modulo
3979 | wir::ModifyOp::Min
3980 | wir::ModifyOp::Max
3981 | wir::ModifyOp::RemoveFromArrayByIndex => ParamCoercions {
3982 false_as_number: true,
3983 true_as_number: true,
3984 ..Default::default()
3985 },
3986 wir::ModifyOp::AppendToArray | wir::ModifyOp::RemoveFromArray => ParamCoercions {
3987 zero_as_null: true,
3988 ..Default::default()
3989 },
3990 wir::ModifyOp::Multiply | wir::ModifyOp::Divide | wir::ModifyOp::RaiseToPower => {
3991 return value_id;
3992 }
3993 };
3994 self.normalize_value_with_coercions(coercions, value_id)
3995 }
3996
3997 fn modify_op_from_value(&self, value_id: wir::ValueId) -> Option<wir::ModifyOp> {
3998 let Value::Call { name, args } = &self.wir.values.get(value_id)?.value else {
3999 return None;
4000 };
4001 if !args.is_empty() {
4002 return None;
4003 }
4004 match name.as_str() {
4005 "add" => Some(wir::ModifyOp::Add),
4006 "subtract" => Some(wir::ModifyOp::Subtract),
4007 "multiply" => Some(wir::ModifyOp::Multiply),
4008 "divide" => Some(wir::ModifyOp::Divide),
4009 "modulo" => Some(wir::ModifyOp::Modulo),
4010 "min" => Some(wir::ModifyOp::Min),
4011 "max" => Some(wir::ModifyOp::Max),
4012 "raiseToPower" => Some(wir::ModifyOp::RaiseToPower),
4013 "appendToArray" => Some(wir::ModifyOp::AppendToArray),
4014 "removeFromArray" | "removeFromArrayByValue" => Some(wir::ModifyOp::RemoveFromArray),
4015 "removeFromArrayByIndex" => Some(wir::ModifyOp::RemoveFromArrayByIndex),
4016 _ => None,
4017 }
4018 }
4019
4020 fn normalize_contextual_arguments(
4021 &mut self,
4022 call_id: &str,
4023 mut args: Vec<wir::ValueId>,
4024 ) -> Vec<wir::ValueId> {
4025 let mut index = 0;
4026 while index < args.len() {
4027 args[index] = self.normalize_contextual_argument(call_id, index, args[index]);
4028 if matches!(
4029 call_id,
4030 "modifyGlobalVariableAtIndex" | "modifyPlayerVariableAtIndex"
4031 ) && index == 3
4032 {
4033 if let Some(op) = args
4034 .get(2)
4035 .and_then(|value_id| self.modify_op_from_value(*value_id))
4036 {
4037 args[index] = self.normalize_modify_value(op, args[index]);
4038 }
4039 }
4040 index += 1;
4041 }
4042 args
4043 }
4044
4045 fn lower_custom_string(
4046 &mut self,
4047 value: String,
4048 span: Option<HirSpan>,
4049 ) -> Result<wir::ValueId, IntegrationError> {
4050 let span = self.wir_span(span)?;
4051 let text = self
4052 .wir
4053 .values
4054 .push(ValueNode::new(Value::String(value), span));
4055 Ok(self.wir.values.push(ValueNode::new(
4056 Value::Call {
4057 name: "customString".to_string(),
4058 args: vec![text],
4059 },
4060 span,
4061 )))
4062 }
4063
4064 fn push_number(&mut self, value: f64, text: &str) -> wir::ValueId {
4065 self.push_value(Value::Number {
4066 value,
4067 text: text.to_string(),
4068 })
4069 }
4070
4071 fn fold_numeric_binary(
4072 &self,
4073 op: &str,
4074 left: wir::ValueId,
4075 right: wir::ValueId,
4076 ) -> Option<f64> {
4077 let number = |id| match self.wir.values.get(id)?.value {
4078 Value::Number { value, .. } => Some(value),
4079 _ => None,
4080 };
4081 let left = number(left)?;
4082 let right = number(right)?;
4083 let value = match op {
4084 "+" => left + right,
4085 "-" => left - right,
4086 "*" => left * right,
4087 "/" if right != 0.0 => left / right,
4088 "%" if right != 0.0 => left % right,
4089 "**" => left.powf(right),
4090 _ => return None,
4091 };
4092 value.is_finite().then_some(value)
4093 }
4094
4095 fn lower_condition(&mut self, expr: &Expr) -> Result<wir::ValueId, IntegrationError> {
4096 let value = self.lower_value(expr)?;
4097 let is_comparison = |expr: &Expr| matches!(expr, Expr::Binary { op, .. } if matches!(op.as_str(), "==" | "!=" | "<" | "<=" | ">" | ">="));
4098 if is_comparison(expr)
4099 || matches!(expr, Expr::Unary { op, operand, .. } if op == "not" && is_comparison(operand))
4100 {
4101 return Ok(value);
4102 }
4103 let true_value = self.wir.values.push(ValueNode::new(
4104 Value::Bool(true),
4105 self.wir_span(expr.span().copied())?,
4106 ));
4107 Ok(self.wir.values.push(ValueNode::new(
4108 Value::Call {
4109 name: "==".to_string(),
4110 args: vec![value, true_value],
4111 },
4112 self.wir_span(expr.span().copied())?,
4113 )))
4114 }
4115
4116 fn lower_delete(
4117 &mut self,
4118 target: &Expr,
4119 span: Option<HirSpan>,
4120 ) -> Result<wir::ActionId, IntegrationError> {
4121 let Expr::Index { array, index, .. } = target else {
4122 return Err(self.unsupported(
4123 "delete statements require an indexed global or player variable",
4124 span,
4125 ));
4126 };
4127 let (root, assignment) = match array.as_ref() {
4128 Expr::GlobalVar {
4129 name,
4130 span: target_span,
4131 } => {
4132 let variable = *self.globals.get(name).ok_or_else(|| {
4133 self.unsupported(format!("unknown global variable '{name}'"), *target_span)
4134 })?;
4135 let value = self.wir.values.push(ValueNode::new(
4136 Value::GlobalVariable(variable),
4137 self.wir_span(*target_span)?,
4138 ));
4139 (value, DeleteAssignment::Global(variable))
4140 }
4141 Expr::PlayerVar {
4142 player,
4143 name,
4144 span: target_span,
4145 ..
4146 } => {
4147 let variable = *self.players.get(name).ok_or_else(|| {
4148 self.unsupported(format!("unknown player variable '{name}'"), *target_span)
4149 })?;
4150 let player = self.lower_value(player)?;
4151 let value = self.wir.values.push(ValueNode::new(
4152 Value::PlayerVariable { player, variable },
4153 self.wir_span(*target_span)?,
4154 ));
4155 (value, DeleteAssignment::Player { player, variable })
4156 }
4157 _ => {
4158 return Err(self.unsupported(
4159 "delete statements are only representable for global or player variables",
4160 target.span().copied(),
4161 ));
4162 }
4163 };
4164 let index = self.lower_value(index)?;
4165 let zero = self.push_number(0.0, "0");
4166 let one = self.push_number(1.0, "1");
4167 let end = self.push_call("add", vec![index, one]);
4168 let maximum = self.push_number(999_999_999_999.0, "999999999999");
4169 let prefix = self.push_call("slice", vec![root, zero, index]);
4170 let suffix = self.push_call("slice", vec![root, end, maximum]);
4171 let value = self.push_call("appendToArray", vec![prefix, suffix]);
4172 let span = self.wir_span(span)?;
4173 Ok(match assignment {
4174 DeleteAssignment::Global(variable) => {
4175 self.wir.actions.push(Action::SetGlobalVariable {
4176 variable,
4177 value,
4178 span,
4179 target_span: span,
4180 })
4181 }
4182 DeleteAssignment::Player { player, variable } => {
4183 self.wir.actions.push(Action::SetPlayerVariable {
4184 player,
4185 variable,
4186 value,
4187 span,
4188 target_span: span,
4189 })
4190 }
4191 })
4192 }
4193
4194 fn lower_assign(
4195 &mut self,
4196 target: &Expr,
4197 value: &Expr,
4198 span: Option<HirSpan>,
4199 ) -> Result<wir::ActionId, IntegrationError> {
4200 let mut indices = Vec::new();
4201 if let Some(root) = indexed_target_parts(target, &mut indices) {
4202 if indices.len() > 3 {
4203 return Err(self.unsupported("Cannot assign to 4d array", target.span().copied()));
4204 }
4205 if indices.len() > 1 {
4206 indices.reverse();
4207 return self.lower_nested_indexed_assign(root, &indices, target, value, span);
4208 }
4209 }
4210 match target {
4211 Expr::GlobalVar {
4212 name,
4213 span: target_span,
4214 } => {
4215 let variable = *self.globals.get(name).ok_or_else(|| {
4216 self.unsupported(format!("unknown global variable '{name}'"), *target_span)
4217 })?;
4218 if let Expr::Binary {
4219 op, left, right, ..
4220 } = value
4221 {
4222 if let Expr::GlobalVar {
4223 name: left_name, ..
4224 } = left.as_ref()
4225 {
4226 if left_name == name {
4227 if let Some(modify_op) = modify_op_from_str(op) {
4228 let right = self.lower_value(right)?;
4229 let val = self.normalize_modify_value(modify_op, right);
4230 return Ok(self.wir.actions.push(Action::ModifyGlobalVariable {
4231 variable,
4232 op: modify_op,
4233 value: val,
4234 span: self.wir_span(span)?,
4235 target_span: self.wir_span(*target_span)?,
4236 }));
4237 }
4238 }
4239 }
4240 }
4241 let val = self.lower_value(value)?;
4242 Ok(self.wir.actions.push(Action::SetGlobalVariable {
4243 variable,
4244 value: val,
4245 span: self.wir_span(span)?,
4246 target_span: self.wir_span(*target_span)?,
4247 }))
4248 }
4249 Expr::PlayerVar {
4250 player,
4251 name,
4252 span: target_span,
4253 ..
4254 } => {
4255 let variable = *self.players.get(name).ok_or_else(|| {
4256 self.unsupported(format!("unknown player variable '{name}'"), *target_span)
4257 })?;
4258 let player_val = self.lower_value(player)?;
4259 if let Expr::Binary {
4260 op, left, right, ..
4261 } = value
4262 {
4263 if let Expr::PlayerVar {
4264 player: left_player,
4265 name: left_name,
4266 ..
4267 } = left.as_ref()
4268 {
4269 if left_name == name && left_player.as_ref() == player.as_ref() {
4270 if let Some(modify_op) = modify_op_from_str(op) {
4271 let right = self.lower_value(right)?;
4272 let val = self.normalize_modify_value(modify_op, right);
4273 return Ok(self.wir.actions.push(Action::ModifyPlayerVariable {
4274 player: player_val,
4275 variable,
4276 op: modify_op,
4277 value: val,
4278 span: self.wir_span(span)?,
4279 target_span: self.wir_span(*target_span)?,
4280 }));
4281 }
4282 }
4283 }
4284 }
4285 let val = self.lower_value(value)?;
4286 Ok(self.wir.actions.push(Action::SetPlayerVariable {
4287 player: player_val,
4288 variable,
4289 value: val,
4290 span: self.wir_span(span)?,
4291 target_span: self.wir_span(*target_span)?,
4292 }))
4293 }
4294 Expr::Index {
4295 array,
4296 index,
4297 span: target_span,
4298 } => match array.as_ref() {
4299 Expr::GlobalVar {
4300 name,
4301 span: arr_span,
4302 } => {
4303 let variable = *self.globals.get(name).ok_or_else(|| {
4304 self.unsupported(format!("unknown global variable '{name}'"), *arr_span)
4305 })?;
4306 let var_node = self.wir.values.push(ValueNode::new(
4307 Value::GlobalVariable(variable),
4308 self.wir_span(*arr_span)?,
4309 ));
4310 let index_val = self.lower_value(index)?;
4311 if let Expr::Binary {
4312 op, left, right, ..
4313 } = value
4314 {
4315 if let Expr::Index {
4316 array: left_arr,
4317 index: left_idx,
4318 ..
4319 } = left.as_ref()
4320 {
4321 if left_arr.as_ref() == array.as_ref()
4322 && left_idx.as_ref() == index.as_ref()
4323 {
4324 if let Some(modify_op) = modify_op_from_str(op) {
4325 let op_id = modify_catalog_name_from_str(op)
4326 .expect("known modify operator has a catalog name");
4327 let op_node = self.wir.values.push(ValueNode::new(
4328 Value::Call {
4329 name: op_id.to_string(),
4330 args: Vec::new(),
4331 },
4332 None,
4333 ));
4334 let right = self.lower_value(right)?;
4335 let right_val = self.normalize_modify_value(modify_op, right);
4336 let args = self.normalize_contextual_arguments(
4337 "modifyGlobalVariableAtIndex",
4338 vec![var_node, index_val, op_node, right_val],
4339 );
4340 return Ok(self.wir.actions.push(Action::Call {
4341 name: "modifyGlobalVariableAtIndex".to_string(),
4342 args,
4343 span: self.wir_span(span)?,
4344 }));
4345 }
4346 }
4347 }
4348 }
4349 let val = self.lower_value(value)?;
4350 let args = self.normalize_contextual_arguments(
4351 "setGlobalVariableAtIndex",
4352 vec![var_node, index_val, val],
4353 );
4354 Ok(self.wir.actions.push(Action::Call {
4355 name: "setGlobalVariableAtIndex".to_string(),
4356 args,
4357 span: self.wir_span(span)?,
4358 }))
4359 }
4360 Expr::PlayerVar {
4361 player,
4362 name,
4363 span: arr_span,
4364 ..
4365 } => {
4366 let player_val = self.lower_value(player)?;
4367 let variable = *self.players.get(name).ok_or_else(|| {
4368 self.unsupported(format!("unknown player variable '{name}'"), *arr_span)
4369 })?;
4370 let var_node = self.wir.values.push(ValueNode::new(
4371 Value::PlayerVariable {
4372 player: player_val,
4373 variable,
4374 },
4375 self.wir_span(*arr_span)?,
4376 ));
4377 let index_val = self.lower_value(index)?;
4378 if let Expr::Binary {
4379 op, left, right, ..
4380 } = value
4381 {
4382 if let Expr::Index {
4383 array: left_arr,
4384 index: left_idx,
4385 ..
4386 } = left.as_ref()
4387 {
4388 if left_arr.as_ref() == array.as_ref()
4389 && left_idx.as_ref() == index.as_ref()
4390 {
4391 if let Some(modify_op) = modify_op_from_str(op) {
4392 let op_id = modify_catalog_name_from_str(op)
4393 .expect("known modify operator has a catalog name");
4394 let op_node = self.wir.values.push(ValueNode::new(
4395 Value::Call {
4396 name: op_id.to_string(),
4397 args: Vec::new(),
4398 },
4399 None,
4400 ));
4401 let right = self.lower_value(right)?;
4402 let right_val = self.normalize_modify_value(modify_op, right);
4403 let args = self.normalize_contextual_arguments(
4404 "modifyPlayerVariableAtIndex",
4405 vec![var_node, index_val, op_node, right_val],
4406 );
4407 return Ok(self.wir.actions.push(Action::Call {
4412 name: "modifyPlayerVariableAtIndex".to_string(),
4413 args,
4414 span: self.wir_span(span)?,
4415 }));
4416 }
4417 }
4418 }
4419 }
4420 let val = self.lower_value(value)?;
4421 let args = self.normalize_contextual_arguments(
4422 "setPlayerVariableAtIndex",
4423 vec![var_node, index_val, val],
4424 );
4425 Ok(self.wir.actions.push(Action::Call {
4429 name: "setPlayerVariableAtIndex".to_string(),
4430 args,
4431 span: self.wir_span(span)?,
4432 }))
4433 }
4434 _ => Err(self.unsupported(
4435 "indexing assignment is only representable for global or player variables",
4436 *target_span,
4437 )),
4438 },
4439 _ => Err(self.unsupported(
4440 "only global-variable, player-variable, or index assignment is currently representable in canonical WIR",
4441 span,
4442 )),
4443 }
4444 }
4445
4446 fn lower_nested_indexed_assign(
4447 &mut self,
4448 root: &Expr,
4449 indices: &[&Expr],
4450 target: &Expr,
4451 value: &Expr,
4452 span: Option<HirSpan>,
4453 ) -> Result<wir::ActionId, IntegrationError> {
4454 let (action_name, root_value) = match root {
4455 Expr::GlobalVar {
4456 name,
4457 span: target_span,
4458 } => {
4459 let variable = *self.globals.get(name).ok_or_else(|| {
4460 self.unsupported(format!("unknown global variable '{name}'"), *target_span)
4461 })?;
4462 let root_value = self.wir.values.push(ValueNode::new(
4463 Value::GlobalVariable(variable),
4464 self.wir_span(*target_span)?,
4465 ));
4466 ("setGlobalVariableAtIndex", root_value)
4467 }
4468 Expr::PlayerVar {
4469 player,
4470 name,
4471 span: target_span,
4472 ..
4473 } => {
4474 let player_value = self.lower_value(player)?;
4475 let variable = *self.players.get(name).ok_or_else(|| {
4476 self.unsupported(format!("unknown player variable '{name}'"), *target_span)
4477 })?;
4478 let root_value = self.wir.values.push(ValueNode::new(
4479 Value::PlayerVariable {
4480 player: player_value,
4481 variable,
4482 },
4483 self.wir_span(*target_span)?,
4484 ));
4485 ("setPlayerVariableAtIndex", root_value)
4486 }
4487 _ => {
4488 return Err(self.unsupported(
4489 "indexing assignment is only representable for global or player variables",
4490 target.span().copied(),
4491 ));
4492 }
4493 };
4494
4495 let outer_index = self.lower_value(indices[0])?;
4496 let outer_array = self.lower_indexed_read(root_value, indices[0], outer_index)?;
4497 let replacement =
4498 self.rebuild_indexed_value(outer_array, &indices[1..], target, value, span)?;
4499 let args = self.normalize_contextual_arguments(
4500 action_name,
4501 vec![root_value, outer_index, replacement],
4502 );
4503 Ok(self.wir.actions.push(Action::Call {
4504 name: action_name.to_string(),
4505 args,
4506 span: self.wir_span(span)?,
4507 }))
4508 }
4509
4510 fn rebuild_indexed_value(
4511 &mut self,
4512 array: wir::ValueId,
4513 indices: &[&Expr],
4514 target: &Expr,
4515 value: &Expr,
4516 span: Option<HirSpan>,
4517 ) -> Result<wir::ValueId, IntegrationError> {
4518 let index = indices
4519 .first()
4520 .copied()
4521 .expect("nested indexed assignment has an inner index");
4522 let index_value = self.lower_value(index)?;
4523 let replacement = if indices.len() == 1 {
4524 if let Expr::Binary {
4525 op, left, right, ..
4526 } = value
4527 && left.as_ref() == target
4528 && let Some(call_name) = modify_catalog_name_from_str(op)
4529 {
4530 let current = self.lower_indexed_read(array, index, index_value)?;
4531 let right = self.lower_value(right)?;
4532 self.push_call(call_name, vec![current, right])
4533 } else {
4534 self.lower_value(value)?
4535 }
4536 } else {
4537 let child = self.lower_indexed_read(array, index, index_value)?;
4538 self.rebuild_indexed_value(child, &indices[1..], target, value, span)?
4539 };
4540 self.replace_array_element(array, index_value, replacement, span)
4541 }
4542
4543 fn lower_indexed_read(
4544 &mut self,
4545 array: wir::ValueId,
4546 index: &Expr,
4547 index_value: wir::ValueId,
4548 ) -> Result<wir::ValueId, IntegrationError> {
4549 if matches!(index, Expr::Number { value, .. } if *value == 0.0) {
4550 Ok(self.push_call("firstOf", vec![array]))
4551 } else {
4552 Ok(self.push_call("valueInArray", vec![array, index_value]))
4553 }
4554 }
4555
4556 fn replace_array_element(
4557 &mut self,
4558 array: wir::ValueId,
4559 index: wir::ValueId,
4560 replacement: wir::ValueId,
4561 span: Option<HirSpan>,
4562 ) -> Result<wir::ValueId, IntegrationError> {
4563 let zero = self.push_number(0.0, "0");
4564 let one = self.push_number(1.0, "1");
4565 let end = self.push_call("add", vec![index, one]);
4566 let maximum = self.push_number(999_999_999_999.0, "999999999999");
4567 let prefix = self.push_call("slice", vec![array, zero, index]);
4568 let middle = self.lower_array(vec![replacement], span)?;
4569 let suffix = self.push_call("slice", vec![array, end, maximum]);
4570 let with_replacement = self.push_call("appendToArray", vec![prefix, middle]);
4571 Ok(self.push_call("appendToArray", vec![with_replacement, suffix]))
4572 }
4573
4574 fn lower_action_call(
4575 &mut self,
4576 name: &str,
4577 args: &[Expr],
4578 span: Option<HirSpan>,
4579 ) -> Result<wir::ActionId, IntegrationError> {
4580 if args.is_empty() {
4581 if let Some(&subroutine) = self.subroutines.get(name) {
4582 return Ok(self.wir.actions.push(Action::CallSubroutine {
4583 subroutine,
4584 span: self.wir_span(span)?,
4585 callee_span: self.wir_span(span)?,
4586 }));
4587 }
4588 }
4589 if name == "chaseAtRate" {
4590 let args = args
4591 .iter()
4592 .map(|expr| self.lower_value(expr))
4593 .collect::<Result<Vec<_>, _>>()?;
4594 return Ok(self.wir.actions.push(Action::Call {
4595 name: name.to_string(),
4596 args,
4597 span: self.wir_span(span)?,
4598 }));
4599 }
4600 let function = self
4601 .compiler
4602 .manifest
4603 .resolve_function(name)
4604 .ok_or_else(|| self.unsupported(format!("unknown action '{name}'"), span))?;
4605 if !matches!(function.kind, FunctionKind::Action) {
4606 return Err(self.unsupported(format!("'{name}' is not a generic OPY action"), span));
4607 }
4608 if function.id == "async" {
4609 let [subroutine, behavior] = args else {
4610 return Err(
4611 self.unsupported("async requires a subroutine and an AsyncBehavior", span)
4612 );
4613 };
4614 let subroutine_name = match subroutine {
4615 Expr::Call { name, args, .. } if args.is_empty() => name,
4616 _ => {
4617 return Err(self.unsupported(
4618 "async requires a declared subroutine",
4619 subroutine.span().copied(),
4620 ));
4621 }
4622 };
4623 let subroutine_id = *self.subroutines.get(subroutine_name).ok_or_else(|| {
4624 self.unsupported(
4625 format!("unknown subroutine '{subroutine_name}'"),
4626 subroutine.span().copied(),
4627 )
4628 })?;
4629 let subroutine = self.push_value(Value::Subroutine(subroutine_id));
4630 let behavior = self.lower_value(behavior)?;
4631 return Ok(self.wir.actions.push(Action::Call {
4632 name: "startRule".to_string(),
4633 args: vec![subroutine, behavior],
4634 span: self.wir_span(span)?,
4635 }));
4636 }
4637 if matches!(
4638 function.id.as_str(),
4639 "hudHeader" | "hudSubheader" | "hudSubtext"
4640 ) {
4641 let text_slot = match function.id.as_str() {
4642 "hudHeader" => 1,
4643 "hudSubheader" => 2,
4644 "hudSubtext" => 3,
4645 _ => unreachable!(),
4646 };
4647 return self.lower_hud_text(args, span, text_slot, &function.id);
4648 }
4649 if function.id == "createDummy" && args.len() == 4 {
4650 let mut lowered = args
4651 .iter()
4652 .map(|expr| self.lower_value(expr))
4653 .collect::<Result<Vec<_>, _>>()?;
4654 let mut zero_vector = Vec::with_capacity(3);
4655 for value in [0.0, 0.0, 0.0] {
4656 zero_vector.push(self.push_value(Value::Number {
4657 value,
4658 text: "0".to_string(),
4659 }));
4660 }
4661 lowered.push(self.push_call("vector", zero_vector));
4662 let args = self.normalize_contextual_arguments("createDummyBot", lowered);
4663 return Ok(self.wir.actions.push(Action::Call {
4664 name: "createDummyBot".to_string(),
4665 args,
4666 span: self.wir_span(span)?,
4667 }));
4668 }
4669 let catalog_id = function.catalog_id.as_ref().ok_or_else(|| {
4670 self.unsupported(
4671 format!(
4672 "action '{}' requires a special lowering not in #46",
4673 function.id
4674 ),
4675 span,
4676 )
4677 })?;
4678 let args = args
4679 .iter()
4680 .map(|expr| self.lower_value(expr))
4681 .collect::<Result<Vec<_>, _>>()?;
4682 let args = self.normalize_catalog_argument_domains(catalog_id, args);
4683 Ok(self.wir.actions.push(Action::Call {
4684 name: catalog_id.clone(),
4685 args,
4686 span: self.wir_span(span)?,
4687 }))
4688 }
4689
4690 fn normalize_catalog_argument_domains(
4691 &mut self,
4692 catalog_id: &str,
4693 mut args: Vec<wir::ValueId>,
4694 ) -> Vec<wir::ValueId> {
4695 if self
4696 .compiler
4697 .catalog
4698 .entry(Kind::Action, catalog_id)
4699 .is_none()
4700 {
4701 return args;
4702 }
4703 let mut index = 0;
4704 while index < args.len() {
4705 args[index] = self.normalize_contextual_argument(catalog_id, index, args[index]);
4706 let Some(domain) = self
4707 .compiler
4708 .catalog
4709 .entry(Kind::Action, catalog_id)
4710 .and_then(|entry| entry.param_domain(index))
4711 .map(str::to_string)
4712 else {
4713 index += 1;
4714 continue;
4715 };
4716 let Some(ValueNode {
4717 value: Value::Enum { value_type, value },
4718 span,
4719 }) = self.wir.values.get(args[index])
4720 else {
4721 index += 1;
4722 continue;
4723 };
4724 if value_type == "Team" && domain == "Color" {
4725 args[index] = self.wir.values.push(ValueNode::new(
4726 Value::Enum {
4727 value_type: domain,
4728 value: value.clone(),
4729 },
4730 *span,
4731 ));
4732 }
4733 index += 1;
4734 }
4735 args
4736 }
4737
4738 fn lower_hud_text(
4739 &mut self,
4740 args: &[Expr],
4741 span: Option<HirSpan>,
4742 text_slot: usize,
4743 function_name: &str,
4744 ) -> Result<wir::ActionId, IntegrationError> {
4745 let [
4746 visible_to,
4747 text,
4748 position,
4749 sort_order,
4750 color,
4751 reevaluation,
4752 spectators,
4753 ] = args
4754 else {
4755 return Err(self.unsupported(
4756 format!("{function_name} requires exactly seven bound arguments"),
4757 span,
4758 ));
4759 };
4760 let visible_to = self.lower_hud_visible_to(visible_to)?;
4761 let mut text_slots = [
4762 self.push_value(Value::Null),
4763 self.push_value(Value::Null),
4764 self.push_value(Value::Null),
4765 ];
4766 let text_value = self.lower_text_value(text)?;
4767 text_slots[text_slot - 1] = self.push_call("customString", vec![text_value]);
4768 let mut colors = [
4769 self.push_value(Value::Null),
4770 self.push_value(Value::Null),
4771 self.push_value(Value::Null),
4772 ];
4773 colors[text_slot - 1] = self.lower_value(color)?;
4774 let args = vec![
4775 visible_to,
4776 text_slots[0],
4777 text_slots[1],
4778 text_slots[2],
4779 self.lower_value(position)?,
4780 self.lower_value(sort_order)?,
4781 colors[0],
4782 colors[1],
4783 colors[2],
4784 self.lower_value(reevaluation)?,
4785 self.lower_value(spectators)?,
4786 ];
4787 let args = self.normalize_contextual_arguments("createHudText", args);
4788 Ok(self.wir.actions.push(Action::Call {
4789 name: "createHudText".to_string(),
4790 args,
4791 span: self.wir_span(span)?,
4792 }))
4793 }
4794
4795 fn lower_hud_visible_to(&mut self, expr: &Expr) -> Result<wir::ValueId, IntegrationError> {
4796 if let Expr::Call { name, args, .. } = expr {
4797 if name == "getAllPlayers" && args.is_empty() {
4798 let all_teams = self.push_value(Value::Enum {
4799 value_type: "Team".to_string(),
4800 value: "ALL".to_string(),
4801 });
4802 return Ok(self.push_call("allPlayers", vec![all_teams]));
4803 }
4804 }
4805 self.lower_value(expr)
4806 }
4807
4808 fn lower_receiver_action_call(
4809 &mut self,
4810 receiver: &Expr,
4811 name: &str,
4812 args: &[Expr],
4813 span: Option<HirSpan>,
4814 ) -> Result<wir::ActionId, IntegrationError> {
4815 let function = self
4816 .compiler
4817 .manifest
4818 .resolve_member(name)
4819 .ok_or_else(|| self.unsupported(format!("unknown member action '{name}'"), span))?;
4820 if !matches!(function.kind, FunctionKind::MemberAction) {
4821 return Err(self.unsupported(format!("'{name}' is not a member action"), span));
4822 }
4823
4824 if matches!(function.id.as_str(), "append" | "remove") {
4827 let [value] = args else {
4828 return Err(self.unsupported(
4829 format!("{} requires exactly one argument", function.id),
4830 span,
4831 ));
4832 };
4833 let op = if function.id == "append" {
4834 wir::ModifyOp::AppendToArray
4835 } else {
4836 wir::ModifyOp::RemoveFromArray
4837 };
4838 let value = self.lower_value(value)?;
4839 let value = self.normalize_modify_value(op, value);
4840 return match receiver {
4841 Expr::GlobalVar {
4842 name,
4843 span: target_span,
4844 } => {
4845 let variable = *self.globals.get(name).ok_or_else(|| {
4846 self.unsupported(format!("unknown global variable '{name}'"), *target_span)
4847 })?;
4848 Ok(self.wir.actions.push(Action::ModifyGlobalVariable {
4849 variable,
4850 op,
4851 value,
4852 span: self.wir_span(span)?,
4853 target_span: self.wir_span(*target_span)?,
4854 }))
4855 }
4856 Expr::PlayerVar {
4857 player,
4858 name,
4859 span: target_span,
4860 ..
4861 } => {
4862 let variable = *self.players.get(name).ok_or_else(|| {
4863 self.unsupported(format!("unknown player variable '{name}'"), *target_span)
4864 })?;
4865 let player = self.lower_value(player)?;
4866 Ok(self.wir.actions.push(Action::ModifyPlayerVariable {
4867 player,
4868 variable,
4869 op,
4870 value,
4871 span: self.wir_span(span)?,
4872 target_span: self.wir_span(*target_span)?,
4873 }))
4874 }
4875 _ => Err(self.unsupported(
4876 "append requires a global or player variable receiver",
4877 receiver.span().copied().or(span),
4878 )),
4879 };
4880 }
4881
4882 let catalog_id = function.catalog_id.as_ref().ok_or_else(|| {
4883 self.unsupported(
4884 format!(
4885 "member action '{}' has no canonical catalog identity",
4886 function.id
4887 ),
4888 span,
4889 )
4890 })?;
4891 let mut lowered = Vec::with_capacity(args.len() + 1);
4892 lowered.push(self.lower_value(receiver)?);
4893 lowered.extend(
4894 args.iter()
4895 .map(|arg| self.lower_value(arg))
4896 .collect::<Result<Vec<_>, _>>()?,
4897 );
4898 let args = self.normalize_contextual_arguments(catalog_id, lowered);
4899 Ok(self.wir.actions.push(Action::Call {
4900 name: catalog_id.clone(),
4901 args,
4902 span: self.wir_span(span)?,
4903 }))
4904 }
4905
4906 fn lower_value(&mut self, expr: &Expr) -> Result<wir::ValueId, IntegrationError> {
4907 let span = expr.span().copied();
4908 let value = match expr {
4909 Expr::Number { value, text, .. } => Value::Number {
4910 value: *value,
4911 text: canonical_number_text(*value, text),
4912 },
4913 Expr::String { value, .. } => {
4914 return self.lower_custom_string(value.clone(), span);
4915 }
4916 Expr::Bool { value, .. } => Value::Bool(*value),
4917 Expr::Null { .. } => Value::Null,
4918 Expr::Local { name, .. } => {
4919 let binding = self.array_bindings.iter().rev().find(|binding| {
4920 binding.element == *name || binding.index.as_deref() == Some(name)
4921 });
4922 match binding {
4923 Some(binding) if binding.element == *name => {
4924 return Ok(self.push_call("currentArrayElement", Vec::new()));
4925 }
4926 Some(_) => return Ok(self.push_call("currentArrayIndex", Vec::new())),
4927 None => {
4928 return Err(self.unsupported(
4929 format!("local '{name}' is not inside a supported array callback"),
4930 span,
4931 ));
4932 }
4933 }
4934 }
4935 Expr::Type { .. } => {
4936 return Err(self.unsupported(
4937 "type expressions are only valid as createWorkshopSetting type arguments",
4938 span,
4939 ));
4940 }
4941 Expr::GlobalVar { name, .. } => {
4942 let id = *self.globals.get(name).ok_or_else(|| {
4943 self.unsupported(format!("unknown global variable '{name}'"), span)
4944 })?;
4945 Value::GlobalVariable(id)
4946 }
4947 Expr::PlayerVar { player, name, .. } => {
4948 let player = self.lower_value(player)?;
4949 let id = *self.players.get(name).ok_or_else(|| {
4950 self.unsupported(format!("unknown player variable '{name}'"), span)
4951 })?;
4952 Value::PlayerVariable {
4953 player,
4954 variable: id,
4955 }
4956 }
4957 Expr::EventPlayer { .. } => Value::EventPlayer,
4958 Expr::HostPlayer { .. } => Value::Call {
4959 name: "hostPlayer".to_string(),
4960 args: Vec::new(),
4961 },
4962 Expr::Enum {
4963 value_type, value, ..
4964 } => {
4965 if self
4966 .compiler
4967 .catalog
4968 .enum_spelling(value_type, &Locale::new("en-US"), value)
4969 .is_none()
4970 {
4971 return Err(self.unsupported(
4972 format!("unknown catalog enum member '{value_type}.{value}'"),
4973 span,
4974 ));
4975 }
4976 Value::Enum {
4977 value_type: value_type.clone(),
4978 value: value.clone(),
4979 }
4980 }
4981 Expr::Array { elements, .. } => {
4982 let elements = elements
4983 .iter()
4984 .map(|element| self.lower_value(element))
4985 .collect::<Result<Vec<_>, _>>()?;
4986 return self.lower_array(elements, span);
4987 }
4988 Expr::Vector { x, y, z, .. } => Value::Call {
4989 name: "vector".to_string(),
4990 args: vec![
4991 self.lower_value(x)?,
4992 self.lower_value(y)?,
4993 self.lower_value(z)?,
4994 ],
4995 },
4996 Expr::Constant { name, .. } => {
4997 let const_expr = *self
4998 .constants
4999 .get(name)
5000 .ok_or_else(|| self.unsupported(format!("unknown constant '{name}'"), span))?;
5001 return self.lower_value(const_expr);
5002 }
5003 Expr::Index { array, index, .. } => {
5004 if let Expr::Dict { entries, .. } = array.as_ref()
5005 && is_literal_key(index)
5006 && entries.iter().all(|entry| is_literal_key(&entry.key))
5007 {
5008 if let Some(value) = entries
5009 .iter()
5010 .find(|entry| literal_key_matches(&entry.key, index))
5011 .map(|entry| &entry.value)
5012 {
5013 return self.lower_value(value);
5014 }
5015 return Ok(self
5016 .wir
5017 .values
5018 .push(ValueNode::new(Value::Null, self.wir_span(span)?)));
5019 }
5020 if matches!(index.as_ref(), Expr::Number { value, .. } if *value == 0.0) {
5024 Value::Call {
5025 name: "firstOf".to_string(),
5026 args: vec![self.lower_value(array)?],
5027 }
5028 } else {
5029 Value::Call {
5030 name: "valueInArray".to_string(),
5031 args: vec![self.lower_value(array)?, self.lower_value(index)?],
5032 }
5033 }
5034 }
5035 Expr::Format { text, args, .. } => {
5036 if let Some(value) = fold_literal_format(text, args) {
5037 return self.lower_custom_string(value, span);
5038 }
5039 let format_text = canonical_format_text(text);
5040 if args.len() <= 3 {
5041 let text_node = self.wir.values.push(ValueNode::new(
5042 Value::String(format_text),
5043 self.wir_span(span)?,
5044 ));
5045 let mut call_args = vec![text_node];
5046 for arg in args {
5047 call_args.push(self.lower_value(arg)?);
5048 }
5049 Value::Call {
5050 name: "customString".to_string(),
5051 args: call_args,
5052 }
5053 } else {
5054 let chunks = split_format_chunks(&format_text, args.len()).ok_or_else(|| {
5055 self.unsupported(
5056 "format strings with more than three replacements require sequential placeholders",
5057 span,
5058 )
5059 })?;
5060 let lowered_args = args
5061 .iter()
5062 .map(|arg| self.lower_value(arg))
5063 .collect::<Result<Vec<_>, _>>()?;
5064 let mut parts = Vec::with_capacity(chunks.len());
5065 for (chunk, indices) in chunks {
5066 let text = self
5067 .wir
5068 .values
5069 .push(ValueNode::new(Value::String(chunk), self.wir_span(span)?));
5070 let mut call_args = vec![text];
5071 call_args.extend(indices.into_iter().map(|index| lowered_args[index]));
5072 parts.push(self.push_call("customString", call_args));
5073 }
5074 let separator = self.push_value(Value::String("{0}{1}".to_string()));
5075 let mut value = parts[0];
5076 for part in parts.into_iter().skip(1) {
5077 value = self.push_call("customString", vec![separator, value, part]);
5078 }
5079 return Ok(value);
5080 }
5081 }
5082 Expr::Conditional {
5083 then_value,
5084 condition,
5085 else_value,
5086 ..
5087 } => Value::Call {
5088 name: "ifThenElse".to_string(),
5089 args: vec![
5090 self.lower_value(condition)?,
5091 self.lower_value(then_value)?,
5092 self.lower_value(else_value)?,
5093 ],
5094 },
5095 Expr::Binary {
5096 op, left, right, ..
5097 } => {
5098 let left = self.lower_value(left)?;
5099 let right = self.lower_value(right)?;
5100 if let Some(value) = self.fold_numeric_binary(op, left, right) {
5101 Value::Number {
5102 value,
5103 text: computed_number_text(value),
5104 }
5105 } else {
5106 match op.as_str() {
5107 "==" | "!=" | "<" | "<=" | ">" | ">=" => Value::Call {
5108 name: op.clone(),
5109 args: vec![left, right],
5110 },
5111 "+" => Value::Call {
5112 name: "add".to_string(),
5113 args: vec![left, right],
5114 },
5115 "-" => Value::Call {
5116 name: "subtract".to_string(),
5117 args: vec![left, right],
5118 },
5119 "*" => Value::Call {
5120 name: "multiply".to_string(),
5121 args: vec![left, right],
5122 },
5123 "/" => Value::Call {
5124 name: "divide".to_string(),
5125 args: vec![left, right],
5126 },
5127 "%" => Value::Call {
5128 name: "modulo".to_string(),
5129 args: vec![left, right],
5130 },
5131 "**" => Value::Call {
5132 name: "raiseToPower".to_string(),
5133 args: vec![left, right],
5134 },
5135 "and" => Value::Call {
5136 name: "and".to_string(),
5137 args: vec![left, right],
5138 },
5139 "or" => Value::Call {
5140 name: "or".to_string(),
5141 args: vec![left, right],
5142 },
5143 "in" => Value::Call {
5144 name: "arrayContains".to_string(),
5145 args: vec![right, left],
5146 },
5147 "not in" => {
5148 let wir_span = self.wir_span(span)?;
5149 let contains = self.wir.values.push(ValueNode::new(
5150 Value::Call {
5151 name: "arrayContains".to_string(),
5152 args: vec![right, left],
5153 },
5154 wir_span,
5155 ));
5156 Value::Call {
5157 name: "not".to_string(),
5158 args: vec![contains],
5159 }
5160 }
5161 _ => {
5162 return Err(self.unsupported(
5163 format!(
5164 "binary operator '{op}' is not currently representable in canonical WIR"
5165 ),
5166 span,
5167 ));
5168 }
5169 }
5170 }
5171 }
5172 Expr::Unary { op, operand, .. } => match op.as_str() {
5173 "not" => {
5174 if let Expr::Binary {
5179 op: comparison,
5180 left,
5181 right,
5182 ..
5183 } = operand.as_ref()
5184 {
5185 if let Some(negated) = negated_comparison(comparison) {
5186 Value::Call {
5187 name: negated.to_string(),
5188 args: vec![self.lower_value(left)?, self.lower_value(right)?],
5189 }
5190 } else {
5191 Value::Call {
5192 name: "not".to_string(),
5193 args: vec![self.lower_value(operand)?],
5194 }
5195 }
5196 } else {
5197 Value::Call {
5198 name: "not".to_string(),
5199 args: vec![self.lower_value(operand)?],
5200 }
5201 }
5202 }
5203 "-" => Value::Call {
5204 name: "-".to_string(),
5205 args: vec![self.lower_value(operand)?],
5206 },
5207 "+" => return self.lower_value(operand),
5208 _ => {
5209 return Err(self.unsupported(
5210 format!(
5211 "unary operator '{op}' is not currently representable in canonical WIR"
5212 ),
5213 span,
5214 ));
5215 }
5216 },
5217 Expr::Call { name, args, .. } => {
5218 if name == "createWorkshopSetting" {
5219 return self.lower_workshop_setting(args, span);
5220 }
5221 if matches!(name.as_str(), "attacker" | "victim") && args.is_empty() {
5222 return Ok(self.push_call(name, Vec::new()));
5223 }
5224 if name == "localPlayer" && args.is_empty() {
5225 return Ok(self.push_call(name, Vec::new()));
5226 }
5227 if name == "ruleCondition" {
5228 if !args.is_empty() {
5229 return Err(
5230 self.unsupported("ruleCondition does not accept arguments", span)
5231 );
5232 }
5233 let conditions = self.current_rule_conditions.clone().ok_or_else(|| {
5234 self.unsupported("ruleCondition is only valid inside a rule", span)
5235 })?;
5236 let Some((first, rest)) = conditions.split_first() else {
5237 return Ok(self.push_value(Value::Bool(true)));
5238 };
5239 let mut combined = *first;
5240 for condition in rest {
5241 combined = self.push_call("and", vec![combined, *condition]);
5242 }
5243 return Ok(combined);
5244 }
5245 if name == "vect" && args.len() == 3 {
5246 Value::Vector {
5247 x: self.lower_value(&args[0])?,
5248 y: self.lower_value(&args[1])?,
5249 z: self.lower_value(&args[2])?,
5250 }
5251 } else if matches!(
5252 name.as_str(),
5253 "createWorkshopSettingBool"
5254 | "createWorkshopSettingEnum"
5255 | "createWorkshopSettingInt"
5256 | "createWorkshopSettingFloat"
5257 ) {
5258 let mut lowered = args
5259 .iter()
5260 .map(|arg| self.lower_value(arg))
5261 .collect::<Result<Vec<_>, _>>()?;
5262 if name == "createWorkshopSettingFloat" && lowered.len() == 5 {
5263 lowered.push(self.push_number(0.0, "0"));
5264 }
5265 Value::Call {
5266 name: match name.as_str() {
5267 "createWorkshopSettingBool" => "workshopSettingToggle",
5268 "createWorkshopSettingEnum" => "workshopSettingCombo",
5269 "createWorkshopSettingInt" => "workshopSettingInteger",
5270 _ => name,
5271 }
5272 .to_string(),
5273 args: lowered,
5274 }
5275 } else if matches!(name.as_str(), "all" | "any") {
5276 let call_name = if name == "all" {
5277 "isTrueForAll"
5278 } else {
5279 "isTrueForAny"
5280 };
5281 let [array] = args.as_slice() else {
5282 return Err(self.unsupported(
5283 format!("{name} requires exactly one array argument"),
5284 span,
5285 ));
5286 };
5287 let (array, condition) = match array {
5288 Expr::Comprehension {
5289 element,
5290 variable,
5291 index,
5292 iterable,
5293 ..
5294 } => {
5295 if index.is_some() {
5296 return Err(self.unsupported(
5297 format!("{name} does not support an index binder"),
5298 span,
5299 ));
5300 }
5301 let iterable = self.lower_value(iterable)?;
5302 self.array_bindings.push(ArrayBinding {
5303 element: variable.clone(),
5304 index: None,
5305 });
5306 let condition = self.lower_value(element);
5307 self.array_bindings.pop();
5308 (iterable, condition?)
5309 }
5310 array => (
5311 self.lower_value(array)?,
5312 self.push_call("currentArrayElement", Vec::new()),
5313 ),
5314 };
5315 Value::Call {
5316 name: call_name.to_string(),
5317 args: vec![array, condition],
5318 }
5319 } else if matches!(name.as_str(), "ceil" | "floor" | "round") {
5320 let [value] = args.as_slice() else {
5321 return Err(self.unsupported(
5322 format!("{name} requires exactly one numeric argument"),
5323 span,
5324 ));
5325 };
5326 let rounding = match name.as_str() {
5327 "ceil" => "UP",
5328 "floor" => "DOWN",
5329 "round" => "NEAREST",
5330 _ => unreachable!(),
5331 };
5332 let rounding = self.push_value(Value::Enum {
5333 value_type: "Rounding".to_string(),
5334 value: rounding.to_string(),
5335 });
5336 Value::Call {
5337 name: "roundToInteger".to_string(),
5338 args: vec![self.lower_value(value)?, rounding],
5339 }
5340 } else if name == "sorted" {
5341 let (array, key) = match args.as_slice() {
5342 [array] => (
5343 self.lower_value(array)?,
5344 self.push_call("currentArrayElement", Vec::new()),
5345 ),
5346 [
5347 array,
5348 Expr::Lambda {
5349 params, body, span, ..
5350 },
5351 ] => {
5352 let array = self.lower_value(array)?;
5353 let key = self.lower_array_callback(params, body, *span)?;
5354 (array, key)
5355 }
5356 _ => {
5357 return Err(self.unsupported(
5358 "sorted requires an array and an optional lambda key",
5359 span,
5360 ));
5361 }
5362 };
5363 Value::Call {
5364 name: "sortedArray".to_string(),
5365 args: vec![array, key],
5366 }
5367 } else {
5368 let function = self
5369 .compiler
5370 .manifest
5371 .resolve_function(name)
5372 .ok_or_else(|| self.unsupported(format!("unknown value '{name}'"), span))?;
5373 if !matches!(function.kind, FunctionKind::Value) {
5374 return Err(
5375 self.unsupported(format!("'{name}' is not a generic OPY value"), span)
5376 );
5377 }
5378 let catalog_id = function.catalog_id.as_ref().ok_or_else(|| {
5379 self.unsupported(
5380 format!(
5381 "value '{}' requires a special lowering not in #46",
5382 function.id
5383 ),
5384 span,
5385 )
5386 })?;
5387 Value::Call {
5388 name: catalog_id.clone(),
5389 args: args
5390 .iter()
5391 .map(|arg| self.lower_value(arg))
5392 .collect::<Result<Vec<_>, _>>()?,
5393 }
5394 }
5395 }
5396 Expr::ReceiverCall {
5397 receiver,
5398 name,
5399 args,
5400 ..
5401 } => {
5402 if matches!(name.as_str(), "all" | "any") {
5403 let [
5404 Expr::Lambda {
5405 params, body, span, ..
5406 },
5407 ] = args.as_slice()
5408 else {
5409 return Err(
5410 self.unsupported(format!("{name} requires one lambda argument"), span)
5411 );
5412 };
5413 let condition = self.lower_array_callback(params, body, *span)?;
5414 let receiver = self.lower_value(receiver)?;
5415 return Ok(self.wir.values.push(ValueNode::new(
5416 Value::Call {
5417 name: if name == "all" {
5418 "isTrueForAll"
5419 } else {
5420 "isTrueForAny"
5421 }
5422 .to_string(),
5423 args: vec![receiver, condition],
5424 },
5425 self.wir_span(*span)?,
5426 )));
5427 }
5428 let function = self.compiler.manifest.resolve_member(name).ok_or_else(|| {
5429 self.unsupported(format!("unknown member value '{name}'"), span)
5430 })?;
5431 if !matches!(function.kind, FunctionKind::MemberValue) {
5432 return Err(self.unsupported(format!("'{name}' is not a member value"), span));
5433 }
5434 if matches!(
5435 function.id.as_str(),
5436 "getHitPosition" | "getPlayerHit" | "getNormal"
5437 ) {
5438 let member_name = function.id.as_str();
5439 let Expr::Call {
5440 name: receiver_name,
5441 args: receiver_args,
5442 ..
5443 } = receiver.as_ref()
5444 else {
5445 return Err(self.unsupported(
5446 format!("{member_name} requires a raycast receiver"),
5447 span,
5448 ));
5449 };
5450 if receiver_name != "raycast" || !args.is_empty() {
5451 return Err(self.unsupported(
5452 format!("{member_name} requires raycast(...) with no member arguments"),
5453 span,
5454 ));
5455 }
5456 let catalog_id = function.catalog_id.clone().ok_or_else(|| {
5457 self.unsupported(
5458 format!("{member_name} has no canonical catalog identity"),
5459 span,
5460 )
5461 })?;
5462 let lowered_args = receiver_args
5463 .iter()
5464 .map(|arg| self.lower_value(arg))
5465 .collect::<Result<Vec<_>, _>>()?;
5466 return Ok(self.wir.values.push(ValueNode::new(
5467 Value::Call {
5468 name: catalog_id,
5469 args: lowered_args,
5470 },
5471 self.wir_span(span)?,
5472 )));
5473 }
5474 if function.id == "filter" {
5475 let [
5476 Expr::Lambda {
5477 params, body, span, ..
5478 },
5479 ] = args.as_slice()
5480 else {
5481 return Err(self.unsupported("filter requires one lambda argument", span));
5482 };
5483 let condition = self.lower_array_callback(params, body, *span)?;
5484 Value::Call {
5485 name: "filteredArray".to_string(),
5486 args: vec![self.lower_value(receiver)?, condition],
5487 }
5488 } else if matches!(function.id.as_str(), "concat" | "exclude") {
5489 let [value] = args.as_slice() else {
5490 return Err(self.unsupported(
5491 format!("{} requires exactly one argument", function.id),
5492 span,
5493 ));
5494 };
5495 Value::Call {
5496 name: if function.id == "concat" {
5497 "appendToArray"
5498 } else {
5499 "removeFromArray"
5500 }
5501 .to_string(),
5502 args: vec![self.lower_value(receiver)?, self.lower_value(value)?],
5503 }
5504 } else {
5505 let catalog_id = function.catalog_id.as_ref().ok_or_else(|| {
5506 self.unsupported(
5507 format!(
5508 "member value '{}' has no canonical catalog identity",
5509 function.id
5510 ),
5511 span,
5512 )
5513 })?;
5514 let mut lowered = Vec::with_capacity(args.len() + 1);
5515 lowered.push(self.lower_value(receiver)?);
5516 lowered.extend(
5517 args.iter()
5518 .map(|arg| self.lower_value(arg))
5519 .collect::<Result<Vec<_>, _>>()?,
5520 );
5521 Value::Call {
5522 name: catalog_id.clone(),
5523 args: lowered,
5524 }
5525 }
5526 }
5527 Expr::Member {
5528 receiver, member, ..
5529 } => {
5530 let receiver = self.lower_value(receiver)?;
5531 if let Some(name) = match member.as_str() {
5532 "x" => Some("__xComponentOf__"),
5533 "y" => Some("__yComponentOf__"),
5534 "z" => Some("__zComponentOf__"),
5535 _ => None,
5536 } {
5537 Value::Call {
5538 name: name.to_string(),
5539 args: vec![receiver],
5540 }
5541 } else {
5542 let member = self.wir.values.push(ValueNode::new(
5543 Value::String(member.clone()),
5544 self.wir_span(span)?,
5545 ));
5546 Value::Call {
5547 name: "memberAccess".to_string(),
5548 args: vec![receiver, member],
5549 }
5550 }
5551 }
5552 Expr::Comprehension {
5553 element,
5554 variable,
5555 index,
5556 iterable,
5557 condition,
5558 span: comprehension_span,
5559 ..
5560 } => {
5561 if condition.is_some() && index.is_some() {
5562 return Err(self.unsupported(
5563 "comprehensions with both a filter and an index binder are not currently representable in canonical WIR",
5564 *comprehension_span,
5565 ));
5566 }
5567 let iterable = self.lower_value(iterable)?;
5568 let iterable = if self.value_is_known_player(iterable) {
5569 self.push_call("array", vec![iterable])
5570 } else {
5571 iterable
5572 };
5573 let binding = ArrayBinding {
5574 element: variable.clone(),
5575 index: index.clone(),
5576 };
5577 self.array_bindings.push(binding);
5578 let predicate = condition
5579 .as_deref()
5580 .map(|condition| self.lower_value(condition));
5581 let element = self.lower_value(element);
5582 self.array_bindings.pop();
5583 let element = element?;
5584 let iterable = if let Some(predicate) = predicate {
5585 let predicate = predicate?;
5586 self.push_call("filteredArray", vec![iterable, predicate])
5587 } else {
5588 iterable
5589 };
5590 Value::Call {
5591 name: "mappedArray".to_string(),
5592 args: vec![iterable, element],
5593 }
5594 }
5595 Expr::Lambda { span, .. } => {
5596 return Err(self.unsupported(
5597 "lambda expressions are only representable as supported array operation arguments",
5598 *span,
5599 ));
5600 }
5601 Expr::StringModifier {
5602 modifier,
5603 value,
5604 span,
5605 } => {
5606 let value = match modifier.as_str() {
5607 "b" => big_letters(value),
5608 "c" => case_sensitive(value),
5609 "w" => fullwidth(value),
5610 _ => {
5611 return Err(self.unsupported(
5612 format!(
5613 "string modifier '{modifier}' is not currently representable in canonical WIR"
5614 ),
5615 *span,
5616 ));
5617 }
5618 };
5619 return self.lower_custom_string(value, *span);
5620 }
5621 _ => {
5622 return Err(self.unsupported(
5623 format!(
5624 "expression '{}' is not currently representable in canonical WIR",
5625 expr.kind_name()
5626 ),
5627 span,
5628 ));
5629 }
5630 };
5631 let value_id = self
5632 .wir
5633 .values
5634 .push(ValueNode::new(value, self.wir_span(span)?));
5635 let Some(ValueNode {
5636 value: Value::Call { name, args },
5637 ..
5638 }) = self.wir.values.get(value_id)
5639 else {
5640 return Ok(value_id);
5641 };
5642 let name = name.clone();
5643 let args = args.clone();
5644 let args = self.normalize_contextual_arguments(&name, args);
5645 if let Some(ValueNode {
5646 value: Value::Call {
5647 args: target_args, ..
5648 },
5649 ..
5650 }) = self.wir.values.get_mut(value_id)
5651 {
5652 *target_args = args;
5653 }
5654 Ok(value_id)
5655 }
5656
5657 fn lower_array_callback(
5658 &mut self,
5659 params: &[String],
5660 body: &Expr,
5661 span: Option<HirSpan>,
5662 ) -> Result<wir::ValueId, IntegrationError> {
5663 if !(1..=2).contains(¶ms.len()) {
5664 return Err(self.unsupported(
5665 "array callbacks require one element parameter and at most one index parameter",
5666 span,
5667 ));
5668 }
5669 if params.windows(2).any(|pair| pair[0] == pair[1]) {
5670 return Err(
5671 self.unsupported("array callback parameters must have distinct names", span)
5672 );
5673 }
5674 self.array_bindings.push(ArrayBinding {
5675 element: params[0].clone(),
5676 index: params.get(1).cloned(),
5677 });
5678 let result = self.lower_value(body);
5679 self.array_bindings.pop();
5680 result
5681 }
5682
5683 fn lower_workshop_setting(
5684 &mut self,
5685 args: &[Expr],
5686 span: Option<HirSpan>,
5687 ) -> Result<wir::ValueId, IntegrationError> {
5688 let [
5689 Expr::Type {
5690 name: setting_type,
5691 args: type_args,
5692 span: type_span,
5693 },
5694 category,
5695 setting_name,
5696 default,
5697 sort_order,
5698 ] = args
5699 else {
5700 return Err(self.unsupported(
5701 "createWorkshopSetting requires a type and four value arguments",
5702 span,
5703 ));
5704 };
5705
5706 let catalog_name = match (setting_type.as_str(), type_args.as_slice()) {
5707 ("bool", []) => "createWorkshopSettingBool",
5708 ("int", [_, _]) => "createWorkshopSettingInt",
5709 ("float", [_, _]) => "createWorkshopSettingFloat",
5710 ("int", []) | ("float", []) => {
5711 return Err(self.unsupported(
5712 format!("createWorkshopSetting type '{setting_type}' requires a numeric range"),
5713 type_span.or(span),
5714 ));
5715 }
5716 _ => {
5717 return Err(self.unsupported(
5718 format!("unsupported createWorkshopSetting type '{setting_type}'"),
5719 type_span.or(span),
5720 ));
5721 }
5722 };
5723
5724 let category = match category {
5727 Expr::String { value, .. } if value.is_empty() => {
5728 self.push_value(Value::String("\u{3000}".to_string()))
5729 }
5730 _ => self.lower_value(category)?,
5731 };
5732 let mut lowered = vec![
5733 category,
5734 self.lower_value(setting_name)?,
5735 self.lower_value(default)?,
5736 ];
5737 if let [minimum, maximum] = type_args.as_slice() {
5738 lowered.push(self.lower_value(minimum)?);
5739 lowered.push(self.lower_value(maximum)?);
5740 }
5741 lowered.push(self.lower_value(sort_order)?);
5742 Ok(self.push_call(catalog_name, lowered))
5743 }
5744
5745 fn wir_span(&self, span: Option<HirSpan>) -> Result<Option<WorkshopSpan>, IntegrationError> {
5746 let Some(span) = span else {
5747 return Ok(None);
5748 };
5749 let file = *self.files.get(&span.file).ok_or_else(|| {
5750 IntegrationError::new(
5751 "source-file",
5752 format!("HIR span references unknown source file id {}", span.file),
5753 Some(span),
5754 )
5755 })?;
5756 Ok(Some(WorkshopSpan::new(
5757 file,
5758 WorkshopPosition::new(span.start.line, span.start.col),
5759 WorkshopPosition::new(span.end.line, span.end.col),
5760 )))
5761 }
5762
5763 fn hir_span_from_workshop(&self, span: WorkshopSpan) -> Option<HirSpan> {
5764 let file = *self.wir_to_hir_files.get(span.file.index())?;
5765 Some(HirSpan {
5766 file,
5767 start: hir::Position {
5768 line: span.start.line,
5769 col: span.start.col,
5770 },
5771 end: hir::Position {
5772 line: span.end.line,
5773 col: span.end.col,
5774 },
5775 })
5776 }
5777
5778 fn unsupported(&self, message: impl Into<String>, span: Option<HirSpan>) -> IntegrationError {
5779 IntegrationError::new("unsupported-integration-surface", message, span)
5780 }
5781}
5782
5783fn implicit_default_variables(
5787 hir: &hir::Program,
5788) -> (
5789 BTreeMap<String, Option<HirSpan>>,
5790 BTreeMap<String, Option<HirSpan>>,
5791) {
5792 let declared_globals = hir
5793 .declarations
5794 .iter()
5795 .filter_map(|declaration| match declaration {
5796 hir::Declaration::GlobalVariable { name, .. } => Some(name.as_str()),
5797 _ => None,
5798 })
5799 .collect::<HashSet<_>>();
5800 let declared_players = hir
5801 .declarations
5802 .iter()
5803 .filter_map(|declaration| match declaration {
5804 hir::Declaration::PlayerVariable { name, .. } => Some(name.as_str()),
5805 _ => None,
5806 })
5807 .collect::<HashSet<_>>();
5808 let mut globals = BTreeMap::new();
5809 let mut players = BTreeMap::new();
5810 for declaration in &hir.declarations {
5811 let initializer = match declaration {
5812 hir::Declaration::GlobalVariable { initializer, .. }
5813 | hir::Declaration::PlayerVariable { initializer, .. } => initializer.as_ref(),
5814 hir::Declaration::Constant { value, .. } => Some(value),
5815 _ => None,
5816 };
5817 if let Some(expr) = initializer {
5818 collect_implicit_expr(
5819 expr,
5820 &declared_globals,
5821 &declared_players,
5822 &mut globals,
5823 &mut players,
5824 );
5825 }
5826 }
5827 for entry in &hir.rules {
5828 match entry {
5829 RuleEntry::Rule(rule) => {
5830 for condition in &rule.conditions {
5831 collect_implicit_expr(
5832 condition,
5833 &declared_globals,
5834 &declared_players,
5835 &mut globals,
5836 &mut players,
5837 );
5838 }
5839 collect_implicit_stmts(
5840 &rule.actions,
5841 &declared_globals,
5842 &declared_players,
5843 &mut globals,
5844 &mut players,
5845 );
5846 }
5847 RuleEntry::SubroutineDef { body, .. } => collect_implicit_stmts(
5848 body,
5849 &declared_globals,
5850 &declared_players,
5851 &mut globals,
5852 &mut players,
5853 ),
5854 }
5855 }
5856 (globals, players)
5857}
5858
5859fn collect_implicit_stmts(
5860 statements: &[Stmt],
5861 declared_globals: &HashSet<&str>,
5862 declared_players: &HashSet<&str>,
5863 globals: &mut BTreeMap<String, Option<HirSpan>>,
5864 players: &mut BTreeMap<String, Option<HirSpan>>,
5865) {
5866 for statement in statements {
5867 match statement {
5868 Stmt::Expr { expr, .. } => {
5869 collect_implicit_expr(expr, declared_globals, declared_players, globals, players)
5870 }
5871 Stmt::Assign { target, value, .. } => {
5872 collect_implicit_expr(target, declared_globals, declared_players, globals, players);
5873 collect_implicit_expr(value, declared_globals, declared_players, globals, players);
5874 }
5875 Stmt::Delete { target, .. } => {
5876 collect_implicit_expr(target, declared_globals, declared_players, globals, players);
5877 }
5878 Stmt::If {
5879 branches, r#else, ..
5880 } => {
5881 for branch in branches {
5882 collect_implicit_expr(
5883 &branch.condition,
5884 declared_globals,
5885 declared_players,
5886 globals,
5887 players,
5888 );
5889 collect_implicit_stmts(
5890 &branch.body,
5891 declared_globals,
5892 declared_players,
5893 globals,
5894 players,
5895 );
5896 }
5897 if let Some(default_body) = r#else {
5898 collect_implicit_stmts(
5899 default_body,
5900 declared_globals,
5901 declared_players,
5902 globals,
5903 players,
5904 );
5905 }
5906 }
5907 Stmt::For {
5908 variable,
5909 iterable,
5910 body,
5911 ..
5912 } => {
5913 collect_implicit_expr(
5914 variable,
5915 declared_globals,
5916 declared_players,
5917 globals,
5918 players,
5919 );
5920 collect_implicit_expr(
5921 iterable,
5922 declared_globals,
5923 declared_players,
5924 globals,
5925 players,
5926 );
5927 collect_implicit_stmts(body, declared_globals, declared_players, globals, players);
5928 }
5929 Stmt::While {
5930 condition, body, ..
5931 }
5932 | Stmt::DoWhile {
5933 condition, body, ..
5934 } => {
5935 collect_implicit_expr(
5936 condition,
5937 declared_globals,
5938 declared_players,
5939 globals,
5940 players,
5941 );
5942 collect_implicit_stmts(body, declared_globals, declared_players, globals, players);
5943 }
5944 Stmt::Switch { value, arms, .. } => {
5945 collect_implicit_expr(value, declared_globals, declared_players, globals, players);
5946 for arm in arms {
5947 match arm {
5948 SwitchArm::Case { value, body, .. } => {
5949 collect_implicit_expr(
5950 value,
5951 declared_globals,
5952 declared_players,
5953 globals,
5954 players,
5955 );
5956 collect_implicit_stmts(
5957 body,
5958 declared_globals,
5959 declared_players,
5960 globals,
5961 players,
5962 );
5963 }
5964 SwitchArm::Default { body, .. } => {
5965 collect_implicit_stmts(
5966 body,
5967 declared_globals,
5968 declared_players,
5969 globals,
5970 players,
5971 );
5972 }
5973 }
5974 }
5975 }
5976 Stmt::Goto { offset, .. } => {
5977 if let Some(offset) = offset {
5978 collect_implicit_expr(
5979 offset,
5980 declared_globals,
5981 declared_players,
5982 globals,
5983 players,
5984 );
5985 }
5986 }
5987 Stmt::Break { .. }
5988 | Stmt::Return { .. }
5989 | Stmt::Continue { .. }
5990 | Stmt::Label { .. }
5991 | Stmt::CallSubroutine { .. }
5992 | Stmt::Pass { .. } => {}
5993 }
5994 }
5995}
5996
5997fn collect_implicit_expr(
5998 expr: &Expr,
5999 declared_globals: &HashSet<&str>,
6000 declared_players: &HashSet<&str>,
6001 globals: &mut BTreeMap<String, Option<HirSpan>>,
6002 players: &mut BTreeMap<String, Option<HirSpan>>,
6003) {
6004 match expr {
6005 Expr::GlobalVar { name, span } => {
6006 if !declared_globals.contains(name.as_str()) && default_var_index(name).is_some() {
6007 globals.entry(name.clone()).or_insert(*span);
6008 }
6009 }
6010 Expr::Array { elements, .. } => {
6011 for element in elements {
6012 collect_implicit_expr(
6013 element,
6014 declared_globals,
6015 declared_players,
6016 globals,
6017 players,
6018 );
6019 }
6020 }
6021 Expr::Dict { entries, .. } => {
6022 for entry in entries {
6023 collect_implicit_expr(
6024 &entry.key,
6025 declared_globals,
6026 declared_players,
6027 globals,
6028 players,
6029 );
6030 collect_implicit_expr(
6031 &entry.value,
6032 declared_globals,
6033 declared_players,
6034 globals,
6035 players,
6036 );
6037 }
6038 }
6039 Expr::Comprehension {
6040 element,
6041 iterable,
6042 condition,
6043 ..
6044 } => {
6045 collect_implicit_expr(
6046 element,
6047 declared_globals,
6048 declared_players,
6049 globals,
6050 players,
6051 );
6052 collect_implicit_expr(
6053 iterable,
6054 declared_globals,
6055 declared_players,
6056 globals,
6057 players,
6058 );
6059 if let Some(condition) = condition {
6060 collect_implicit_expr(
6061 condition,
6062 declared_globals,
6063 declared_players,
6064 globals,
6065 players,
6066 );
6067 }
6068 }
6069 Expr::Lambda { body, .. } => {
6070 collect_implicit_expr(body, declared_globals, declared_players, globals, players)
6071 }
6072 Expr::Type { args, .. } => {
6073 for arg in args {
6074 collect_implicit_expr(arg, declared_globals, declared_players, globals, players);
6075 }
6076 }
6077 Expr::Vector { x, y, z, .. } => {
6078 collect_implicit_expr(x, declared_globals, declared_players, globals, players);
6079 collect_implicit_expr(y, declared_globals, declared_players, globals, players);
6080 collect_implicit_expr(z, declared_globals, declared_players, globals, players);
6081 }
6082 Expr::PlayerVar {
6083 player,
6084 name,
6085 member_span,
6086 span,
6087 } => {
6088 if !declared_players.contains(name.as_str()) && default_var_index(name).is_some() {
6089 players.entry(name.clone()).or_insert(member_span.or(*span));
6090 }
6091 collect_implicit_expr(player, declared_globals, declared_players, globals, players);
6092 }
6093 Expr::Member {
6094 receiver,
6095 member,
6096 span,
6097 ..
6098 } => {
6099 if !declared_players.contains(member.as_str()) && default_var_index(member).is_some() {
6100 players.entry(member.clone()).or_insert(*span);
6101 }
6102 collect_implicit_expr(
6103 receiver,
6104 declared_globals,
6105 declared_players,
6106 globals,
6107 players,
6108 );
6109 }
6110 Expr::Call { args, .. } | Expr::MacroCall { args, .. } => {
6111 for arg in args {
6112 collect_implicit_expr(arg, declared_globals, declared_players, globals, players);
6113 }
6114 }
6115 Expr::ReceiverCall { receiver, args, .. } => {
6116 collect_implicit_expr(
6117 receiver,
6118 declared_globals,
6119 declared_players,
6120 globals,
6121 players,
6122 );
6123 for arg in args {
6124 collect_implicit_expr(arg, declared_globals, declared_players, globals, players);
6125 }
6126 }
6127 Expr::Binary { left, right, .. } => {
6128 collect_implicit_expr(left, declared_globals, declared_players, globals, players);
6129 collect_implicit_expr(right, declared_globals, declared_players, globals, players);
6130 }
6131 Expr::Conditional {
6132 then_value,
6133 condition,
6134 else_value,
6135 ..
6136 } => {
6137 collect_implicit_expr(
6138 then_value,
6139 declared_globals,
6140 declared_players,
6141 globals,
6142 players,
6143 );
6144 collect_implicit_expr(
6145 condition,
6146 declared_globals,
6147 declared_players,
6148 globals,
6149 players,
6150 );
6151 collect_implicit_expr(
6152 else_value,
6153 declared_globals,
6154 declared_players,
6155 globals,
6156 players,
6157 );
6158 }
6159 Expr::Unary { operand, .. } => collect_implicit_expr(
6160 operand,
6161 declared_globals,
6162 declared_players,
6163 globals,
6164 players,
6165 ),
6166 Expr::Index { array, index, .. } => {
6167 collect_implicit_expr(array, declared_globals, declared_players, globals, players);
6168 collect_implicit_expr(index, declared_globals, declared_players, globals, players);
6169 }
6170 Expr::Format { args, .. } => {
6171 for arg in args {
6172 collect_implicit_expr(arg, declared_globals, declared_players, globals, players);
6173 }
6174 }
6175 Expr::Number { .. }
6176 | Expr::String { .. }
6177 | Expr::Bool { .. }
6178 | Expr::Null { .. }
6179 | Expr::StringModifier { .. }
6180 | Expr::Local { .. }
6181 | Expr::Enum { .. }
6182 | Expr::EventPlayer { .. }
6183 | Expr::HostPlayer { .. }
6184 | Expr::Constant { .. }
6185 | Expr::MacroParam { .. } => {}
6186 }
6187}
6188
6189fn allocate_indices(
6190 entries: &[(Option<u32>, Option<HirSpan>)],
6191 pre_reserved: &HashSet<u32>,
6192 kind: &str,
6193) -> Result<Vec<u32>, IntegrationError> {
6194 let mut reserved = pre_reserved.clone();
6195 for (index, span) in entries {
6196 let Some(index) = index else {
6197 continue;
6198 };
6199 if !reserved.insert(*index) {
6200 return Err(IntegrationError::new(
6201 "index-collision",
6202 format!("duplicate explicit {kind} index {index}"),
6203 *span,
6204 ));
6205 }
6206 }
6207
6208 let mut next = 0;
6213 let mut allocated = Vec::with_capacity(entries.len());
6214 for (index, span) in entries {
6215 let assigned = if let Some(index) = index {
6216 *index
6217 } else {
6218 while reserved.contains(&next) {
6219 next = next.checked_add(1).ok_or_else(|| {
6220 IntegrationError::new(
6221 "index-exhausted",
6222 format!("no available {kind} index remains"),
6223 *span,
6224 )
6225 })?;
6226 }
6227 reserved.insert(next);
6228 let assigned = next;
6229 next = next.checked_add(1).ok_or_else(|| {
6230 IntegrationError::new(
6231 "index-exhausted",
6232 format!("no available {kind} index remains"),
6233 *span,
6234 )
6235 })?;
6236 assigned
6237 };
6238 allocated.push(assigned);
6239 }
6240 Ok(allocated)
6241}
6242
6243fn player_event_kind(name: &str) -> Option<PlayerEventKind> {
6244 Some(match name {
6245 "playerDealtDamage" => PlayerEventKind::DealtDamage,
6246 "playerDealtFinalBlow" => PlayerEventKind::DealtFinalBlow,
6247 "playerDealtHealing" => PlayerEventKind::DealtHealing,
6248 "playerDealtKnockback" => PlayerEventKind::DealtKnockback,
6249 "playerDied" => PlayerEventKind::Died,
6250 "playerEarnedElimination" => PlayerEventKind::EarnedElimination,
6251 "playerJoined" => PlayerEventKind::Joined,
6252 "playerLeft" => PlayerEventKind::Left,
6253 "playerReceivedHealing" => PlayerEventKind::ReceivedHealing,
6254 "playerReceivedKnockback" => PlayerEventKind::ReceivedKnockback,
6255 "playerTookDamage" => PlayerEventKind::TookDamage,
6256 _ => return None,
6257 })
6258}
6259
6260fn is_zero_initializer(expr: &hir::Expr) -> bool {
6261 match expr {
6262 hir::Expr::Number { text, value, .. } => text == "0" && *value == 0.0,
6263 hir::Expr::Null { .. } => true,
6264 _ => false,
6265 }
6266}
6267
6268fn literal_key_matches(left: &hir::Expr, right: &hir::Expr) -> bool {
6269 match (left, right) {
6270 (hir::Expr::Number { value: left, .. }, hir::Expr::Number { value: right, .. }) => {
6271 left == right
6272 }
6273 (hir::Expr::String { value: left, .. }, hir::Expr::String { value: right, .. }) => {
6274 left == right
6275 }
6276 (hir::Expr::Bool { value: left, .. }, hir::Expr::Bool { value: right, .. }) => {
6277 left == right
6278 }
6279 (hir::Expr::Null { .. }, hir::Expr::Null { .. }) => true,
6280 _ => false,
6281 }
6282}
6283
6284fn indexed_target_parts<'a>(
6285 target: &'a hir::Expr,
6286 indices: &mut Vec<&'a hir::Expr>,
6287) -> Option<&'a hir::Expr> {
6288 match target {
6289 hir::Expr::Index { array, index, .. } => {
6290 indices.push(index);
6291 indexed_target_parts(array, indices)
6292 }
6293 hir::Expr::GlobalVar { .. } | hir::Expr::PlayerVar { .. } => Some(target),
6294 _ => None,
6295 }
6296}
6297
6298fn is_literal_key(expr: &hir::Expr) -> bool {
6299 matches!(
6300 expr,
6301 hir::Expr::Number { .. }
6302 | hir::Expr::String { .. }
6303 | hir::Expr::Bool { .. }
6304 | hir::Expr::Null { .. }
6305 )
6306}
6307
6308fn translation_locale(language: &str) -> Option<&'static str> {
6309 Some(match language {
6310 "de" => "de-DE",
6311 "en" => "en-US",
6312 "es" | "es_mx" => "es-MX",
6313 "es_es" => "es-ES",
6314 "fr" => "fr-FR",
6315 "it" => "it-IT",
6316 "ja" => "ja-JP",
6317 "ko" => "ko-KR",
6318 "pl" => "pl-PL",
6319 "pt" => "pt-BR",
6320 "ru" => "ru-RU",
6321 "th" => "th-TH",
6322 "tr" => "tr-TR",
6323 "zh" | "zh_cn" => "zh-CN",
6324 "zh_tw" => "zh-TW",
6325 _ => return None,
6326 })
6327}
6328
6329fn big_letters(value: &str) -> String {
6330 let mut output = String::with_capacity(value.len());
6331 let mut converted = false;
6332 for character in value.chars() {
6333 if !converted {
6334 if let Some(mapped) = big_letter(character) {
6335 output.push(mapped);
6336 converted = true;
6337 continue;
6338 }
6339 }
6340 output.push(character);
6341 }
6342 output
6343}
6344
6345fn big_letter(character: char) -> Option<char> {
6346 Some(match character {
6347 'a' | 'A' => 'Α',
6348 'b' | 'B' => 'Β',
6349 'e' | 'E' => 'Ε',
6350 'h' | 'H' => 'Η',
6351 'i' | 'I' => 'Ι',
6352 'k' | 'K' => 'Κ',
6353 'm' | 'M' => 'Μ',
6354 'n' | 'N' => 'Ν',
6355 'o' | 'O' => 'Ο',
6356 'p' | 'P' => 'Ρ',
6357 't' | 'T' => 'Τ',
6358 'x' | 'X' => 'Χ',
6359 'y' | 'Y' => 'Υ',
6360 'z' | 'Z' => 'Ζ',
6361 '.' => '\u{2024}',
6362 ' ' => '\u{2028}',
6363 _ => return None,
6364 })
6365}
6366
6367fn fullwidth(value: &str) -> String {
6368 value
6369 .chars()
6370 .map(|character| match character {
6371 ' ' => '\u{2001}',
6372 '\u{00a5}' => '\u{ffe5}',
6373 '\u{20a9}' => '\u{ffe6}',
6374 '\u{00a2}' => '\u{ffe0}',
6375 '\u{00a3}' => '\u{ffe1}',
6376 '\u{00af}' => '\u{ffe3}',
6377 '\u{00ac}' => '\u{ffe2}',
6378 '\u{00a6}' => '\u{ffe4}',
6379 character if ('!'..='~').contains(&character) => {
6380 char::from_u32(character as u32 + 65248).unwrap_or(character)
6381 }
6382 _ => character,
6383 })
6384 .collect()
6385}
6386
6387fn case_sensitive(value: &str) -> String {
6388 let mut output = value.replace('æ', "\u{04d5}").replace("nj", "\u{01cc}");
6389 output = output.replace(" a ", " a ");
6390 output
6391 .chars()
6392 .map(|character| match character {
6393 'a' => 'ạ',
6394 'b' => 'ḅ',
6395 'c' => 'ƈ',
6396 'd' => 'ḍ',
6397 'e' => 'ẹ',
6398 'f' => 'ƒ',
6399 'g' => 'ǥ',
6400 'h' => '\u{04bb}',
6401 'i' => 'і',
6402 'j' => 'ј',
6403 'k' => 'ḳ',
6404 'l' => 'I',
6405 'm' => 'ṃ',
6406 'n' => 'ṇ',
6407 'o' => 'ο',
6408 'p' => 'ṗ',
6409 'q' => 'ǫ',
6410 'r' => 'ṛ',
6411 's' => 'ѕ',
6412 't' => 'ṭ',
6413 'u' => 'υ',
6414 'v' => 'ν',
6415 'w' => 'ẉ',
6416 'x' => '\u{04b3}',
6417 'y' => 'ỵ',
6418 'z' => 'ẓ',
6419 _ => character,
6420 })
6421 .collect()
6422}
6423
6424fn canonical_number_text(value: f64, text: &str) -> String {
6425 if text.starts_with("0x") || text.starts_with("0X") {
6426 value.to_string()
6427 } else {
6428 text.to_string()
6429 }
6430}
6431
6432fn computed_number_text(value: f64) -> String {
6433 if value.fract() == 0.0 {
6434 format!("{value:.0}")
6435 } else {
6436 value.to_string()
6437 }
6438}
6439
6440fn canonical_format_text(text: &str) -> String {
6441 let mut output = String::with_capacity(text.len());
6442 let mut chars = text.chars().peekable();
6443 let mut index = 0;
6444 while let Some(character) = chars.next() {
6445 if character == '{' && chars.peek() == Some(&'}') {
6446 chars.next();
6447 output.push('{');
6448 output.push_str(&index.to_string());
6449 output.push('}');
6450 index += 1;
6451 } else {
6452 output.push(character);
6453 }
6454 }
6455 output
6456}
6457
6458fn split_format_chunks(text: &str, arg_count: usize) -> Option<Vec<(String, Vec<usize>)>> {
6459 let mut chunks = Vec::new();
6460 let mut current = String::new();
6461 let mut indices = Vec::new();
6462 let mut pending = String::new();
6463 let mut cursor = 0;
6464 while cursor < text.len() {
6465 let Some(open_rel) = text[cursor..].find('{') else {
6466 pending.push_str(&text[cursor..]);
6467 break;
6468 };
6469 let open = cursor + open_rel;
6470 let Some(close_rel) = text[open + 1..].find('}') else {
6471 pending.push_str(&text[cursor..]);
6472 break;
6473 };
6474 let close = open + 1 + close_rel;
6475 let marker = &text[open + 1..close];
6476 let Ok(index) = marker.parse::<usize>() else {
6477 pending.push_str(&text[cursor..=close]);
6478 cursor = close + 1;
6479 continue;
6480 };
6481 if index >= arg_count {
6482 return None;
6483 }
6484 pending.push_str(&text[cursor..open]);
6485 if indices.len() == 3 && !indices.contains(&index) {
6486 chunks.push((current, indices));
6487 current = String::new();
6488 indices = Vec::new();
6489 }
6490 current.push_str(&pending);
6491 pending.clear();
6492 let local = if let Some(local) = indices.iter().position(|candidate| *candidate == index) {
6493 local
6494 } else {
6495 indices.push(index);
6496 indices.len() - 1
6497 };
6498 current.push('{');
6499 current.push_str(&local.to_string());
6500 current.push('}');
6501 cursor = close + 1;
6502 }
6503 current.push_str(&pending);
6504 if current.is_empty() && chunks.is_empty() {
6505 return Some(vec![(text.to_string(), Vec::new())]);
6506 }
6507 chunks.push((current, indices));
6508 Some(chunks)
6509}
6510
6511fn fold_literal_format(text: &str, args: &[hir::Expr]) -> Option<String> {
6512 let values = args
6513 .iter()
6514 .map(|arg| match arg {
6515 hir::Expr::Number { text, value, .. } => Some(canonical_number_text(*value, text)),
6516 hir::Expr::String { value, .. } => Some(value.clone()),
6517 hir::Expr::Bool { value, .. } => Some(value.to_string()),
6518 hir::Expr::Null { .. } => Some("null".to_string()),
6519 _ => None,
6520 })
6521 .collect::<Option<Vec<_>>>()?;
6522 let mut output = canonical_format_text(text);
6523 for (index, value) in values.iter().enumerate() {
6524 output = output.replace(&format!("{{{index}}}"), value);
6525 }
6526 Some(output)
6527}
6528
6529fn debug_expr_text(expr: &Expr) -> String {
6530 match expr {
6531 Expr::Number { text, .. } => text.clone(),
6532 Expr::String { value, .. } => {
6533 format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
6534 }
6535 Expr::Bool { value, .. } => value.to_string(),
6536 Expr::Null { .. } => "null".to_string(),
6537 Expr::Array { elements, .. } => format!(
6538 "[{}]",
6539 elements
6540 .iter()
6541 .map(debug_expr_text)
6542 .collect::<Vec<_>>()
6543 .join(", ")
6544 ),
6545 Expr::Dict { entries, .. } => format!(
6546 "{{{}}}",
6547 entries
6548 .iter()
6549 .map(|entry| format!(
6550 "{}: {}",
6551 debug_expr_text(&entry.key),
6552 debug_expr_text(&entry.value)
6553 ))
6554 .collect::<Vec<_>>()
6555 .join(", ")
6556 ),
6557 Expr::Comprehension {
6558 element,
6559 variable,
6560 iterable,
6561 condition,
6562 ..
6563 } => {
6564 let condition = condition
6565 .as_deref()
6566 .map(|condition| format!(" if {}", debug_expr_text(condition)))
6567 .unwrap_or_default();
6568 format!(
6569 "[{} for {} in {}{}]",
6570 debug_expr_text(element),
6571 variable,
6572 debug_expr_text(iterable),
6573 condition
6574 )
6575 }
6576 Expr::Lambda { params, body, .. } => {
6577 format!("lambda {}: {}", params.join(", "), debug_expr_text(body))
6578 }
6579 Expr::StringModifier {
6580 modifier, value, ..
6581 } => format!("{}\"{}\"", modifier, value),
6582 Expr::Local { name, .. }
6583 | Expr::GlobalVar { name, .. }
6584 | Expr::Constant { name, .. }
6585 | Expr::MacroParam { name, .. } => name.clone(),
6586 Expr::Type { name, args, .. } => {
6587 if args.is_empty() {
6588 name.clone()
6589 } else {
6590 format!(
6591 "{}[{}]",
6592 name,
6593 args.iter()
6594 .map(debug_expr_text)
6595 .collect::<Vec<_>>()
6596 .join(": ")
6597 )
6598 }
6599 }
6600 Expr::Vector { x, y, z, .. } => format!(
6601 "vect({}, {}, {})",
6602 debug_expr_text(x),
6603 debug_expr_text(y),
6604 debug_expr_text(z)
6605 ),
6606 Expr::Enum {
6607 value_type, value, ..
6608 } => format!("{}.{}", value_type, value),
6609 Expr::PlayerVar { player, name, .. } => {
6610 format!("{}.{}", debug_expr_text(player), name)
6611 }
6612 Expr::Member {
6613 receiver, member, ..
6614 } => format!("{}.{}", debug_expr_text(receiver), member),
6615 Expr::EventPlayer { .. } => "eventPlayer".to_string(),
6616 Expr::HostPlayer { .. } => "hostPlayer".to_string(),
6617 Expr::Call { name, args, .. } if name == "sorted" && args.len() == 2 => {
6618 format!(
6619 "sorted({}, key = {})",
6620 debug_expr_text(&args[0]),
6621 debug_expr_text(&args[1])
6622 )
6623 }
6624 Expr::Call { name, args, .. } | Expr::MacroCall { name, args, .. } => format!(
6625 "{}({})",
6626 name,
6627 args.iter()
6628 .map(debug_expr_text)
6629 .collect::<Vec<_>>()
6630 .join(", ")
6631 ),
6632 Expr::ReceiverCall {
6633 receiver,
6634 name,
6635 args,
6636 ..
6637 } => format!(
6638 "{}.{}({})",
6639 debug_expr_text(receiver),
6640 name,
6641 args.iter()
6642 .map(debug_expr_text)
6643 .collect::<Vec<_>>()
6644 .join(", ")
6645 ),
6646 Expr::Binary {
6647 left, op, right, ..
6648 } => format!(
6649 "{} {} {}",
6650 debug_expr_text(left),
6651 op,
6652 debug_expr_text(right)
6653 ),
6654 Expr::Conditional {
6655 then_value,
6656 condition,
6657 else_value,
6658 ..
6659 } => format!(
6660 "{} if {} else {}",
6661 debug_expr_text(then_value),
6662 debug_expr_text(condition),
6663 debug_expr_text(else_value)
6664 ),
6665 Expr::Unary { op, operand, .. } => format!("{} {}", op, debug_expr_text(operand)),
6666 Expr::Index { array, index, .. } => {
6667 format!("{}[{}]", debug_expr_text(array), debug_expr_text(index))
6668 }
6669 Expr::Format { text, args, .. } => format!(
6670 "\"{}\".format({})",
6671 text,
6672 args.iter()
6673 .map(debug_expr_text)
6674 .collect::<Vec<_>>()
6675 .join(", ")
6676 ),
6677 }
6678}
6679
6680fn canonical_debug_text(text: &str) -> String {
6681 text.chars()
6682 .map(|character| match character {
6683 'a' => 'ạ',
6684 'b' => 'ḅ',
6685 'c' => 'ƈ',
6686 'd' => 'ḍ',
6687 'e' => 'ẹ',
6688 'f' => 'ƒ',
6689 'g' => 'ǥ',
6690 'h' => 'һ',
6691 'i' => 'і',
6692 'j' => 'ј',
6693 'k' => 'ḳ',
6694 'l' => 'I',
6695 'm' => 'ṃ',
6696 'n' => 'ṇ',
6697 'o' => 'ο',
6698 'p' => 'ṗ',
6699 'q' => 'ǫ',
6700 'r' => 'ṛ',
6701 's' => 'ѕ',
6702 't' => 'ṭ',
6703 'u' => 'υ',
6704 'v' => 'ν',
6705 'w' => 'ẉ',
6706 'x' => 'ҳ',
6707 'y' => 'ỵ',
6708 'z' => 'ẓ',
6709 _ => character,
6710 })
6711 .collect()
6712}
6713
6714fn negated_comparison(op: &str) -> Option<&'static str> {
6715 Some(match op {
6716 "==" => "!=",
6717 "!=" => "==",
6718 "<" => ">=",
6719 ">" => "<=",
6720 "<=" => ">",
6721 ">=" => "<",
6722 _ => return None,
6723 })
6724}
6725
6726fn modify_op_from_str(op: &str) -> Option<wir::ModifyOp> {
6727 match op {
6728 "+" => Some(wir::ModifyOp::Add),
6729 "-" => Some(wir::ModifyOp::Subtract),
6730 "*" => Some(wir::ModifyOp::Multiply),
6731 "/" => Some(wir::ModifyOp::Divide),
6732 "%" => Some(wir::ModifyOp::Modulo),
6733 "**" => Some(wir::ModifyOp::RaiseToPower),
6734 _ => None,
6735 }
6736}
6737
6738fn modify_catalog_name_from_str(op: &str) -> Option<&'static str> {
6739 match op {
6740 "+" => Some("add"),
6741 "-" => Some("subtract"),
6742 "*" => Some("multiply"),
6743 "/" => Some("divide"),
6744 "%" => Some("modulo"),
6745 "**" => Some("raiseToPower"),
6746 _ => None,
6747 }
6748}
6749
6750fn hir_span_from_diag(span: crate::diag::Span) -> HirSpan {
6751 HirSpan {
6752 file: span.file,
6753 start: hir::Position {
6754 line: span.start.line,
6755 col: span.start.col,
6756 },
6757 end: hir::Position {
6758 line: span.end.line,
6759 col: span.end.col,
6760 },
6761 }
6762}
6763
6764fn workshop_error_span(error: &workshop_rs::WorkshopError) -> Option<WorkshopSpan> {
6765 match error {
6766 workshop_rs::WorkshopError::Unknown { span, .. }
6767 | workshop_rs::WorkshopError::Malformed { span, .. }
6768 | workshop_rs::WorkshopError::Unsupported { span, .. } => *span,
6769 workshop_rs::WorkshopError::Catalog(_)
6770 | workshop_rs::WorkshopError::MissingMapping { .. } => None,
6771 }
6772}
6773
6774#[cfg(test)]
6775mod tests {
6776 use super::{
6777 COMPILE_SCHEMA_VERSION, CompileFailureClass, CompileStatus, Compiler, WORKSHOP_RS_VERSION,
6778 cross_check_manifest,
6779 };
6780 use crate::manifest::Manifest;
6781 use std::path::Path;
6782 use workshop_rs::catalog::{Catalog, Locale};
6783
6784 #[test]
6785 fn public_contract_is_pinned_and_manifest_links_are_checked() {
6786 let compiler = Compiler::new().expect("released workshop contract must load");
6787 let identity = compiler.catalog_identity();
6788 assert_eq!(identity.implementation_version, WORKSHOP_RS_VERSION);
6789 assert!(compiler.link_report().catalog_ids_checked > 0);
6790 assert!(compiler.link_report().domains_checked > 0);
6791 }
6792
6793 #[test]
6794 fn compile_report_is_versioned_and_contains_reproducibility_identity() {
6795 let compiler = Compiler::new().unwrap();
6796 let report = compiler.compile_source_report_with_locale(
6797 "rule \"report\":\n @Event global\n disableInspector()\n",
6798 "report.opy",
6799 Path::new("."),
6800 &Locale::new("en-US"),
6801 );
6802 assert_eq!(report.schema_version, COMPILE_SCHEMA_VERSION);
6803 assert_eq!(report.compiler.name, "opy-rs");
6804 assert_eq!(report.catalog.implementation_version, WORKSHOP_RS_VERSION);
6805 assert_eq!(report.compile.status, CompileStatus::Success);
6806 assert_eq!(report.compile.exit_code, 0);
6807 assert!(report.compile.diagnostics.is_empty());
6808 assert_eq!(
6809 report.compile.workshop,
6810 report
6811 .compile
6812 .workshop_exact
6813 .trim_end_matches('\n')
6814 .to_owned()
6815 + "\n"
6816 );
6817 assert!(serde_json::to_value(report).unwrap()["catalog"]["catalog-version"].is_string());
6818 }
6819
6820 #[test]
6821 fn compile_report_preserves_frontend_failure_class_and_source_path() {
6822 let compiler = Compiler::new().unwrap();
6823 let report = compiler.compile_source_report_with_locale(
6824 "rule \"broken\":\n @Event global\n missing()\n",
6825 "broken.opy",
6826 Path::new("."),
6827 &Locale::new("en-US"),
6828 );
6829 assert_eq!(report.compile.status, CompileStatus::Failure);
6830 assert_eq!(
6831 report.compile.failure_class,
6832 Some(CompileFailureClass::Frontend)
6833 );
6834 assert_eq!(report.compile.exit_code, 1);
6835 let diagnostic = &report.compile.diagnostics[0];
6836 assert_eq!(diagnostic.code, "unknown-action");
6837 assert_eq!(diagnostic.span.as_ref().unwrap().path, "broken.opy");
6838 }
6839
6840 #[test]
6841 fn compile_report_preserves_integration_failure_class_and_source_path() {
6842 let compiler = Compiler::new().unwrap();
6843 let report = compiler.compile_source_report_with_locale(
6844 "rule \"broken\":\n @Event global\n {\"a\": 1}[\"b\"] = 3\n",
6845 "broken.opy",
6846 Path::new("."),
6847 &Locale::new("en-US"),
6848 );
6849 assert_eq!(report.compile.status, CompileStatus::Failure);
6850 assert_eq!(
6851 report.compile.failure_class,
6852 Some(CompileFailureClass::Integration)
6853 );
6854 assert_eq!(
6855 report.compile.diagnostics[0].span.as_ref().unwrap().path,
6856 "broken.opy"
6857 );
6858 }
6859
6860 #[test]
6861 fn compile_report_preserves_frontend_warnings_on_integration_failure() {
6862 let compiler = Compiler::new().unwrap();
6863 let root = Path::new(env!("CARGO_MANIFEST_DIR"))
6864 .join("../../compatibility/fixtures/synthetic/preprocessing");
6865 let report = compiler.compile_source_report_with_locale(
6866 concat!(
6867 "#!include \"shared.opy\"\n",
6868 "#!include \"shared.opy\"\n",
6869 "rule \"broken\":\n",
6870 " @Event global\n",
6871 " {\"a\": 1}[\"b\"] = 3\n",
6872 ),
6873 "broken.opy",
6874 &root,
6875 &Locale::new("en-US"),
6876 );
6877 assert_eq!(report.compile.status, CompileStatus::Failure);
6878 assert_eq!(
6879 report.compile.failure_class,
6880 Some(CompileFailureClass::Integration)
6881 );
6882 assert_eq!(report.compile.diagnostics.len(), 2);
6883 assert_eq!(
6884 report.compile.diagnostics[0].severity,
6885 crate::tooling::DiagnosticSeverity::Warning
6886 );
6887 assert_eq!(report.compile.diagnostics[0].code, "w_already_imported");
6888 assert_eq!(
6889 report.compile.diagnostics[1].severity,
6890 crate::tooling::DiagnosticSeverity::Error
6891 );
6892 assert_eq!(
6893 report.compile.diagnostics[1].span.as_ref().unwrap().path,
6894 "broken.opy"
6895 );
6896 }
6897
6898 #[test]
6899 fn vertical_slice_preserves_source_files_spans_and_emits_workshop() {
6900 let compiler = Compiler::new().unwrap();
6901 let hir = crate::compile(
6902 "globalvar A\nrule \"issue 35 integration\":\n @Event global\n A = 1\n disableInspector()\n",
6903 "issue-35-integration.opy",
6904 Path::new("."),
6905 )
6906 .unwrap();
6907 let artifact = compiler.compile_hir(&hir).unwrap();
6908 assert_eq!(
6909 artifact
6910 .wir
6911 .files
6912 .get(workshop_rs::source::FileId::from_index(0))
6913 .unwrap()
6914 .path,
6915 "issue-35-integration.opy"
6916 );
6917 let rule = artifact
6918 .wir
6919 .rules
6920 .get(workshop_rs::wir::RuleId::from_index(0))
6921 .unwrap();
6922 assert_eq!(rule.span.unwrap().file.index(), 0);
6923 assert_eq!(rule.name_span.unwrap().start.line, 2);
6924 assert!(artifact.emitted.contains("Disable Inspector Recording;"));
6925 assert_eq!(artifact.catalog_identity.implementation_version, "0.1.18");
6926 }
6927
6928 #[test]
6929 fn stale_catalog_links_fail_explicitly() {
6930 let manifest = Manifest::builtin().unwrap().clone();
6931 let mut stale = manifest;
6932 stale.functions[0].catalog_id = Some("missing-catalog-id".to_string());
6933 let error = cross_check_manifest(&stale, &Catalog::builtin().unwrap()).unwrap_err();
6934 assert_eq!(error.diagnostic.code, "catalog-link-missing");
6935 }
6936
6937 #[test]
6938 fn while_lowering_is_source_attributed() {
6939 let compiler = Compiler::new().unwrap();
6940 let hir = crate::compile(
6941 "rule \"while\":\n @Event global\n while true:\n disableInspector()\n",
6942 "while.opy",
6943 Path::new("."),
6944 )
6945 .unwrap();
6946 let artifact = compiler.compile_hir(&hir).unwrap();
6947 let rule = artifact
6948 .wir
6949 .rules
6950 .get(workshop_rs::wir::RuleId::from_index(0))
6951 .unwrap();
6952 assert!(matches!(
6953 artifact.wir.actions.get(rule.actions[0]),
6954 Some(workshop_rs::wir::Action::While { .. })
6955 ));
6956 assert!(artifact.emitted.contains("While(True);"));
6957 }
6958
6959 #[test]
6960 fn structural_subroutines_lower_to_canonical_wir() {
6961 let compiler = Compiler::new().unwrap();
6962 let hir = crate::compile(
6963 "globalvar score\nsubroutine showStatus\ndef showStatus():\n @Name \"Friendly\"\n @SuppressWarnings unusedVariable\n disableInspector()\nrule \"caller\":\n @Event global\n showStatus()\n",
6964 "structure.opy",
6965 Path::new("."),
6966 )
6967 .unwrap();
6968 let artifact = compiler.compile_hir(&hir).unwrap();
6969 let subroutine = artifact
6970 .wir
6971 .subroutines
6972 .get(workshop_rs::wir::SubroutineId::from_index(0))
6973 .unwrap();
6974 assert_eq!(subroutine.name, "showStatus");
6975 assert_eq!(subroutine.index, 0);
6976 assert_eq!(subroutine.name_span.unwrap().start.line, 2);
6977 assert_eq!(artifact.wir.rules.len(), 2);
6978 let subroutine_rule = artifact
6979 .wir
6980 .rules
6981 .get(workshop_rs::wir::RuleId::from_index(0))
6982 .unwrap();
6983 let workshop_rs::wir::Event::Subroutine(subroutine_id) = subroutine_rule.event else {
6984 panic!("expected a subroutine event");
6985 };
6986 assert_eq!(
6987 artifact.wir.subroutines.get(subroutine_id).unwrap().name,
6988 "showStatus"
6989 );
6990 assert!(matches!(
6991 artifact
6992 .wir
6993 .actions
6994 .get(workshop_rs::wir::ActionId::from_index(1))
6995 .unwrap(),
6996 workshop_rs::wir::Action::CallSubroutine { .. }
6997 ));
6998 assert!(artifact.emitted.contains("Subroutine Friendly"));
6999 }
7000
7001 #[test]
7002 fn player_event_filters_resolve_through_canonical_catalog() {
7003 let compiler = Compiler::new().unwrap();
7004 let hir = crate::compile(
7005 "rule \"joined\":\n @Event playerJoined\n @Team 1\n @Slot 2\n disableInspector()\n",
7006 "filters.opy",
7007 Path::new("."),
7008 )
7009 .unwrap();
7010 let artifact = compiler.compile_hir(&hir).unwrap();
7011 assert!(matches!(
7012 &artifact
7013 .wir
7014 .rules
7015 .get(workshop_rs::wir::RuleId::from_index(0))
7016 .unwrap()
7017 .event,
7018 workshop_rs::wir::Event::Player {
7019 kind: workshop_rs::wir::PlayerEventKind::Joined,
7020 team: workshop_rs::wir::EventTeam::Team1,
7021 target: workshop_rs::wir::EventTarget::Slot(2),
7022 }
7023 ));
7024 assert!(artifact.emitted.contains("Player Joined Match;"));
7025 }
7026
7027 #[test]
7028 fn hero_event_filters_accept_legacy_aliases() {
7029 let compiler = Compiler::new().unwrap();
7030 let hir = crate::compile(
7031 "rule \"hero\":\n @Event eachPlayer\n @Hero soldier\n disableInspector()\n",
7032 "hero-filter.opy",
7033 Path::new("."),
7034 )
7035 .unwrap();
7036 let artifact = compiler.compile_hir(&hir).unwrap();
7037 assert!(matches!(
7038 &artifact
7039 .wir
7040 .rules
7041 .get(workshop_rs::wir::RuleId::from_index(0))
7042 .unwrap()
7043 .event,
7044 workshop_rs::wir::Event::EachPlayerWithFilters {
7045 target: workshop_rs::wir::EventTarget::Hero(hero),
7046 ..
7047 } if hero == "SOLDIER_76"
7048 ));
7049 }
7050
7051 #[test]
7052 fn explicit_indices_are_reserved_before_deterministic_allocation() {
7053 let compiler = Compiler::new().unwrap();
7054 let hir = crate::compile(
7055 "globalvar first\nglobalvar reserved 0\nglobalvar next\nrule \"indices\":\n @Event global\n disableInspector()\n",
7056 "indices.opy",
7057 Path::new("."),
7058 )
7059 .unwrap();
7060 let artifact = compiler.compile_hir(&hir).unwrap();
7061 let by_name = artifact
7062 .wir
7063 .global_variables
7064 .iter()
7065 .map(|variable| (variable.name.as_str(), variable.index))
7066 .collect::<std::collections::BTreeMap<_, _>>();
7067 assert_eq!(
7068 by_name,
7069 std::collections::BTreeMap::from([("first", 1), ("reserved", 0), ("next", 2)])
7070 );
7071 let indices = artifact
7073 .wir
7074 .global_variables
7075 .iter()
7076 .map(|variable| variable.index)
7077 .collect::<Vec<_>>();
7078 assert_eq!(indices, vec![0, 1, 2]);
7079 }
7080
7081 #[test]
7082 fn implicit_default_variables_use_reference_fixed_slots() {
7083 let compiler = Compiler::new().unwrap();
7084 let hir = crate::compile(
7085 r#"
7086globalvar timer
7087globalvar extra 5
7088
7089rule "implicit":
7090 @Event global
7091 A = timer + 1
7092 B = A
7093 B += 2
7094 A[0] = 7
7095 DX = B * A
7096"#,
7097 "implicit.opy",
7098 Path::new("."),
7099 )
7100 .unwrap();
7101 let artifact = compiler.compile_hir(&hir).unwrap();
7102 let globals = artifact
7103 .wir
7104 .global_variables
7105 .iter()
7106 .map(|variable| (variable.name.clone(), variable.index))
7107 .collect::<Vec<_>>();
7108 assert_eq!(
7113 globals,
7114 vec![
7115 ("A".to_string(), 0),
7116 ("B".to_string(), 1),
7117 ("timer".to_string(), 2),
7118 ("extra".to_string(), 5),
7119 ("DX".to_string(), 127),
7120 ]
7121 );
7122 assert!(
7123 artifact
7124 .emitted
7125 .contains("Set Global Variable(A, Add(Global.timer, 1));")
7126 );
7127 assert!(
7128 artifact
7129 .emitted
7130 .contains("Set Global Variable(B, Global.A);")
7131 );
7132 assert!(
7133 artifact
7134 .emitted
7135 .contains("Modify Global Variable(B, Add, 2);")
7136 );
7137 assert!(
7138 artifact
7139 .emitted
7140 .contains("Set Global Variable At Index(A, 0, 7);")
7141 );
7142 assert!(
7143 artifact
7144 .emitted
7145 .contains("Set Global Variable(DX, Multiply(Global.B, Global.A));")
7146 );
7147 }
7148
7149 #[test]
7150 fn implicit_default_variable_slot_collision_is_source_attributed() {
7151 let compiler = Compiler::new().unwrap();
7152 let hir = crate::compile(
7153 "globalvar x 0\nrule \"collision\":\n @Event global\n x = 1\n A = 2\n",
7154 "collision.opy",
7155 Path::new("."),
7156 )
7157 .unwrap();
7158 let error = match compiler.compile_hir(&hir) {
7159 Ok(_) => panic!("slot collision unexpectedly succeeded"),
7160 Err(error) => error,
7161 };
7162 assert_eq!(error.diagnostic.code, "index-collision");
7163 assert_eq!(error.diagnostic.span.unwrap().start.line, 5);
7164 assert!(error.diagnostic.message.contains("'A' and 'x'"));
7165 }
7166
7167 #[test]
7168 fn implicit_default_player_variables_use_independent_reference_slots() {
7169 let compiler = Compiler::new().unwrap();
7170 let hir = crate::compile(
7171 r#"
7172playervar declaredPlayer
7173
7174rule "implicit player variables":
7175 @Event eachPlayer
7176 A = 1
7177 eventPlayer.A = 1
7178 eventPlayer.A += 2
7179 eventPlayer.E = eventPlayer.A
7180 eventPlayer.DX = eventPlayer.E
7181 eventPlayer.declaredPlayer = eventPlayer.A
7182"#,
7183 "implicit-player.opy",
7184 Path::new("."),
7185 )
7186 .unwrap();
7187 let artifact = compiler.compile_hir(&hir).unwrap();
7188 let globals = artifact
7189 .wir
7190 .global_variables
7191 .iter()
7192 .map(|variable| (variable.name.as_str(), variable.index))
7193 .collect::<std::collections::BTreeMap<_, _>>();
7194 let players = artifact
7195 .wir
7196 .player_variables
7197 .iter()
7198 .map(|variable| (variable.name.as_str(), variable.index))
7199 .collect::<std::collections::BTreeMap<_, _>>();
7200 assert_eq!(globals.get("A"), Some(&0));
7201 assert_eq!(players.get("A"), Some(&0));
7202 assert_eq!(players.get("declaredPlayer"), Some(&1));
7203 assert_eq!(players.get("E"), Some(&4));
7204 assert_eq!(players.get("DX"), Some(&127));
7205 assert!(
7206 artifact
7207 .emitted
7208 .contains("Set Player Variable(Event Player, A, 1);")
7209 );
7210 assert!(
7211 artifact
7212 .emitted
7213 .contains("Modify Player Variable(Event Player, A, Add, 2);")
7214 );
7215 assert!(
7216 artifact
7217 .emitted
7218 .contains("Set Player Variable(Event Player, E, (Event Player).A);")
7219 );
7220 }
7221
7222 #[test]
7223 fn implicit_default_player_slot_collision_is_source_attributed() {
7224 let compiler = Compiler::new().unwrap();
7225 let hir = crate::compile(
7226 "playervar declared 0\nrule \"collision\":\n @Event eachPlayer\n eventPlayer.A = 1\n",
7227 "player-collision.opy",
7228 Path::new("."),
7229 )
7230 .unwrap();
7231 let error = match compiler.compile_hir(&hir) {
7232 Ok(_) => panic!("player slot collision unexpectedly succeeded"),
7233 Err(error) => error,
7234 };
7235 assert_eq!(error.diagnostic.code, "index-collision");
7236 assert!(
7237 error
7238 .diagnostic
7239 .message
7240 .contains("player variables 'A' and 'declared'")
7241 );
7242 assert_eq!(error.diagnostic.span.unwrap().start.line, 4);
7243 }
7244
7245 #[test]
7246 fn power_augmented_assignment_lowers_from_source() {
7247 let compiler = Compiler::new().unwrap();
7248 let hir = crate::compile(
7249 "globalvar g\nrule \"power\":\n @Event global\n g = 2\n g **= 3\n",
7250 "power.opy",
7251 Path::new("."),
7252 )
7253 .unwrap();
7254 let artifact = compiler.compile_hir(&hir).unwrap();
7255 assert!(artifact.emitted.contains("Set Global Variable(g, 2);"));
7256 assert!(
7257 artifact
7258 .emitted
7259 .contains("Modify Global Variable(g, Raise To Power, 3);")
7260 );
7261 }
7262
7263 #[test]
7264 fn opy_hex_numbers_are_normalized_at_the_wir_boundary() {
7265 let compiler = Compiler::new().unwrap();
7266 let hir = crate::compile(
7267 "globalvar large = 0x124BC\nglobalvar small = 0x124\nglobalvar scientific = 1e10\n",
7268 "numbers.opy",
7269 Path::new("."),
7270 )
7271 .unwrap();
7272 let artifact = compiler.compile_hir(&hir).unwrap();
7273 assert!(
7274 artifact
7275 .emitted
7276 .contains("Set Global Variable(large, 74940);")
7277 );
7278 assert!(
7279 artifact
7280 .emitted
7281 .contains("Set Global Variable(small, 292);")
7282 );
7283 assert!(
7284 artifact
7285 .emitted
7286 .contains("Set Global Variable(scientific, 1e10);")
7287 );
7288 assert!(!artifact.emitted.contains("0x124BC"));
7289 assert!(!artifact.emitted.contains("0x124"));
7290 }
7291
7292 #[test]
7293 fn literal_dict_lookup_lowers_to_the_selected_value() {
7294 let compiler = Compiler::new().unwrap();
7295 let hir = crate::compile(
7296 "globalvar total\nrule \"negative\":\n @Event global\n total = {\"a\": 1, \"b\": 2}[\"a\"]\n",
7297 "negative.opy",
7298 Path::new("."),
7299 )
7300 .unwrap();
7301 let artifact = compiler
7302 .compile_hir(&hir)
7303 .expect("literal dict lookup should lower");
7304 assert!(artifact.emitted.contains("Set Global Variable(total, 1);"));
7305 }
7306
7307 #[test]
7308 fn auto_allocation_fills_free_slots_below_early_explicit_indices() {
7309 let compiler = Compiler::new().unwrap();
7310 let hir = crate::compile(
7311 r#"
7312globalvar reserved 5
7313globalvar auto1
7314globalvar auto2
7315
7316rule "allocation":
7317 @Event global
7318 auto1 = 1
7319 auto2 = 2
7320 B = 3
7321"#,
7322 "allocation.opy",
7323 Path::new("."),
7324 )
7325 .unwrap();
7326 let artifact = compiler.compile_hir(&hir).unwrap();
7327 let by_name = artifact
7328 .wir
7329 .global_variables
7330 .iter()
7331 .map(|variable| (variable.name.clone(), variable.index))
7332 .collect::<std::collections::BTreeMap<_, _>>();
7333 assert_eq!(
7338 by_name,
7339 std::collections::BTreeMap::from([
7340 ("B".to_string(), 1),
7341 ("auto1".to_string(), 0),
7342 ("auto2".to_string(), 2),
7343 ("reserved".to_string(), 5),
7344 ])
7345 );
7346 }
7347
7348 #[test]
7349 fn power_expressions_lower_through_the_canonical_contract() {
7350 let compiler = Compiler::new().unwrap();
7351 let hir = crate::compile(
7352 "globalvar a = [2, 4]\nglobalvar out\nrule \"power\":\n @Event global\n out = a ** 2\n a **= 2\n a[0] **= 2\n",
7353 "power.opy",
7354 Path::new("."),
7355 )
7356 .unwrap();
7357 let artifact = compiler.compile_hir(&hir).unwrap();
7358 assert!(
7359 artifact
7360 .emitted
7361 .contains("Set Global Variable(out, Raise To Power(Global.a, 2));")
7362 );
7363 assert!(
7364 artifact
7365 .emitted
7366 .contains("Modify Global Variable(a, Raise To Power, 2);")
7367 );
7368 assert!(
7369 artifact
7370 .emitted
7371 .contains("Modify Global Variable At Index(a, 0, Raise To Power, 2);")
7372 );
7373 }
7374
7375 #[test]
7376 fn unsupported_rule_metadata_is_explicit_and_source_attributed() {
7377 let compiler = Compiler::new().unwrap();
7378 let hir = crate::compile(
7379 "rule \"metadata\":\n @Event global\n @NewPage \"section\"\n disableInspector()\n",
7380 "metadata.opy",
7381 Path::new("."),
7382 )
7383 .unwrap();
7384 let error = match compiler.compile_hir(&hir) {
7385 Ok(_) => panic!("unsupported metadata unexpectedly succeeded"),
7386 Err(error) => error,
7387 };
7388 assert_eq!(error.diagnostic.code, "unsupported-integration-surface");
7389 assert_eq!(error.diagnostic.span.unwrap().start.line, 3);
7390 }
7391
7392 #[test]
7393 fn issue_40_oracle_fixture_and_wir_lowering_agree() {
7394 let compiler = Compiler::new().unwrap();
7395 let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
7396 .join("../../compatibility/fixtures/synthetic/issue-40-structural");
7397 let source = std::fs::read_to_string(fixture.join("source.opy")).unwrap();
7398 let hir = crate::compile(&source, "source.opy", &fixture).unwrap();
7399 let artifact = compiler.compile_hir(&hir).unwrap();
7400 let oracle: serde_json::Value =
7401 serde_json::from_str(&std::fs::read_to_string(fixture.join("oracle.json")).unwrap())
7402 .unwrap();
7403 let oracle_workshop = oracle["compile"]["workshop"].as_str().unwrap();
7404 let oracle_wir = workshop_rs::parser::parse(
7405 oracle_workshop,
7406 &Catalog::builtin().unwrap(),
7407 &Locale::new("en-US"),
7408 )
7409 .unwrap();
7410 assert!(workshop_rs::roundtrip::equivalent(
7411 &artifact.wir,
7412 &oracle_wir
7413 ));
7414
7415 assert!(oracle_workshop.contains("0: reserved"));
7416 assert!(oracle_workshop.contains("1: first"));
7417 assert!(oracle_workshop.contains("2: explicit"));
7418 assert!(oracle_workshop.contains("3: next"));
7419 assert!(oracle_workshop.contains("0: helper"));
7420 assert!(oracle_workshop.contains("Subroutine;\n helper;"));
7421 assert!(oracle_workshop.contains("Player Joined Match;\n Team 1;\n Slot 2;"));
7422
7423 let indices = artifact
7424 .wir
7425 .global_variables
7426 .iter()
7427 .map(|variable| variable.index)
7428 .collect::<Vec<_>>();
7429 assert_eq!(indices, vec![0, 1, 2, 3]);
7430 assert_eq!(
7431 artifact.wir.subroutines.iter().next().unwrap().name,
7432 "helper"
7433 );
7434 assert!(artifact.emitted.contains("[Source] renamed helper"));
7435 assert!(matches!(
7436 artifact
7437 .wir
7438 .rules
7439 .get(workshop_rs::wir::RuleId::from_index(1))
7440 .unwrap()
7441 .event,
7442 workshop_rs::wir::Event::Player {
7443 kind: workshop_rs::wir::PlayerEventKind::Joined,
7444 team: workshop_rs::wir::EventTeam::Team1,
7445 target: workshop_rs::wir::EventTarget::Slot(2),
7446 }
7447 ));
7448 }
7449
7450 #[test]
7451 fn assignments_and_modifications_lower_to_canonical_wir() {
7452 let compiler = Compiler::new().unwrap();
7453 let hir = crate::compile(
7454 r#"
7455globalvar g1
7456globalvar g2
7457playervar p1
7458playervar p2 = [1, 2, 3]
7459
7460rule "assignments":
7461 @Event eachPlayer
7462 g1 = 10
7463 g1 += 5
7464 g1 -= 2
7465 g1 *= 3
7466 g1 /= 2
7467 g1 %= 4
7468 g2 = [1, 2, 3]
7469 g2[0] = 99
7470 g2[1] += 1
7471 eventPlayer.p1 = 42
7472 eventPlayer.p1 += 8
7473 eventPlayer.p1 *= 2
7474 eventPlayer.p2[2] = 7
7475 eventPlayer.p2[0] -= 3
7476"#,
7477 "assign.opy",
7478 Path::new("."),
7479 )
7480 .unwrap();
7481 let artifact = compiler.compile_hir(&hir).unwrap();
7482 assert!(artifact.emitted.contains("Set Global Variable(g1, 10);"));
7483 assert!(
7484 artifact
7485 .emitted
7486 .contains("Modify Global Variable(g1, Add, 5);")
7487 );
7488 assert!(
7489 artifact
7490 .emitted
7491 .contains("Modify Global Variable(g1, Subtract, 2);")
7492 );
7493 assert!(
7494 artifact
7495 .emitted
7496 .contains("Modify Global Variable(g1, Multiply, 3);")
7497 );
7498 assert!(
7499 artifact
7500 .emitted
7501 .contains("Modify Global Variable(g1, Divide, 2);")
7502 );
7503 assert!(
7504 artifact
7505 .emitted
7506 .contains("Modify Global Variable(g1, Modulo, 4);")
7507 );
7508 assert!(
7509 artifact
7510 .emitted
7511 .contains("Set Global Variable At Index(g2, 0, 99);")
7512 );
7513 assert!(
7514 artifact
7515 .emitted
7516 .contains("Modify Global Variable At Index(g2, 1, Add, 1);")
7517 );
7518 assert!(
7519 artifact
7520 .emitted
7521 .contains("Set Player Variable(Event Player, p1, 42);")
7522 );
7523 assert!(
7524 artifact
7525 .emitted
7526 .contains("Modify Player Variable(Event Player, p1, Add, 8);")
7527 );
7528 assert!(
7529 artifact
7530 .emitted
7531 .contains("Modify Player Variable(Event Player, p1, Multiply, 2);")
7532 );
7533 assert!(
7534 artifact
7535 .emitted
7536 .contains("Set Player Variable At Index((Event Player).p2, 2, 7);")
7537 );
7538 assert!(
7539 artifact
7540 .emitted
7541 .contains("Modify Player Variable At Index((Event Player).p2, 0, Subtract, 3);")
7542 );
7543
7544 let rule = artifact
7548 .wir
7549 .rules
7550 .get(workshop_rs::wir::RuleId::from_index(1))
7551 .unwrap();
7552 let direct = artifact.wir.actions.get(rule.actions[0]).unwrap();
7553 match direct {
7554 workshop_rs::wir::Action::SetGlobalVariable {
7555 span,
7556 target_span,
7557 variable,
7558 ..
7559 } => {
7560 assert_eq!(span.unwrap().start.line, 9);
7561 assert_eq!(target_span.unwrap().start.line, 9);
7562 assert_eq!(
7563 artifact.wir.global_variables.get(*variable).unwrap().name,
7564 "g1"
7565 );
7566 }
7567 other => panic!("expected a direct global assignment, got {other:?}"),
7568 }
7569 let indexed = artifact.wir.actions.get(rule.actions[7]).unwrap();
7570 match indexed {
7571 workshop_rs::wir::Action::Call { span, .. } => {
7572 assert_eq!(span.unwrap().start.line, 16);
7573 }
7574 other => panic!("expected an indexed assignment call, got {other:?}"),
7575 }
7576 }
7577
7578 #[test]
7579 fn expressions_and_values_lower_to_canonical_wir() {
7580 let compiler = Compiler::new().unwrap();
7581 let hir = crate::compile(
7582 r#"
7583enum Consts:
7584 BASE
7585
7586globalvar total
7587globalvar arr = [1, 2, 3]
7588globalvar pos = vect(1, 2, 3)
7589
7590rule "expressions":
7591 @Event global
7592 @Condition total == 0
7593 @Condition not (pos == vect(0, 0, 0))
7594 @Condition 2 in arr
7595 total = Consts.BASE + arr[1] * 2 - (10 / 2) + (5 % 2)
7596 print("Total: {}".format(total))
7597 debug(pos)
7598"#,
7599 "expr.opy",
7600 Path::new("."),
7601 )
7602 .unwrap();
7603 let artifact = compiler.compile_hir(&hir).unwrap();
7604 assert!(artifact.emitted.contains("Global.total == 0;"));
7605 assert!(artifact.emitted.contains("Global.pos != Vector(0, 0, 0);"));
7608 assert!(
7609 artifact
7610 .emitted
7611 .contains("Array Contains(Global.arr, 2) == True;")
7612 );
7613 assert!(
7614 artifact
7615 .emitted
7616 .contains("Custom String(\"Total: {0}\", Global.total)")
7617 );
7618 }
7619
7620 #[test]
7621 fn pass_is_supported_as_source_level_noop() {
7622 let compiler = Compiler::new().unwrap();
7623 let hir = crate::compile(
7624 r#"
7625subroutine emptySub
7626
7627def emptySub():
7628 pass
7629
7630rule "empty rule":
7631 @Event global
7632 pass
7633"#,
7634 "pass.opy",
7635 Path::new("."),
7636 )
7637 .unwrap();
7638 let artifact = compiler.compile_hir(&hir).unwrap();
7639 let rule0 = artifact
7640 .wir
7641 .rules
7642 .get(workshop_rs::wir::RuleId::from_index(0))
7643 .unwrap();
7644 assert!(rule0.actions.is_empty());
7645 let rule1 = artifact
7646 .wir
7647 .rules
7648 .get(workshop_rs::wir::RuleId::from_index(1))
7649 .unwrap();
7650 assert!(rule1.actions.is_empty());
7651 }
7652
7653 #[test]
7654 fn variable_initializers_synthesize_initialize_rules() {
7655 let compiler = Compiler::new().unwrap();
7656 let hir = crate::compile(
7657 r#"
7658globalvar j = 5
7659globalvar h = 0
7660globalvar k = 0.0
7661playervar p = 7
7662playervar q = 0
7663
7664rule "main":
7665 @Event global
7666 disableInspector()
7667"#,
7668 "init.opy",
7669 Path::new("."),
7670 )
7671 .unwrap();
7672 let artifact = compiler.compile_hir(&hir).unwrap();
7673 assert_eq!(
7674 artifact
7675 .wir
7676 .rules
7677 .get(workshop_rs::wir::RuleId::from_index(0))
7678 .unwrap()
7679 .name,
7680 "Initialize global variables"
7681 );
7682 assert_eq!(
7683 artifact
7684 .wir
7685 .rules
7686 .get(workshop_rs::wir::RuleId::from_index(1))
7687 .unwrap()
7688 .name,
7689 "Initialize player variables"
7690 );
7691 assert_eq!(
7692 artifact
7693 .wir
7694 .rules
7695 .get(workshop_rs::wir::RuleId::from_index(2))
7696 .unwrap()
7697 .name,
7698 "main"
7699 );
7700 assert!(artifact.emitted.contains("Set Global Variable(j, 5);"));
7701 assert!(artifact.emitted.contains("Set Global Variable(k, 0.0);"));
7702 assert!(!artifact.emitted.contains("Set Global Variable(h,"));
7703 assert!(
7704 artifact
7705 .emitted
7706 .contains("Set Player Variable(Event Player, p, 7);")
7707 );
7708 assert!(
7709 !artifact
7710 .emitted
7711 .contains("Set Player Variable(Event Player, q,")
7712 );
7713 }
7714
7715 #[test]
7716 fn settings_lower_through_workshop_owned_emission() {
7717 let compiler = Compiler::new().unwrap();
7718 let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
7719 .join("../../compatibility/fixtures/synthetic/settings");
7720 let source = std::fs::read_to_string(fixture.join("source.opy")).unwrap();
7721 let hir = crate::compile(&source, "source.opy", &fixture).unwrap();
7722 let artifact = compiler.compile_hir(&hir).unwrap();
7723 let oracle: serde_json::Value =
7724 serde_json::from_str(&std::fs::read_to_string(fixture.join("oracle.json")).unwrap())
7725 .unwrap();
7726 let expected = oracle["compile"]["workshop"]
7727 .as_str()
7728 .unwrap()
7729 .split("\n\nrule")
7730 .next()
7731 .unwrap();
7732 let actual = artifact.emitted.split("\n\nrule").next().unwrap();
7733 let oracle_wir = workshop_rs::parser::parse(
7734 oracle["compile"]["workshop"].as_str().unwrap(),
7735 &Catalog::builtin().unwrap(),
7736 &Locale::new("en-US"),
7737 )
7738 .unwrap();
7739 assert!(workshop_rs::roundtrip::equivalent(
7740 &artifact.wir,
7741 &oracle_wir
7742 ));
7743 assert_eq!(
7744 normalize_workshop_structural_whitespace(actual),
7745 normalize_workshop_structural_whitespace(expected)
7746 );
7747 }
7748
7749 #[test]
7750 fn unsupported_locale_has_no_fabricated_source_span() {
7751 let compiler = Compiler::new().unwrap();
7752 let hir = crate::compile(
7753 "#!translations en\nrule \"r\":\n @Event global\n pass\n",
7754 "locale.opy",
7755 Path::new("."),
7756 )
7757 .unwrap();
7758 let error = match compiler.compile_hir_with_locale(&hir, &Locale::new("xx-XX")) {
7759 Ok(_) => panic!("unsupported locale unexpectedly compiled"),
7760 Err(error) => error,
7761 };
7762 assert_eq!(error.diagnostic.code, "locale-unsupported");
7763 assert_eq!(error.diagnostic.span, None);
7764 }
7765
7766 #[test]
7767 fn locale_selection_emits_catalog_localized_workshop() {
7768 let compiler = Compiler::new().unwrap();
7769 let hir = crate::compile(
7770 "rule \"locale\":\n @Event global\n disableInspector()\n",
7771 "locale.opy",
7772 Path::new("."),
7773 )
7774 .unwrap();
7775 let artifact = compiler
7776 .compile_hir_with_locale(&hir, &Locale::new("zh-CN"))
7777 .unwrap();
7778 assert!(artifact.emitted.contains("规则 (\"locale\")"));
7779 assert!(artifact.emitted.contains("禁用查看器录制"));
7780 }
7781
7782 #[test]
7783 fn unsupported_backend_directives_fail_at_their_source_anchor() {
7784 let compiler = Compiler::new().unwrap();
7785 let hir = crate::compile(
7786 "#!replace0ByCapturePercentage\nrule \"r\":\n @Event global\n pass\n",
7787 "directives.opy",
7788 Path::new("."),
7789 )
7790 .unwrap();
7791 let error = match compiler.compile_hir(&hir) {
7792 Ok(_) => panic!("backend directive unexpectedly compiled"),
7793 Err(error) => error,
7794 };
7795 assert_eq!(error.diagnostic.code, "backend-directive-unsupported");
7796 assert_eq!(error.diagnostic.span.unwrap().start.line, 1);
7797 }
7798
7799 #[test]
7800 fn optimizer_directives_remain_non_blocking_presentation_controls() {
7801 let compiler = Compiler::new().unwrap();
7802 let hir = crate::compile(
7803 "#!disableOptimizations\nrule \"r\":\n @Event global\n pass\n",
7804 "optimization.opy",
7805 Path::new("."),
7806 )
7807 .unwrap();
7808 compiler.compile_hir(&hir).unwrap();
7809 }
7810
7811 #[test]
7812 fn replacement_directive_records_are_checked_even_if_final_state_is_restored() {
7813 let compiler = Compiler::new().unwrap();
7814 let mut hir = crate::compile(
7815 "#!replace0ByCapturePercentage\nrule \"r\":\n @Event global\n pass\n",
7816 "directives.opy",
7817 Path::new("."),
7818 )
7819 .unwrap();
7820 hir.preprocessing.replacements.clear();
7821 let error = match compiler.compile_hir(&hir) {
7822 Ok(_) => panic!("replacement directive unexpectedly compiled"),
7823 Err(error) => error,
7824 };
7825 assert_eq!(error.diagnostic.code, "backend-directive-unsupported");
7826 assert_eq!(error.diagnostic.span.unwrap().start.line, 1);
7827 }
7828
7829 #[test]
7830 fn active_replacement_state_is_checked_without_directive_history() {
7831 let compiler = Compiler::new().unwrap();
7832 let mut hir = crate::compile(
7833 "#!replace0ByCapturePercentage\nrule \"r\":\n @Event global\n pass\n",
7834 "directives.opy",
7835 Path::new("."),
7836 )
7837 .unwrap();
7838 hir.preprocessing.directives.clear();
7839 hir.preprocessing.replacements[0].span = None;
7840 let error = match compiler.compile_hir(&hir) {
7841 Ok(_) => panic!("active replacement state unexpectedly compiled"),
7842 Err(error) => error,
7843 };
7844 assert_eq!(error.diagnostic.code, "backend-directive-unsupported");
7845 assert_eq!(error.diagnostic.span, None);
7846 }
7847
7848 #[test]
7849 fn post_compile_hook_receives_exact_emitted_workshop() {
7850 let compiler = Compiler::new().unwrap();
7851 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/macros");
7852 let source = "#!postCompileHook \"hook.js\"\n\nrule \"setup\":\n pass\n";
7853 let artifact = compiler
7854 .compile_source_with_locale(source, "hook.opy", &root, &Locale::new("en-US"))
7855 .unwrap();
7856 assert!(artifact.emitted.contains("rule (\"setup\")"));
7857 assert!(artifact.final_output.contains("rule (\"transformed\")"));
7858 assert_ne!(artifact.final_output, artifact.emitted);
7859 }
7860
7861 #[test]
7862 fn post_compile_hook_failure_keeps_script_provenance_and_directive_anchor() {
7863 let compiler = Compiler::new().unwrap();
7864 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/macros");
7865 let source = "#!postCompileHook \"hook-boom.js\"\n\nrule \"setup\":\n pass\n";
7866 let error = match compiler.compile_source_with_locale(
7867 source,
7868 "hook.opy",
7869 &root,
7870 &Locale::new("en-US"),
7871 ) {
7872 Ok(_) => panic!("failing post-compile hook unexpectedly compiled"),
7873 Err(error) => error,
7874 };
7875 assert_eq!(error.diagnostic.code, "post-compile-hook");
7876 assert_eq!(error.diagnostic.span.unwrap().start.line, 1);
7877 let script = error.diagnostic.script.unwrap();
7878 assert_eq!(script.source_name.as_deref(), Some("hook-boom.js"));
7879 assert_eq!(script.line, Some(1));
7880 assert!(script.stack.unwrap().contains("hook-boom.js:1"));
7881 }
7882
7883 fn normalize_workshop_structural_whitespace(text: &str) -> String {
7884 let mut normalized = String::with_capacity(text.len());
7885 let mut quote = None;
7886 let mut escaped = false;
7887 for character in text.chars() {
7888 if let Some(delimiter) = quote {
7889 normalized.push(character);
7890 if escaped {
7891 escaped = false;
7892 } else if character == '\\' {
7893 escaped = true;
7894 } else if character == delimiter {
7895 quote = None;
7896 }
7897 } else if matches!(character, '\"' | '\'') {
7898 quote = Some(character);
7899 normalized.push(character);
7900 } else if !character.is_whitespace() {
7901 normalized.push(character);
7902 }
7903 }
7904 normalized
7905 }
7906
7907 #[test]
7908 fn settings_whitespace_normalization_preserves_quoted_values() {
7909 assert_ne!(
7910 normalize_workshop_structural_whitespace("Description: \"a b\""),
7911 normalize_workshop_structural_whitespace("Description: \"ab\"")
7912 );
7913 }
7914}