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