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