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 convert_settings(settings: crate::hir::Settings) -> workshop_rs::settings::Settings {
1114 workshop_rs::settings::Settings {
1115 span: settings.span.map(convert_settings_span),
1116 children: settings
1117 .children
1118 .into_iter()
1119 .map(convert_settings_node)
1120 .collect(),
1121 }
1122}
1123
1124fn convert_settings_node(node: crate::hir::SettingsNode) -> workshop_rs::settings::SettingsNode {
1125 use crate::hir::SettingsNode as SourceNode;
1126 use workshop_rs::settings::{SettingsListElement, SettingsNode as TargetNode};
1127
1128 match node {
1129 SourceNode::Group {
1130 name,
1131 children,
1132 span,
1133 } => TargetNode::Group {
1134 name,
1135 children: children.into_iter().map(convert_settings_node).collect(),
1136 span: span.map(convert_settings_span),
1137 },
1138 SourceNode::Number { name, value, span } => TargetNode::Number {
1139 name,
1140 value,
1141 span: span.map(convert_settings_span),
1142 },
1143 SourceNode::Bool { name, value, span } => TargetNode::Bool {
1144 name,
1145 value,
1146 span: span.map(convert_settings_span),
1147 },
1148 SourceNode::String { name, value, span } => TargetNode::String {
1149 name,
1150 value,
1151 span: span.map(convert_settings_span),
1152 },
1153 SourceNode::Raw { name, value, span } => TargetNode::Raw {
1154 name,
1155 value,
1156 span: span.map(convert_settings_span),
1157 },
1158 SourceNode::List {
1159 name,
1160 elements,
1161 span,
1162 } => TargetNode::List {
1163 name,
1164 elements: elements
1165 .into_iter()
1166 .map(|element| SettingsListElement {
1167 value: element.value,
1168 span: element.span.map(convert_settings_span),
1169 })
1170 .collect(),
1171 span: span.map(convert_settings_span),
1172 },
1173 }
1174}
1175
1176fn convert_settings_span(span: HirSpan) -> WorkshopSpan {
1177 WorkshopSpan::new(
1178 workshop_rs::source::FileId::from_index(span.file as usize),
1179 WorkshopPosition::new(span.start.line, span.start.col),
1180 WorkshopPosition::new(span.end.line, span.end.col),
1181 )
1182}
1183
1184struct Lowering<'a> {
1185 compiler: &'a Compiler,
1186 hir: &'a hir::Program,
1187 wir: Program,
1188 files: HashMap<u32, workshop_rs::source::FileId>,
1189 wir_to_hir_files: Vec<u32>,
1190 globals: HashMap<String, wir::GlobalVarId>,
1191 players: HashMap<String, wir::PlayerVarId>,
1192 subroutines: HashMap<String, wir::SubroutineId>,
1193 constants: HashMap<String, &'a Expr>,
1194 defined_subroutines: HashSet<wir::SubroutineId>,
1195 array_bindings: Vec<ArrayBinding>,
1196 current_rule_conditions: Option<Vec<wir::ValueId>>,
1197}
1198
1199#[derive(Debug, Clone)]
1200struct ArrayBinding {
1201 element: String,
1202 index: Option<String>,
1203}
1204
1205#[derive(Debug, Clone, Copy)]
1206enum BreakTarget {
1207 Loop,
1208 DoWhile,
1209 Switch,
1210}
1211
1212type SwitchBreak = (usize, HirSpan);
1213type LoweredSwitchBody = (Vec<wir::ActionId>, Option<SwitchBreak>);
1214
1215fn pure_continue_conditions(statement: &Stmt) -> Option<Vec<&Expr>> {
1219 match statement {
1220 Stmt::Continue { .. } => Some(Vec::new()),
1221 Stmt::If {
1222 branches,
1223 r#else: None,
1224 ..
1225 } if branches.len() == 1 && branches[0].body.len() == 1 => {
1226 let mut conditions = pure_continue_conditions(&branches[0].body[0])?;
1227 conditions.insert(0, &branches[0].condition);
1228 Some(conditions)
1229 }
1230 _ => None,
1231 }
1232}
1233
1234fn contains_loop_continue(statement: &Stmt) -> bool {
1237 match statement {
1238 Stmt::Continue { .. } => true,
1239 Stmt::If {
1240 branches, r#else, ..
1241 } => {
1242 branches
1243 .iter()
1244 .any(|branch| branch.body.iter().any(contains_loop_continue))
1245 || r#else
1246 .as_ref()
1247 .is_some_and(|body| body.iter().any(contains_loop_continue))
1248 }
1249 Stmt::For { .. } | Stmt::While { .. } | Stmt::DoWhile { .. } => false,
1250 _ => false,
1251 }
1252}
1253
1254impl<'a> Lowering<'a> {
1255 fn new(compiler: &'a Compiler, hir: &'a hir::Program) -> Result<Self, IntegrationError> {
1256 Ok(Self {
1257 compiler,
1258 hir,
1259 wir: Program::default(),
1260 files: HashMap::new(),
1261 wir_to_hir_files: Vec::new(),
1262 globals: HashMap::new(),
1263 players: HashMap::new(),
1264 subroutines: HashMap::new(),
1265 constants: HashMap::new(),
1266 defined_subroutines: HashSet::new(),
1267 array_bindings: Vec::new(),
1268 current_rule_conditions: None,
1269 })
1270 }
1271
1272 fn copy_files(&mut self) -> Result<(), IntegrationError> {
1273 self.wir.settings = self.hir.settings.clone().map(convert_settings);
1274 for file in &self.hir.files {
1275 if self.files.contains_key(&file.id) {
1276 return Err(IntegrationError::new(
1277 "source-file",
1278 format!("duplicate HIR source file id {}", file.id),
1279 None,
1280 ));
1281 }
1282 let id = self.wir.files.push(SourceFile::new(file.path.clone()));
1283 self.files.insert(file.id, id);
1284 self.wir_to_hir_files.push(file.id);
1285 }
1286 Ok(())
1287 }
1288
1289 fn translation_helper_index(
1290 &self,
1291 implicit_reserved: &HashSet<u32>,
1292 ) -> Result<Option<u32>, IntegrationError> {
1293 if self.hir.preprocessing.translations.is_none() {
1294 return Ok(None);
1295 }
1296 let mut reserved = implicit_reserved.clone();
1297 reserved.extend(
1298 self.hir
1299 .declarations
1300 .iter()
1301 .filter_map(|declaration| match declaration {
1302 hir::Declaration::GlobalVariable {
1303 index: Some(index), ..
1304 } => Some(*index),
1305 _ => None,
1306 }),
1307 );
1308 (0..=127)
1309 .rev()
1310 .find(|index| !reserved.contains(index))
1311 .map(Some)
1312 .ok_or_else(|| {
1313 IntegrationError::new(
1314 "index-exhausted",
1315 "no available global variable index remains for translations",
1316 self.hir
1317 .preprocessing
1318 .translations
1319 .as_ref()
1320 .and_then(|value| value.span),
1321 )
1322 })
1323 }
1324
1325 fn lower_declarations(&mut self) -> Result<(), IntegrationError> {
1326 let (implicit_globals, implicit_players) = implicit_default_variables(self.hir);
1327 for declaration in &self.hir.declarations {
1328 if let hir::Declaration::GlobalVariable {
1329 name,
1330 index: Some(index),
1331 span,
1332 ..
1333 } = declaration
1334 {
1335 for (implicit_name, implicit_span) in &implicit_globals {
1336 if default_var_index(implicit_name) == Some(*index) {
1337 return Err(IntegrationError::new(
1338 "index-collision",
1339 format!(
1340 "duplicate use of index {index} for global variables '{implicit_name}' and '{name}'"
1341 ),
1342 implicit_span.or(*span),
1343 ));
1344 }
1345 }
1346 }
1347 if let hir::Declaration::PlayerVariable {
1348 name,
1349 index: Some(index),
1350 span,
1351 ..
1352 } = declaration
1353 {
1354 for (implicit_name, implicit_span) in &implicit_players {
1355 if default_var_index(implicit_name) == Some(*index) {
1356 return Err(IntegrationError::new(
1357 "index-collision",
1358 format!(
1359 "duplicate use of index {index} for player variables '{implicit_name}' and '{name}'"
1360 ),
1361 implicit_span.or(*span),
1362 ));
1363 }
1364 }
1365 }
1366 }
1367
1368 let globals = self
1369 .hir
1370 .declarations
1371 .iter()
1372 .filter_map(|declaration| match declaration {
1373 hir::Declaration::GlobalVariable { index, span, .. } => Some((*index, *span)),
1374 _ => None,
1375 })
1376 .collect::<Vec<_>>();
1377 let players = self
1378 .hir
1379 .declarations
1380 .iter()
1381 .filter_map(|declaration| match declaration {
1382 hir::Declaration::PlayerVariable { index, span, .. } => Some((*index, *span)),
1383 _ => None,
1384 })
1385 .collect::<Vec<_>>();
1386 let subroutines = self
1387 .hir
1388 .declarations
1389 .iter()
1390 .filter_map(|declaration| match declaration {
1391 hir::Declaration::Subroutine { index, span, .. } => Some((*index, *span)),
1392 _ => None,
1393 })
1394 .collect::<Vec<_>>();
1395 let implicit_reserved = implicit_globals
1396 .keys()
1397 .map(|name| default_var_index(name).expect("implicit default variable names resolve"))
1398 .collect::<HashSet<_>>();
1399 let implicit_player_reserved = implicit_players
1400 .keys()
1401 .map(|name| default_var_index(name).expect("implicit default player names resolve"))
1402 .collect::<HashSet<_>>();
1403 let translation_helper_index = self.translation_helper_index(&implicit_reserved)?;
1404 let mut global_reserved = implicit_reserved.clone();
1405 if let Some(index) = translation_helper_index {
1406 global_reserved.insert(index);
1407 }
1408 let empty = HashSet::new();
1409 let global_indices = allocate_indices(&globals, &global_reserved, "global variable")?;
1410 let player_indices =
1411 allocate_indices(&players, &implicit_player_reserved, "player variable")?;
1412 let subroutine_indices = allocate_indices(&subroutines, &empty, "subroutine")?;
1413 let mut global_index = 0;
1414 let mut player_index = 0;
1415 let mut subroutine_index = 0;
1416
1417 let mut declared_globals: Vec<(&str, u32, Option<HirSpan>, Option<HirSpan>)> = Vec::new();
1422 let mut global_initializers = Vec::new();
1423 let mut declared_players: Vec<(&str, u32, Option<HirSpan>, Option<HirSpan>)> = Vec::new();
1424 let mut player_initializers = Vec::new();
1425 let mut declared_subroutines: Vec<(&str, u32, Option<HirSpan>, Option<HirSpan>)> =
1426 Vec::new();
1427
1428 for declaration in &self.hir.declarations {
1429 match declaration {
1430 hir::Declaration::GlobalVariable {
1431 name,
1432 index: _,
1433 span,
1434 name_span,
1435 initializer,
1436 } => {
1437 let assigned = global_indices[global_index];
1438 global_index += 1;
1439 if declared_globals
1440 .iter()
1441 .any(|(existing, ..)| *existing == name)
1442 {
1443 return Err(IntegrationError::new(
1444 "symbol-collision",
1445 format!("duplicate global variable '{name}'"),
1446 *span,
1447 ));
1448 }
1449 declared_globals.push((name, assigned, *span, *name_span));
1450 if let Some(init) = initializer {
1451 if !is_zero_initializer(init) {
1452 global_initializers.push((name, init, *span, *name_span));
1453 }
1454 }
1455 }
1456 hir::Declaration::PlayerVariable {
1457 name,
1458 index: _,
1459 span,
1460 name_span,
1461 initializer,
1462 } => {
1463 let assigned = player_indices[player_index];
1464 player_index += 1;
1465 if declared_players
1466 .iter()
1467 .any(|(existing, ..)| *existing == name)
1468 {
1469 return Err(IntegrationError::new(
1470 "symbol-collision",
1471 format!("duplicate player variable '{name}'"),
1472 *span,
1473 ));
1474 }
1475 declared_players.push((name, assigned, *span, *name_span));
1476 if let Some(init) = initializer {
1477 if !is_zero_initializer(init) {
1478 player_initializers.push((name, init, *span, *name_span));
1479 }
1480 }
1481 }
1482 hir::Declaration::Subroutine {
1483 name,
1484 span,
1485 name_span,
1486 ..
1487 } => {
1488 let assigned = subroutine_indices[subroutine_index];
1489 subroutine_index += 1;
1490 if declared_subroutines
1491 .iter()
1492 .any(|(existing, ..)| *existing == name)
1493 {
1494 return Err(IntegrationError::new(
1495 "symbol-collision",
1496 format!("duplicate subroutine '{name}'"),
1497 *span,
1498 ));
1499 }
1500 declared_subroutines.push((name, assigned, *span, *name_span));
1501 }
1502 hir::Declaration::Constant { name, value, span } => {
1503 if self.constants.insert(name.clone(), value).is_some() {
1504 return Err(IntegrationError::new(
1505 "symbol-collision",
1506 format!("duplicate constant '{name}'"),
1507 *span,
1508 ));
1509 }
1510 }
1511 hir::Declaration::Macro { .. } => {
1512 }
1515 }
1516 }
1517
1518 let mut planned_globals: Vec<(String, u32, Option<HirSpan>, Option<HirSpan>)> =
1519 declared_globals
1520 .into_iter()
1521 .map(|(name, index, span, name_span)| (name.to_string(), index, span, name_span))
1522 .collect();
1523 planned_globals.extend(implicit_globals.iter().map(|(name, span)| {
1524 (
1525 name.clone(),
1526 default_var_index(name).expect("implicit default variable names resolve"),
1527 *span,
1528 None,
1529 )
1530 }));
1531 if let Some(index) = translation_helper_index {
1532 planned_globals.push((TRANSLATION_HELPER_NAME.to_string(), index, None, None));
1533 }
1534 planned_globals.sort_by_key(|(_, index, ..)| *index);
1535 for (name, assigned, span, name_span) in planned_globals {
1536 let id = self.wir.global_variables.push(wir::WorkshopVariable {
1537 name: name.clone(),
1538 index: assigned,
1539 span: self.wir_span(span)?,
1540 name_span: self.wir_span(name_span)?,
1541 });
1542 self.globals.insert(name.clone(), id);
1543 }
1544
1545 let mut planned_players: Vec<(String, u32, Option<HirSpan>, Option<HirSpan>)> =
1546 declared_players
1547 .into_iter()
1548 .map(|(name, index, span, name_span)| (name.to_string(), index, span, name_span))
1549 .collect();
1550 planned_players.extend(implicit_players.iter().map(|(name, span)| {
1551 (
1552 name.clone(),
1553 default_var_index(name).expect("implicit default player names resolve"),
1554 *span,
1555 None,
1556 )
1557 }));
1558 planned_players.sort_by_key(|(_, index, ..)| *index);
1559 for (name, assigned, span, name_span) in planned_players {
1560 let id = self.wir.player_variables.push(wir::WorkshopVariable {
1561 name: name.clone(),
1562 index: assigned,
1563 span: self.wir_span(span)?,
1564 name_span: self.wir_span(name_span)?,
1565 });
1566 self.players.insert(name, id);
1567 }
1568
1569 declared_subroutines.sort_by_key(|(_, index, ..)| *index);
1570 for (name, assigned, span, name_span) in declared_subroutines {
1571 let id = self.wir.subroutines.push(wir::WorkshopSubroutine {
1572 name: name.to_string(),
1573 index: assigned,
1574 span: self.wir_span(span)?,
1575 name_span: self.wir_span(name_span)?,
1576 });
1577 self.subroutines.insert(name.to_string(), id);
1578 }
1579
1580 let translation_initializer = self
1581 .hir
1582 .preprocessing
1583 .translations
1584 .as_ref()
1585 .map(|translations| {
1586 let variable = *self
1587 .globals
1588 .get(TRANSLATION_HELPER_NAME)
1589 .expect("translation helper variable is created");
1590 let value = self.lower_translation_helper(translations)?;
1591 Ok(self.wir.actions.push(Action::SetGlobalVariable {
1592 variable,
1593 value,
1594 span: self.wir_span(translations.span)?,
1595 target_span: None,
1596 }))
1597 })
1598 .transpose()?;
1599
1600 if translation_initializer.is_some() || !global_initializers.is_empty() {
1601 let mut actions = Vec::with_capacity(
1602 global_initializers.len() + usize::from(translation_initializer.is_some()),
1603 );
1604 if let Some(action) = translation_initializer {
1605 actions.push(action);
1606 }
1607 for (name, init_expr, span, target_span) in global_initializers {
1608 let variable = *self.globals.get(name).expect("declared global is created");
1609 let value = self.lower_value(init_expr)?;
1610 actions.push(self.wir.actions.push(Action::SetGlobalVariable {
1611 variable,
1612 value,
1613 span: self.wir_span(span)?,
1614 target_span: self.wir_span(target_span)?,
1615 }));
1616 }
1617 self.wir.rules.push(wir::Rule {
1618 name: self.global_initializer_rule_name(),
1619 span: None,
1620 name_span: None,
1621 disabled: false,
1622 event: Event::Global,
1623 conditions: Vec::new(),
1624 actions,
1625 });
1626 }
1627
1628 if !player_initializers.is_empty() {
1629 let mut actions = Vec::with_capacity(player_initializers.len());
1630 for (name, init_expr, span, target_span) in player_initializers {
1631 let variable = *self
1632 .players
1633 .get(name)
1634 .expect("declared player variable is created");
1635 let player = self
1636 .wir
1637 .values
1638 .push(ValueNode::new(Value::EventPlayer, None));
1639 let value = self.lower_value(init_expr)?;
1640 actions.push(self.wir.actions.push(Action::SetPlayerVariable {
1641 player,
1642 variable,
1643 value,
1644 span: self.wir_span(span)?,
1645 target_span: self.wir_span(target_span)?,
1646 }));
1647 }
1648 self.wir.rules.push(wir::Rule {
1649 name: "Initialize player variables".to_string(),
1650 span: None,
1651 name_span: None,
1652 disabled: false,
1653 event: Event::EachPlayer,
1654 conditions: Vec::new(),
1655 actions,
1656 });
1657 }
1658
1659 Ok(())
1660 }
1661
1662 fn lower_rules(&mut self) -> Result<(), IntegrationError> {
1663 for entry in &self.hir.rules {
1664 match entry {
1665 RuleEntry::Rule(rule) => self.lower_rule(rule)?,
1666 RuleEntry::SubroutineDef {
1667 name,
1668 source_name,
1669 span,
1670 name_span,
1671 body,
1672 annotations,
1673 ..
1674 } => {
1675 self.lower_subroutine(name, source_name, *span, *name_span, body, annotations)?
1676 }
1677 }
1678 }
1679 Ok(())
1680 }
1681
1682 fn lower_rule(&mut self, rule: &hir::Rule) -> Result<(), IntegrationError> {
1683 self.reject_rule_metadata(rule)?;
1684 let event = self.lower_event(&rule.event, &rule.annotations)?;
1685 let conditions = rule
1686 .conditions
1687 .iter()
1688 .map(|expr| self.lower_condition(expr))
1689 .collect::<Result<Vec<_>, _>>()?;
1690 let previous_conditions = self.current_rule_conditions.replace(conditions.clone());
1691 let lowered_actions = self.lower_actions(&rule.actions, None);
1692 self.current_rule_conditions = previous_conditions;
1693 let mut actions = Vec::new();
1694 actions.extend(lowered_actions?);
1695 self.wir.rules.push(wir::Rule {
1696 name: rule.name.clone(),
1697 span: self.wir_span(rule.span)?,
1698 name_span: self.wir_span(rule.name_span)?,
1699 disabled: rule.disabled,
1700 event,
1701 conditions,
1702 actions,
1703 });
1704 Ok(())
1705 }
1706
1707 fn lower_subroutine(
1708 &mut self,
1709 name: &str,
1710 source_name: &str,
1711 span: Option<HirSpan>,
1712 name_span: Option<HirSpan>,
1713 body: &[Stmt],
1714 annotations: &[hir::Annotation],
1715 ) -> Result<(), IntegrationError> {
1716 self.reject_subroutine_metadata(annotations)?;
1717 let source_name = if source_name.is_empty() {
1718 name
1719 } else {
1720 source_name
1721 };
1722 let subroutine = *self.subroutines.get(source_name).ok_or_else(|| {
1723 self.unsupported(
1724 format!("subroutine definition '{source_name}' has no declaration"),
1725 name_span.or(span),
1726 )
1727 })?;
1728 if !self.defined_subroutines.insert(subroutine) {
1729 return Err(self.unsupported(
1730 format!("subroutine '{source_name}' has multiple definitions"),
1731 name_span.or(span),
1732 ));
1733 }
1734 let mut actions = Vec::new();
1735 actions.extend(self.lower_actions(body, None)?);
1736 self.wir.rules.push(wir::Rule {
1737 name: self.subroutine_rule_name(name),
1738 span: self.wir_span(span)?,
1739 name_span: self.wir_span(name_span)?,
1740 disabled: false,
1741 event: Event::Subroutine(subroutine),
1742 conditions: Vec::new(),
1743 actions,
1744 });
1745 Ok(())
1746 }
1747
1748 fn reject_rule_metadata(&self, rule: &hir::Rule) -> Result<(), IntegrationError> {
1749 if rule.delimiter {
1750 let span = rule
1751 .annotations
1752 .iter()
1753 .find(|annotation| annotation.name == "Delimiter")
1754 .and_then(|annotation| annotation.span)
1755 .or(rule.span);
1756 return Err(self.unsupported(
1757 "rule delimiter metadata is not representable in canonical WIR",
1758 span,
1759 ));
1760 }
1761 if rule.new_page.is_some() {
1762 let span = rule
1763 .annotations
1764 .iter()
1765 .find(|annotation| annotation.name == "NewPage")
1766 .and_then(|annotation| annotation.span)
1767 .or(rule.span);
1768 return Err(self.unsupported(
1769 "rule new-page metadata is not representable in canonical WIR",
1770 span,
1771 ));
1772 }
1773 for annotation in &rule.annotations {
1774 match annotation.name.as_str() {
1775 "Event" | "Condition" | "Team" | "Slot" | "Hero" | "Disabled"
1776 | "SuppressWarnings" => {}
1777 _ => {
1778 return Err(self.unsupported(
1779 format!(
1780 "rule annotation '{}' is not representable in canonical WIR",
1781 annotation.name
1782 ),
1783 annotation.span.or(rule.span),
1784 ));
1785 }
1786 }
1787 }
1788 Ok(())
1789 }
1790
1791 fn reject_subroutine_metadata(
1792 &self,
1793 annotations: &[hir::Annotation],
1794 ) -> Result<(), IntegrationError> {
1795 for annotation in annotations {
1796 match annotation.name.as_str() {
1797 "Name" | "SuppressWarnings" => {}
1798 _ => {
1799 return Err(self.unsupported(
1800 format!(
1801 "subroutine annotation '{}' is not representable in canonical WIR",
1802 annotation.name
1803 ),
1804 annotation.span,
1805 ));
1806 }
1807 }
1808 }
1809 Ok(())
1810 }
1811
1812 fn subroutine_rule_name(&self, generated_name: &str) -> String {
1813 if self.hir.preprocessing.rule_prefix_template.is_some() {
1814 generated_name.to_string()
1815 } else {
1816 format!("Subroutine {generated_name}")
1817 }
1818 }
1819
1820 fn global_initializer_rule_name(&self) -> String {
1821 if self.hir.preprocessing.rule_prefix_template.is_some() {
1822 "[] Initialize global variables".to_string()
1823 } else {
1824 "Initialize global variables".to_string()
1825 }
1826 }
1827
1828 fn lower_event(
1829 &self,
1830 event: &hir::Event,
1831 annotations: &[hir::Annotation],
1832 ) -> Result<Event, IntegrationError> {
1833 if !event.args.is_empty() {
1834 return Err(self.unsupported(
1835 "event arguments are not representable in canonical WIR; use structural event filters",
1836 event.span,
1837 ));
1838 }
1839 let team = self.lower_event_team(annotations)?;
1840 let target = self.lower_event_target(annotations)?;
1841 let has_filters =
1842 !matches!(team, wir::EventTeam::All) || !matches!(target, wir::EventTarget::All);
1843 match event.name.as_str() {
1844 "global" => {
1845 if has_filters {
1846 return Err(
1847 self.unsupported("global events cannot have player filters", event.span)
1848 );
1849 }
1850 Ok(Event::Global)
1851 }
1852 "eachPlayer" => {
1853 if has_filters {
1854 Ok(Event::EachPlayerWithFilters { team, target })
1855 } else {
1856 Ok(Event::EachPlayer)
1857 }
1858 }
1859 name => player_event_kind(name).map_or_else(
1860 || {
1861 Err(self.unsupported(
1862 format!("event '{name}' is not supported by canonical WIR"),
1863 event.span,
1864 ))
1865 },
1866 |kind| Ok(Event::Player { kind, team, target }),
1867 ),
1868 }
1869 }
1870
1871 fn lower_event_team(
1872 &self,
1873 annotations: &[hir::Annotation],
1874 ) -> Result<wir::EventTeam, IntegrationError> {
1875 let team_annotations = annotations
1876 .iter()
1877 .filter(|annotation| annotation.name == "Team")
1878 .collect::<Vec<_>>();
1879 if team_annotations.len() > 1 {
1880 return Err(self.unsupported(
1881 "an event cannot have multiple @Team filters",
1882 team_annotations[1].span.or(team_annotations[0].span),
1883 ));
1884 }
1885 let Some(annotation) = team_annotations.first() else {
1886 return Ok(wir::EventTeam::All);
1887 };
1888 let argument = annotation
1889 .args
1890 .first()
1891 .ok_or_else(|| self.unsupported("@Team requires one filter value", annotation.span))?;
1892 if annotation.args.len() != 1 {
1893 return Err(
1894 self.unsupported("@Team requires exactly one filter value", annotation.span)
1895 );
1896 }
1897 let spelling = match argument.text.as_str() {
1898 "1" => "Team 1",
1899 "2" => "Team 2",
1900 value => value,
1901 };
1902 let (_, member) = self
1903 .compiler
1904 .catalog
1905 .resolve_enum_member("EventTeam", &Locale::new("en-US"), spelling)
1906 .ok_or_else(|| {
1907 self.unsupported(
1908 format!("unknown EventTeam filter '{spelling}'"),
1909 argument.span.or(annotation.span),
1910 )
1911 })?;
1912 match member.as_str() {
1913 "ALL" => Ok(wir::EventTeam::All),
1914 "TEAM_1" => Ok(wir::EventTeam::Team1),
1915 "TEAM_2" => Ok(wir::EventTeam::Team2),
1916 _ => Err(self.unsupported(
1917 format!("catalog EventTeam member '{member}' is not supported by canonical WIR"),
1918 argument.span.or(annotation.span),
1919 )),
1920 }
1921 }
1922
1923 fn lower_event_target(
1924 &self,
1925 annotations: &[hir::Annotation],
1926 ) -> Result<wir::EventTarget, IntegrationError> {
1927 let mut filters = Vec::new();
1928 for name in ["Slot", "Hero"] {
1929 let matches = annotations
1930 .iter()
1931 .filter(|annotation| annotation.name == name)
1932 .collect::<Vec<_>>();
1933 if matches.len() > 1 {
1934 return Err(self.unsupported(
1935 format!("an event cannot have multiple @{name} filters"),
1936 matches[1].span.or(matches[0].span),
1937 ));
1938 }
1939 filters.extend(matches);
1940 }
1941 if filters.len() > 1 {
1942 return Err(self.unsupported(
1943 "an event cannot combine @Slot and @Hero filters",
1944 filters[1].span.or(filters[0].span),
1945 ));
1946 }
1947 let Some(annotation) = filters.first() else {
1948 return Ok(wir::EventTarget::All);
1949 };
1950 let argument = annotation.args.first().ok_or_else(|| {
1951 self.unsupported(
1952 format!("@{} requires one filter value", annotation.name),
1953 annotation.span,
1954 )
1955 })?;
1956 if annotation.args.len() != 1 {
1957 return Err(self.unsupported(
1958 format!("@{} requires exactly one filter value", annotation.name),
1959 annotation.span,
1960 ));
1961 }
1962 let spelling = if annotation.name == "Slot" {
1963 match argument.text.as_str() {
1964 value if value.parse::<u8>().is_ok() => {
1965 format!("Slot {}", value.parse::<u8>().unwrap_or_default())
1966 }
1967 value => value.to_string(),
1968 }
1969 } else {
1970 argument.text.clone()
1971 };
1972 let domain = if annotation.name == "Slot" {
1973 "EventPlayer"
1974 } else {
1975 "Hero"
1976 };
1977 let locale = Locale::new("en-US");
1978 let member = self
1979 .compiler
1980 .catalog
1981 .resolve_enum_member(domain, &locale, &spelling)
1982 .map(|(_, member)| member)
1983 .or_else(|| {
1984 (domain == "Hero")
1985 .then(|| {
1986 self.compiler
1987 .catalog
1988 .enum_domain(domain)
1989 .and_then(|domain| {
1990 domain
1991 .members
1992 .iter()
1993 .find(|member| {
1994 member.spellings(&locale).iter().any(|candidate| {
1995 candidate.eq_ignore_ascii_case(&spelling)
1996 })
1997 })
1998 .map(|member| member.member.clone())
1999 })
2000 })
2001 .flatten()
2002 })
2003 .ok_or_else(|| {
2004 self.unsupported(
2005 format!("unknown {domain} filter '{spelling}'"),
2006 argument.span.or(annotation.span),
2007 )
2008 })?;
2009 if domain == "EventPlayer" {
2010 if member == "ALL" {
2011 Ok(wir::EventTarget::All)
2012 } else if let Some(slot) = member.strip_prefix("SLOT_") {
2013 let slot = slot.parse::<u8>().map_err(|_| {
2014 self.unsupported(
2015 format!("catalog EventPlayer member '{member}' is not a slot"),
2016 argument.span.or(annotation.span),
2017 )
2018 })?;
2019 Ok(wir::EventTarget::Slot(slot))
2020 } else {
2021 Err(self.unsupported(
2022 format!(
2023 "catalog EventPlayer member '{member}' is not supported by canonical WIR"
2024 ),
2025 argument.span.or(annotation.span),
2026 ))
2027 }
2028 } else {
2029 Ok(wir::EventTarget::Hero(member))
2030 }
2031 }
2032
2033 fn lower_actions(
2034 &mut self,
2035 statements: &[Stmt],
2036 break_target: Option<BreakTarget>,
2037 ) -> Result<Vec<wir::ActionId>, IntegrationError> {
2038 let mut actions = Vec::new();
2039 for statement in statements {
2040 actions.extend(self.lower_action(statement, break_target)?);
2041 }
2042 Ok(actions)
2043 }
2044
2045 fn lower_action(
2046 &mut self,
2047 stmt: &Stmt,
2048 break_target: Option<BreakTarget>,
2049 ) -> Result<Vec<wir::ActionId>, IntegrationError> {
2050 match stmt {
2051 Stmt::Pass { .. } => Ok(Vec::new()),
2052 Stmt::Assign {
2053 target,
2054 value,
2055 span,
2056 } => self.lower_assign(target, value, *span).map(|action| vec![action]),
2057 Stmt::If {
2058 branches,
2059 r#else,
2060 span,
2061 } => {
2062 let branches = branches
2063 .iter()
2064 .map(|branch| {
2065 Ok(wir::IfBranch {
2066 condition: self.lower_value(&branch.condition)?,
2067 body: self.lower_actions(&branch.body, break_target)?,
2068 })
2069 })
2070 .collect::<Result<Vec<_>, IntegrationError>>()?;
2071 let else_body = r#else
2072 .as_ref()
2073 .map(|body| self.lower_actions(body, break_target))
2074 .transpose()?;
2075 Ok(vec![self.wir.actions.push(Action::If {
2076 branches,
2077 else_body,
2078 span: self.wir_span(*span)?,
2079 })])
2080 }
2081 Stmt::For {
2082 variable,
2083 iterable,
2084 body,
2085 span,
2086 } => {
2087 let (start, stop, step) = self.lower_range(iterable)?;
2088 let body = self.lower_loop_body(body)?;
2089 match variable.as_ref() {
2090 Expr::GlobalVar {
2091 name,
2092 span: target_span,
2093 } => {
2094 let variable_id = *self.globals.get(name).ok_or_else(|| {
2095 self.unsupported(
2096 format!("unknown global variable '{name}'"),
2097 *target_span,
2098 )
2099 })?;
2100 Ok(vec![self.wir.actions.push(Action::ForGlobalVariable {
2101 variable: variable_id,
2102 start,
2103 stop,
2104 step,
2105 body,
2106 span: self.wir_span(*span)?,
2107 target_span: self.wir_span(*target_span)?,
2108 })])
2109 }
2110 Expr::PlayerVar {
2111 player,
2112 name,
2113 span: target_span,
2114 ..
2115 } => {
2116 let variable_id = *self.players.get(name).ok_or_else(|| {
2117 self.unsupported(
2118 format!("unknown player variable '{name}'"),
2119 *target_span,
2120 )
2121 })?;
2122 let player = self.lower_value(player)?;
2123 Ok(vec![self.wir.actions.push(Action::ForPlayerVariable {
2124 player,
2125 variable: variable_id,
2126 start,
2127 stop,
2128 step,
2129 body,
2130 span: self.wir_span(*span)?,
2131 })])
2132 }
2133 _ => Err(self.unsupported(
2134 "range loops require a global- or player-variable binder in canonical WIR",
2135 variable.span().copied(),
2136 )),
2137 }
2138 }
2139 Stmt::While {
2140 condition,
2141 body,
2142 span,
2143 } => {
2144 let condition = self.lower_value(condition)?;
2145 let body = self.lower_loop_body(body)?;
2146 Ok(vec![self.wir.actions.push(Action::While {
2147 condition,
2148 body,
2149 span: self.wir_span(*span)?,
2150 })])
2151 }
2152 Stmt::DoWhile {
2153 condition,
2154 body,
2155 span,
2156 } => {
2157 let body = self.lower_do_while_body(body)?;
2158 let condition = self.lower_value(condition)?;
2159 let loop_if = self.wir.actions.push(Action::Call {
2160 name: "loopIf".to_string(),
2161 args: vec![condition],
2162 span: self.wir_span(*span)?,
2163 });
2164 let mut actions = body;
2167 actions.push(loop_if);
2168 Ok(actions)
2169 }
2170 Stmt::Switch {
2171 value,
2172 arms,
2173 span,
2174 } => self.lower_switch(value, arms, *span).map(|action| vec![action]),
2175 Stmt::Delete { span, .. } => Err(self.unsupported(
2176 "delete statements are not representable in canonical WIR",
2177 *span,
2178 )),
2179 Stmt::Continue { span } => Err(self.unsupported(
2180 "continue statements are only lowered while constructing a loop body",
2181 *span,
2182 )),
2183 Stmt::Goto { span, .. } => Err(self.unsupported(
2184 "goto statements are not representable in canonical WIR",
2185 *span,
2186 )),
2187 Stmt::Label { span, .. } => Err(self.unsupported(
2188 "labels are not representable in canonical WIR",
2189 *span,
2190 )),
2191 Stmt::Break { span } => match break_target {
2192 Some(BreakTarget::Loop) => Ok(vec![self.wir.actions.push(Action::Call {
2193 name: "break".to_string(),
2194 args: Vec::new(),
2195 span: self.wir_span(*span)?,
2196 })]),
2197 Some(BreakTarget::DoWhile) => Err(self.unsupported(
2198 "break inside a do-while must be a direct statement or a single conditional break",
2199 *span,
2200 )),
2201 Some(BreakTarget::Switch) => Err(self.unsupported(
2202 "break inside a nested conditional cannot be normalized into canonical switch control flow",
2203 *span,
2204 )),
2205 None => Err(self.unsupported(
2206 "break has no enclosing canonical loop or switch",
2207 *span,
2208 )),
2209 },
2210 Stmt::Return { span } => {
2211 let true_value = self.wir.values.push(ValueNode::new(
2212 Value::Bool(true),
2213 self.wir_span(*span)?,
2214 ));
2215 Ok(vec![self.wir.actions.push(Action::Call {
2216 name: "abortIf".to_string(),
2217 args: vec![true_value],
2218 span: self.wir_span(*span)?,
2219 })])
2220 }
2221 Stmt::Expr { expr, span } => match expr.as_ref() {
2222 Expr::Call { name, args, .. } => {
2223 if name == "disableInspector" && args.is_empty() {
2224 Ok(vec![self.wir.actions.push(Action::Call {
2225 name: "disableInspector".to_string(),
2226 args: Vec::new(),
2227 span: self.wir_span(*span)?,
2228 })])
2229 } else if name == "debug" && args.len() == 1 {
2230 Ok(vec![self.lower_debug(&args[0], *span)?])
2231 } else if name == "print" && args.len() == 1 {
2232 Ok(vec![self.lower_print(&args[0], *span)?])
2233 } else {
2234 self.lower_action_call(name, args, *span).map(|action| vec![action])
2235 }
2236 }
2237 Expr::ReceiverCall {
2238 receiver,
2239 name,
2240 args,
2241 span: call_span,
2242 } => self
2243 .lower_receiver_action_call(receiver, name, args, *call_span)
2244 .map(|action| vec![action]),
2245 _ => Err(self.unsupported(
2246 "only action calls are currently representable as expression statements in canonical WIR",
2247 *span,
2248 )),
2249 },
2250 Stmt::CallSubroutine { name, span } => {
2251 let subroutine = *self.subroutines.get(name).ok_or_else(|| {
2252 self.unsupported(format!("unknown subroutine '{name}'"), *span)
2253 })?;
2254 let span = self.wir_span(*span)?;
2255 Ok(vec![self.wir.actions.push(Action::CallSubroutine {
2256 subroutine,
2257 span,
2258 callee_span: span,
2259 })])
2260 }
2261 }
2262 }
2263
2264 fn lower_loop_body(
2270 &mut self,
2271 statements: &[Stmt],
2272 ) -> Result<Vec<wir::ActionId>, IntegrationError> {
2273 self.lower_loop_sequence(statements, &[], 0)
2274 }
2275
2276 fn lower_loop_sequence(
2282 &mut self,
2283 statements: &[Stmt],
2284 after: &[wir::ActionId],
2285 structural_after: usize,
2286 ) -> Result<Vec<wir::ActionId>, IntegrationError> {
2287 let mut actions = Vec::new();
2288 let mut index = 0;
2289 while index < statements.len() {
2290 let statement = &statements[index];
2291 let tail = &statements[index + 1..];
2292 if let Some(conditions) = pure_continue_conditions(statement) {
2293 let tail = self.lower_loop_sequence(tail, after, structural_after)?;
2294 let distance = self.canonical_action_width(&tail, statement.span().copied())?
2295 + structural_after
2296 + self.canonical_action_width(after, statement.span().copied())?;
2297 if distance > 0 {
2298 let mut args = Vec::with_capacity(conditions.len() + 1);
2299 if let Some((first, rest)) = conditions.split_first() {
2300 let mut condition = self.lower_value(first)?;
2301 for expression in rest {
2302 let right = self.lower_value(expression)?;
2303 condition = self.push_call("and", vec![condition, right]);
2304 }
2305 args.push(condition);
2306 }
2307 let distance = self.wir.values.push(ValueNode::new(
2308 Value::Number {
2309 value: distance as f64,
2310 text: distance.to_string(),
2311 },
2312 self.wir_span(statement.span().copied())?,
2313 ));
2314 args.push(distance);
2315 actions.push(
2316 self.wir.actions.push(Action::Call {
2317 name: if conditions.is_empty() {
2318 "skip"
2319 } else {
2320 "skipIf"
2321 }
2322 .to_string(),
2323 args,
2324 span: self.wir_span(statement.span().copied())?,
2325 }),
2326 );
2327 }
2328 actions.extend(tail);
2329 return Ok(actions);
2330 }
2331 if contains_loop_continue(statement) {
2332 let tail = self.lower_loop_sequence(tail, after, structural_after)?;
2333 let mut continuation_after = tail.clone();
2334 continuation_after.extend_from_slice(after);
2335 let lowered = self.lower_if_with_loop_continue(
2336 statement,
2337 &continuation_after,
2338 structural_after,
2339 )?;
2340 actions.push(lowered);
2341 actions.extend(tail);
2342 return Ok(actions);
2343 }
2344 actions.extend(self.lower_action(statement, Some(BreakTarget::Loop))?);
2345 index += 1;
2346 }
2347 Ok(actions)
2348 }
2349
2350 fn lower_if_with_loop_continue(
2351 &mut self,
2352 statement: &Stmt,
2353 after: &[wir::ActionId],
2354 structural_after: usize,
2355 ) -> Result<wir::ActionId, IntegrationError> {
2356 let Stmt::If {
2357 branches,
2358 r#else,
2359 span,
2360 } = statement
2361 else {
2362 unreachable!("continue-containing loop statement must be an if")
2363 };
2364 let mut lowered_branches = Vec::with_capacity(branches.len());
2365 let mut suffix = after.to_vec();
2366 let mut suffix_structural = structural_after + 1;
2367 let mut lowered_else = None;
2368 if let Some(body) = r#else {
2369 let body = self.lower_loop_sequence(body, after, suffix_structural)?;
2370 suffix.splice(0..0, body.iter().copied());
2371 suffix_structural += 1;
2372 lowered_else = Some(body);
2373 }
2374 for index in (0..branches.len()).rev() {
2375 let body =
2376 self.lower_loop_sequence(&branches[index].body, &suffix, suffix_structural)?;
2377 suffix_structural += 1;
2378 suffix.splice(0..0, body.iter().copied());
2379 lowered_branches.push(body);
2380 }
2381 lowered_branches.reverse();
2382 let mut branch_actions = Vec::with_capacity(branches.len());
2383 for (branch, body) in branches.iter().zip(lowered_branches) {
2384 branch_actions.push(wir::IfBranch {
2385 condition: self.lower_value(&branch.condition)?,
2386 body,
2387 });
2388 }
2389 Ok(self.wir.actions.push(Action::If {
2390 branches: branch_actions,
2391 else_body: lowered_else,
2392 span: self.wir_span(*span)?,
2393 }))
2394 }
2395
2396 fn lower_do_while_body(
2397 &mut self,
2398 statements: &[Stmt],
2399 ) -> Result<Vec<wir::ActionId>, IntegrationError> {
2400 let mut actions = Vec::new();
2401 for (index, statement) in statements.iter().enumerate() {
2402 let direct_break = matches!(statement, Stmt::Break { .. });
2403 let conditional_break = match statement {
2404 Stmt::If {
2405 branches,
2406 r#else: None,
2407 ..
2408 } if branches.len() == 1 => {
2409 matches!(branches[0].body.as_slice(), [Stmt::Break { .. }])
2410 }
2411 _ => false,
2412 };
2413
2414 if direct_break || conditional_break {
2415 let tail = self.lower_do_while_body(&statements[index + 1..])?;
2416 let distance = self.canonical_action_width(&tail, statement.span().copied())? + 1;
2417 let (name, args, span) = if let Stmt::Break { span } = statement {
2418 ("skip", Vec::new(), *span)
2419 } else if let Stmt::If { branches, span, .. } = statement {
2420 (
2421 "skipIf",
2422 vec![self.lower_value(&branches[0].condition)?],
2423 *span,
2424 )
2425 } else {
2426 unreachable!("break shape was checked above")
2427 };
2428 let distance = self.wir.values.push(ValueNode::new(
2429 Value::Number {
2430 value: distance as f64,
2431 text: distance.to_string(),
2432 },
2433 self.wir_span(span)?,
2434 ));
2435 let mut args = args;
2436 args.push(distance);
2437 actions.push(self.wir.actions.push(Action::Call {
2438 name: name.to_string(),
2439 args,
2440 span: self.wir_span(span)?,
2441 }));
2442 actions.extend(tail);
2443 return Ok(actions);
2444 }
2445
2446 actions.extend(self.lower_action(statement, Some(BreakTarget::DoWhile))?);
2447 }
2448 Ok(actions)
2449 }
2450
2451 fn lower_range(
2452 &mut self,
2453 iterable: &Expr,
2454 ) -> Result<(wir::ValueId, wir::ValueId, wir::ValueId), IntegrationError> {
2455 let Expr::Call { name, args, .. } = iterable else {
2456 return Err(self.unsupported(
2457 "range loop iterable must be a range(...) call",
2458 iterable.span().copied(),
2459 ));
2460 };
2461 if name != "range" || !(1..=3).contains(&args.len()) {
2462 return Err(self.unsupported(
2463 "range loop requires one to three arguments",
2464 iterable.span().copied(),
2465 ));
2466 }
2467 let span = iterable.span().copied();
2468 let number = |this: &mut Self, value: f64| -> Result<wir::ValueId, IntegrationError> {
2469 Ok(this.wir.values.push(ValueNode::new(
2470 Value::Number {
2471 value,
2472 text: value.to_string(),
2473 },
2474 this.wir_span(span)?,
2475 )))
2476 };
2477 match args.as_slice() {
2478 [stop] => Ok((
2479 number(self, 0.0)?,
2480 self.lower_value(stop)?,
2481 number(self, 1.0)?,
2482 )),
2483 [start, stop] => Ok((
2484 self.lower_value(start)?,
2485 self.lower_value(stop)?,
2486 number(self, 1.0)?,
2487 )),
2488 [start, stop, step] => Ok((
2489 self.lower_value(start)?,
2490 self.lower_value(stop)?,
2491 self.lower_value(step)?,
2492 )),
2493 _ => unreachable!("range arity checked above"),
2494 }
2495 }
2496
2497 fn lower_switch(
2498 &mut self,
2499 value: &Expr,
2500 arms: &[SwitchArm],
2501 span: Option<HirSpan>,
2502 ) -> Result<wir::ActionId, IntegrationError> {
2503 let selector = self.lower_value(value)?;
2504 let mut case_values = Vec::new();
2505 let mut lowered_arms = Vec::with_capacity(arms.len());
2506 let mut case_offsets = Vec::new();
2507 let mut offset = 0usize;
2508 let mut default_offset = None;
2509
2510 for arm in arms {
2511 let (value, (body, break_at)) = match arm {
2512 SwitchArm::Case { value, body, .. } => {
2513 case_values.push(self.lower_value(value)?);
2514 (Some(value), self.lower_switch_body(body)?)
2515 }
2516 SwitchArm::Default { body, span } => {
2517 if default_offset.is_some() {
2518 return Err(
2519 self.unsupported("a switch may contain at most one default arm", *span)
2520 );
2521 }
2522 default_offset = Some(offset);
2523 (None, self.lower_switch_body(body)?)
2524 }
2525 };
2526 if value.is_some() {
2527 case_offsets.push(offset);
2528 }
2529 offset += self.canonical_action_width(&body, span)? + usize::from(break_at.is_some());
2530 lowered_arms.push((value, body, break_at));
2531 }
2532 let default_offset = default_offset.unwrap_or(offset);
2533
2534 let break_arms: Vec<_> = lowered_arms
2535 .iter()
2536 .enumerate()
2537 .filter_map(|(index, (_, _, break_at))| break_at.map(|break_at| (index, break_at)))
2538 .collect();
2539 if break_arms.len() > 1 {
2540 let (first_index, first_break) = break_arms[0];
2541 let has_actions_after_first = lowered_arms[first_index].1.len() > first_break.0
2542 || lowered_arms
2543 .iter()
2544 .skip(first_index + 1)
2545 .any(|(_, body, _)| !body.is_empty());
2546 if has_actions_after_first {
2547 return Err(self.unsupported(
2548 "multiple switch breaks with later reachable actions require canonical switch targets",
2549 Some(break_arms[1].1.1),
2550 ));
2551 }
2552 }
2553
2554 let case_values = self.lower_array(case_values, span)?;
2555 let value_span = self.wir_span(span)?;
2556 let offset_values = std::iter::once(default_offset)
2557 .chain(case_offsets)
2558 .map(|value| {
2559 self.wir.values.push(ValueNode::new(
2560 Value::Number {
2561 value: value as f64,
2562 text: value.to_string(),
2563 },
2564 value_span,
2565 ))
2566 })
2567 .collect();
2568 let offsets = self.lower_array(offset_values, span)?;
2569 let one = self.wir.values.push(ValueNode::new(
2570 Value::Number {
2571 value: 1.0,
2572 text: "1".to_string(),
2573 },
2574 self.wir_span(span)?,
2575 ));
2576 let index = self.wir.values.push(ValueNode::new(
2577 Value::Call {
2578 name: "indexOfArrayValue".to_string(),
2579 args: vec![case_values, selector],
2580 },
2581 self.wir_span(span)?,
2582 ));
2583 let case_offset = self.wir.values.push(ValueNode::new(
2584 Value::Call {
2585 name: "add".to_string(),
2586 args: vec![one, index],
2587 },
2588 self.wir_span(span)?,
2589 ));
2590 let skip_condition = self.wir.values.push(ValueNode::new(
2591 Value::Call {
2592 name: "valueInArray".to_string(),
2593 args: vec![offsets, case_offset],
2594 },
2595 self.wir_span(span)?,
2596 ));
2597 let skip = self.wir.actions.push(Action::Call {
2598 name: "skip".to_string(),
2599 args: vec![skip_condition],
2600 span: self.wir_span(span)?,
2601 });
2602 let true_value = self
2603 .wir
2604 .values
2605 .push(ValueNode::new(Value::Bool(true), self.wir_span(span)?));
2606
2607 let first_break = break_arms.first().copied();
2608 let mut branch_body = vec![skip];
2609 let else_body = if let Some((break_index, (break_at, _))) = first_break {
2610 for (index, (_, body, _)) in lowered_arms.iter().enumerate() {
2611 if index < break_index {
2612 branch_body.extend(body.iter().copied());
2613 } else if index == break_index {
2614 branch_body.extend(body[..break_at].iter().copied());
2615 }
2616 }
2617 let mut tail = Vec::new();
2618 tail.extend(lowered_arms[break_index].1[break_at..].iter().copied());
2619 for (_, body, _) in lowered_arms.iter().skip(break_index + 1) {
2620 tail.extend(body.iter().copied());
2621 }
2622 Some(tail)
2623 } else {
2624 for (_, body, _) in &lowered_arms {
2625 branch_body.extend(body.iter().copied());
2626 }
2627 None
2628 };
2629
2630 Ok(self.wir.actions.push(Action::If {
2631 branches: vec![wir::IfBranch {
2632 condition: true_value,
2633 body: branch_body,
2634 }],
2635 else_body,
2636 span: self.wir_span(span)?,
2637 }))
2638 }
2639
2640 fn lower_switch_body(
2641 &mut self,
2642 statements: &[Stmt],
2643 ) -> Result<LoweredSwitchBody, IntegrationError> {
2644 let mut actions = Vec::new();
2645 let mut break_at = None;
2646 for statement in statements {
2647 if let Stmt::Break { span } = statement {
2648 if break_at.is_some() {
2649 return Err(self.unsupported(
2650 "multiple switch breaks in one arm require canonical switch targets",
2651 *span,
2652 ));
2653 }
2654 break_at = Some((
2655 actions.len(),
2656 span.ok_or_else(|| {
2657 self.unsupported("switch break is missing source provenance", None)
2658 })?,
2659 ));
2660 continue;
2661 }
2662 actions.extend(self.lower_action(statement, Some(BreakTarget::Switch))?);
2663 }
2664 Ok((actions, break_at))
2665 }
2666
2667 fn canonical_action_width(
2669 &self,
2670 actions: &[wir::ActionId],
2671 fallback_span: Option<HirSpan>,
2672 ) -> Result<usize, IntegrationError> {
2673 workshop_rs::emitter::action_width(
2674 &self.wir,
2675 &self.compiler.catalog,
2676 &Locale::new("en-US"),
2677 actions,
2678 )
2679 .map(|layout| layout.width)
2680 .map_err(|error| {
2681 let workshop_span = match &error {
2682 workshop_rs::emitter::ActionLayoutError::InvalidWIR(error) => error.span(),
2683 workshop_rs::emitter::ActionLayoutError::Emission(error) => {
2684 workshop_error_span(error)
2685 }
2686 };
2687 let span = workshop_span
2688 .and_then(|span| self.hir_span_from_workshop(span))
2689 .or(fallback_span);
2690 IntegrationError::new("workshop-action-layout", error.to_string(), span)
2691 })
2692 }
2693
2694 fn lower_array(
2695 &mut self,
2696 elements: Vec<wir::ValueId>,
2697 span: Option<HirSpan>,
2698 ) -> Result<wir::ValueId, IntegrationError> {
2699 let name = if elements.is_empty() {
2700 "emptyArray"
2701 } else {
2702 "array"
2703 };
2704 Ok(self.wir.values.push(ValueNode::new(
2705 Value::Call {
2706 name: name.to_string(),
2707 args: elements,
2708 },
2709 self.wir_span(span)?,
2710 )))
2711 }
2712
2713 fn lower_translation_helper(
2714 &mut self,
2715 translations: &hir::TranslationState,
2716 ) -> Result<wir::ValueId, IntegrationError> {
2717 let translated_white = translations
2718 .languages
2719 .iter()
2720 .map(|language| {
2721 let locale = translation_locale(language).ok_or_else(|| {
2722 IntegrationError::new(
2723 "translations-invalid",
2724 format!("unsupported translation language '{language}'"),
2725 translations.span,
2726 )
2727 })?;
2728 self.compiler
2729 .catalog
2730 .enum_spelling("Color", &Locale::new(locale), "WHITE")
2731 .map(str::to_string)
2732 .ok_or_else(|| {
2733 IntegrationError::new(
2734 "translations-invalid",
2735 format!("catalog has no Color.WHITE spelling for locale '{locale}'"),
2736 translations.span,
2737 )
2738 })
2739 })
2740 .collect::<Result<Vec<_>, _>>()?
2741 .join("0");
2742 let text = self.push_value(Value::String(format!("\u{ec48}0{translated_white}")));
2743 let custom_string = self.push_call("customString", vec![text]);
2744 let null = self.push_value(Value::Null);
2745 let separator = self.push_call("firstOf", vec![null]);
2746 Ok(self.push_call("stringSplit", vec![custom_string, separator]))
2747 }
2748
2749 fn lower_debug(
2750 &mut self,
2751 expr: &Expr,
2752 span: Option<HirSpan>,
2753 ) -> Result<wir::ActionId, IntegrationError> {
2754 macro_rules! call {
2755 ($name:literal $(, $arg:expr)* $(,)?) => {{
2756 let args = vec![$($arg),*];
2757 self.push_call($name, args)
2758 }};
2759 }
2760
2761 let value = self.lower_text_value(expr)?;
2762 let array_text = if self.debug_value_is_array(value) {
2763 self.lower_debug_array_text(value)
2764 } else {
2765 value
2766 };
2767 let debug_label = canonical_debug_text(&debug_expr_text(expr));
2768 let debug_prefix = format!("{debug_label}\u{2028}= {{0}}");
2769 let inline_padding = 128 - debug_prefix.chars().count() - "{1}".chars().count();
2770 let padding_text = self.push_value(Value::String(" ".repeat(170 - inline_padding)));
2771 let padding = self.push_call("customString", vec![padding_text]);
2772 let debug_label = self.push_value(Value::String(format!(
2773 "{debug_prefix}{}{{1}}",
2774 " ".repeat(inline_padding)
2775 )));
2776 let text = self.push_call("customString", vec![debug_label, array_text, padding]);
2777 let all_teams = self.push_value(Value::Enum {
2778 value_type: "Team".to_string(),
2779 value: "ALL".to_string(),
2780 });
2781 let all_players = call!("allPlayers", all_teams);
2782 let null_value = self.push_value(Value::Null);
2783 let null_value_2 = self.push_value(Value::Null);
2784 let null_value_3 = self.push_value(Value::Null);
2785 let null_value_4 = self.push_value(Value::Null);
2786 let hud_position = self.push_value(Value::Enum {
2787 value_type: "HudPosition".to_string(),
2788 value: "LEFT".to_string(),
2789 });
2790 let sort_order = self.push_value(Value::Number {
2791 value: -9999.0,
2792 text: "-9999".to_string(),
2793 });
2794 let color = self.push_value(Value::Enum {
2795 value_type: "Color".to_string(),
2796 value: "WHITE".to_string(),
2797 });
2798 let reevaluation = self.push_value(Value::Enum {
2799 value_type: "HudReeval".to_string(),
2800 value: "VISIBILITY_SORT_ORDER_STRING_AND_COLOR".to_string(),
2801 });
2802 let visibility = self.push_value(Value::Enum {
2803 value_type: "SpecVisibility".to_string(),
2804 value: "DEFAULT".to_string(),
2805 });
2806 Ok(self.wir.actions.push(Action::Call {
2807 name: "createHudText".to_string(),
2808 args: vec![
2809 all_players,
2810 null_value,
2811 text,
2812 null_value_2,
2813 hud_position,
2814 sort_order,
2815 null_value_3,
2816 color,
2817 null_value_4,
2818 reevaluation,
2819 visibility,
2820 ],
2821 span: self.wir_span(span)?,
2822 }))
2823 }
2824
2825 fn lower_print(
2826 &mut self,
2827 expr: &Expr,
2828 span: Option<HirSpan>,
2829 ) -> Result<wir::ActionId, IntegrationError> {
2830 macro_rules! call {
2831 ($name:literal $(, $arg:expr)* $(,)?) => {{
2832 let args = vec![$($arg),*];
2833 self.push_call($name, args)
2834 }};
2835 }
2836
2837 let message = self.lower_text_value(expr)?;
2838 let padding_text = self.push_value(Value::String(" ".repeat(45)));
2839 let padding = self.push_call("customString", vec![padding_text]);
2840 let body_text = self.push_value(Value::String(format!("{}{{0}}", " ".repeat(125))));
2841 let body = self.push_call("customString", vec![body_text, padding]);
2842 let all_teams = self.push_value(Value::Enum {
2843 value_type: "Team".to_string(),
2844 value: "ALL".to_string(),
2845 });
2846 let all_players = call!("allPlayers", all_teams);
2847 let null_value = self.push_value(Value::Null);
2848 let null_value_2 = self.push_value(Value::Null);
2849 let null_value_3 = self.push_value(Value::Null);
2850 let hud_position = self.push_value(Value::Enum {
2851 value_type: "HudPosition".to_string(),
2852 value: "LEFT".to_string(),
2853 });
2854 let sort_order = self.push_value(Value::Number {
2855 value: -9999.0,
2856 text: "-9999".to_string(),
2857 });
2858 let color = self.push_value(Value::Enum {
2859 value_type: "Color".to_string(),
2860 value: "ORANGE".to_string(),
2861 });
2862 let reevaluation = self.push_value(Value::Enum {
2863 value_type: "HudReeval".to_string(),
2864 value: "VISIBILITY_AND_STRING".to_string(),
2865 });
2866 let visibility = self.push_value(Value::Enum {
2867 value_type: "SpecVisibility".to_string(),
2868 value: "DEFAULT".to_string(),
2869 });
2870 Ok(self.wir.actions.push(Action::Call {
2871 name: "createHudText".to_string(),
2872 args: vec![
2873 all_players,
2874 message,
2875 body,
2876 null_value,
2877 hud_position,
2878 sort_order,
2879 color,
2880 null_value_2,
2881 null_value_3,
2882 reevaluation,
2883 visibility,
2884 ],
2885 span: self.wir_span(span)?,
2886 }))
2887 }
2888
2889 fn lower_debug_array_text(&mut self, value: wir::ValueId) -> wir::ValueId {
2890 macro_rules! call {
2891 ($name:literal $(, $arg:expr)* $(,)?) => {{
2892 let args = vec![$($arg),*];
2893 self.push_call($name, args)
2894 }};
2895 }
2896
2897 let current_count = call!("countOf", call!("currentArrayElement"));
2898 let is_single = call!(
2899 "==",
2900 call!("countOf", call!("currentArrayElement")),
2901 self.push_number(1.0, "1")
2902 );
2903 let is_empty = call!("==", call!("currentArrayElement"), call!("emptyArray"));
2904 let not_null = call!(
2905 "!=",
2906 call!("currentArrayElement"),
2907 self.push_value(Value::Null)
2908 );
2909 let has_empty_array = call!("and", is_empty, not_null);
2910 let brackets = call!("or", is_single, has_empty_array);
2911 let first_element = call!(
2912 "customString",
2913 self.push_value(Value::String("[{0}]".to_string())),
2914 call!("currentArrayElement"),
2915 );
2916 let many_elements = call!(
2917 "customString",
2918 self.push_value(Value::String("[{0}, …+{1}]".to_string())),
2919 call!("currentArrayElement"),
2920 call!(
2921 "subtract",
2922 call!("countOf", call!("currentArrayElement")),
2923 self.push_number(1.0, "1"),
2924 ),
2925 );
2926 let element_text = call!(
2927 "ifThenElse",
2928 brackets,
2929 first_element,
2930 call!(
2931 "ifThenElse",
2932 current_count,
2933 many_elements,
2934 call!("currentArrayElement"),
2935 ),
2936 );
2937 let mapped_elements = call!("mappedArray", value, element_text,);
2938 let mapped_input = call!("array", mapped_elements);
2939 let current_array = call!("currentArrayElement");
2940 let actual_array = call!(
2941 "or",
2942 call!("countOf", current_array),
2943 call!(
2944 "and",
2945 call!("==", call!("currentArrayElement"), call!("emptyArray")),
2946 call!(
2947 "!=",
2948 call!("currentArrayElement"),
2949 self.push_value(Value::Null)
2950 ),
2951 ),
2952 );
2953 let empty_length = call!(
2954 "ifThenElse",
2955 call!(
2956 "and",
2957 call!("not", call!("countOf", call!("currentArrayElement"))),
2958 call!("!=", call!("currentArrayElement"), call!("emptyArray"),),
2959 ),
2960 self.push_number(3.0, "3"),
2961 call!(
2962 "multiply",
2963 call!("countOf", call!("currentArrayElement")),
2964 self.push_number(3.0, "3"),
2965 ),
2966 );
2967 let x = call!(
2968 "appendToArray",
2969 call!("appendToArray", actual_array, empty_length),
2970 current_array,
2971 );
2972 let x_input = call!("mappedArray", mapped_input, x);
2973 let x_length = |this: &mut Self| {
2974 let current = this.push_call("currentArrayElement", Vec::new());
2975 let index = this.push_number(1.0, "1");
2976 this.push_call("valueInArray", vec![current, index])
2977 };
2978 let x_value = |this: &mut Self, index: f64| {
2979 let current = this.push_call("currentArrayElement", Vec::new());
2980 let index_value = this.push_number(index, &index.to_string());
2981 this.push_call("valueInArray", vec![current, index_value])
2982 };
2983 let first = call!("firstOf", call!("currentArrayElement"));
2984 let array_tail = call!(
2985 "customString",
2986 self.push_value(Value::String("{0}, {1}, {2}".to_string())),
2987 x_value(self, 4.0),
2988 x_value(self, 5.0),
2989 call!(
2990 "customString",
2991 self.push_value(Value::String("{0}, {1}, …\u{0001}".to_string())),
2992 x_value(self, 6.0),
2993 x_value(self, 7.0),
2994 ),
2995 );
2996 let array_head = call!(
2997 "customString",
2998 self.push_value(Value::String("{0}, {1}, {2}".to_string())),
2999 x_value(self, 2.0),
3000 x_value(self, 3.0),
3001 array_tail,
3002 );
3003 let placeholder = call!(
3004 "customString",
3005 self.push_value(Value::String("0, 0, 0, 0, 0, 0, …\u{0001}".to_string())),
3006 );
3007 let length_for_slice = x_length(self);
3008 let end_length_for_slice = x_length(self);
3009 let slice = call!(
3010 "stringSlice",
3011 placeholder,
3012 call!("add", self.push_number(-2.0, "-2"), length_for_slice),
3013 call!(
3014 "subtract",
3015 self.push_number(22.0, "22"),
3016 end_length_for_slice,
3017 ),
3018 );
3019 let replaced = call!("stringReplace", array_head, slice, call!("emptyArray"),);
3020 let length_for_compare = x_length(self);
3021 let length_for_divide = x_length(self);
3022 let plus = call!(
3023 "ifThenElse",
3024 call!(">", length_for_compare, self.push_number(18.0, "18")),
3025 call!(
3026 "customString",
3027 self.push_value(Value::String("+{0}".to_string())),
3028 call!(
3029 "subtract",
3030 call!("divide", length_for_divide, self.push_number(3.0, "3")),
3031 self.push_number(6.0, "6"),
3032 ),
3033 ),
3034 call!("emptyArray"),
3035 );
3036 let formatted_array = call!(
3037 "customString",
3038 self.push_value(Value::String("[{0}{1}]".to_string())),
3039 replaced,
3040 plus,
3041 );
3042 let current_for_split = call!("currentArrayElement");
3043 let rendered = call!(
3044 "ifThenElse",
3045 first,
3046 formatted_array,
3047 call!(
3048 "stringSplit",
3049 call!(
3050 "valueInArray",
3051 current_for_split,
3052 self.push_number(2.0, "2")
3053 ),
3054 call!("emptyArray"),
3055 ),
3056 );
3057 call!("mappedArray", x_input, rendered)
3058 }
3059
3060 fn lower_text_value(&mut self, expr: &Expr) -> Result<wir::ValueId, IntegrationError> {
3061 let value = self.lower_value(expr)?;
3062 let Value::Call { name, args } = &self
3063 .wir
3064 .values
3065 .get(value)
3066 .expect("lowered text value must exist")
3067 .value
3068 else {
3069 return Ok(value);
3070 };
3071 if name == "customString" && args.len() == 1 {
3072 Ok(args[0])
3073 } else {
3074 Ok(value)
3075 }
3076 }
3077
3078 fn debug_value_is_array(&self, value: wir::ValueId) -> bool {
3079 match &self
3080 .wir
3081 .values
3082 .get(value)
3083 .expect("lowered value must exist")
3084 .value
3085 {
3086 Value::GlobalVariable(_) | Value::Array(_) => true,
3087 Value::Call { name, .. } if matches!(name.as_str(), "array" | "emptyArray") => true,
3088 Value::Call { name, .. } => self
3089 .compiler
3090 .catalog
3091 .entry(Kind::Value, name)
3092 .and_then(|entry| entry.return_type())
3093 .is_some_and(|return_type| {
3094 return_type.split('|').any(|part| part.trim() == "Array")
3095 }),
3096 _ => false,
3097 }
3098 }
3099
3100 fn push_value(&mut self, value: Value) -> wir::ValueId {
3101 self.wir.values.push(ValueNode::new(value, None))
3102 }
3103
3104 fn push_call(&mut self, name: &str, args: Vec<wir::ValueId>) -> wir::ValueId {
3105 self.push_value(Value::Call {
3106 name: name.to_string(),
3107 args,
3108 })
3109 }
3110
3111 fn lower_custom_string(
3112 &mut self,
3113 value: String,
3114 span: Option<HirSpan>,
3115 ) -> Result<wir::ValueId, IntegrationError> {
3116 let span = self.wir_span(span)?;
3117 let text = self
3118 .wir
3119 .values
3120 .push(ValueNode::new(Value::String(value), span));
3121 Ok(self.wir.values.push(ValueNode::new(
3122 Value::Call {
3123 name: "customString".to_string(),
3124 args: vec![text],
3125 },
3126 span,
3127 )))
3128 }
3129
3130 fn push_number(&mut self, value: f64, text: &str) -> wir::ValueId {
3131 self.push_value(Value::Number {
3132 value,
3133 text: text.to_string(),
3134 })
3135 }
3136
3137 fn fold_numeric_binary(
3138 &self,
3139 op: &str,
3140 left: wir::ValueId,
3141 right: wir::ValueId,
3142 ) -> Option<f64> {
3143 let number = |id| match self.wir.values.get(id)?.value {
3144 Value::Number { value, .. } => Some(value),
3145 _ => None,
3146 };
3147 let left = number(left)?;
3148 let right = number(right)?;
3149 let value = match op {
3150 "+" => left + right,
3151 "-" => left - right,
3152 "*" => left * right,
3153 "/" if right != 0.0 => left / right,
3154 "%" if right != 0.0 => left % right,
3155 "**" => left.powf(right),
3156 _ => return None,
3157 };
3158 value.is_finite().then_some(value)
3159 }
3160
3161 fn lower_condition(&mut self, expr: &Expr) -> Result<wir::ValueId, IntegrationError> {
3162 let value = self.lower_value(expr)?;
3163 let is_comparison = |expr: &Expr| matches!(expr, Expr::Binary { op, .. } if matches!(op.as_str(), "==" | "!=" | "<" | "<=" | ">" | ">="));
3164 if is_comparison(expr)
3165 || matches!(expr, Expr::Unary { op, operand, .. } if op == "not" && is_comparison(operand))
3166 {
3167 return Ok(value);
3168 }
3169 let true_value = self.wir.values.push(ValueNode::new(
3170 Value::Bool(true),
3171 self.wir_span(expr.span().copied())?,
3172 ));
3173 Ok(self.wir.values.push(ValueNode::new(
3174 Value::Call {
3175 name: "==".to_string(),
3176 args: vec![value, true_value],
3177 },
3178 self.wir_span(expr.span().copied())?,
3179 )))
3180 }
3181
3182 fn lower_assign(
3183 &mut self,
3184 target: &Expr,
3185 value: &Expr,
3186 span: Option<HirSpan>,
3187 ) -> Result<wir::ActionId, IntegrationError> {
3188 let mut indices = Vec::new();
3189 if let Some(root) = indexed_target_parts(target, &mut indices) {
3190 if indices.len() > 3 {
3191 return Err(self.unsupported("Cannot assign to 4d array", target.span().copied()));
3192 }
3193 if indices.len() > 1 {
3194 indices.reverse();
3195 return self.lower_nested_indexed_assign(root, &indices, target, value, span);
3196 }
3197 }
3198 match target {
3199 Expr::GlobalVar {
3200 name,
3201 span: target_span,
3202 } => {
3203 let variable = *self.globals.get(name).ok_or_else(|| {
3204 self.unsupported(format!("unknown global variable '{name}'"), *target_span)
3205 })?;
3206 if let Expr::Binary {
3207 op, left, right, ..
3208 } = value
3209 {
3210 if let Expr::GlobalVar {
3211 name: left_name, ..
3212 } = left.as_ref()
3213 {
3214 if left_name == name {
3215 if let Some(modify_op) = modify_op_from_str(op) {
3216 let val = self.lower_value(right)?;
3217 return Ok(self.wir.actions.push(Action::ModifyGlobalVariable {
3218 variable,
3219 op: modify_op,
3220 value: val,
3221 span: self.wir_span(span)?,
3222 target_span: self.wir_span(*target_span)?,
3223 }));
3224 }
3225 }
3226 }
3227 }
3228 let val = self.lower_value(value)?;
3229 Ok(self.wir.actions.push(Action::SetGlobalVariable {
3230 variable,
3231 value: val,
3232 span: self.wir_span(span)?,
3233 target_span: self.wir_span(*target_span)?,
3234 }))
3235 }
3236 Expr::PlayerVar {
3237 player,
3238 name,
3239 span: target_span,
3240 ..
3241 } => {
3242 let variable = *self.players.get(name).ok_or_else(|| {
3243 self.unsupported(format!("unknown player variable '{name}'"), *target_span)
3244 })?;
3245 let player_val = self.lower_value(player)?;
3246 if let Expr::Binary {
3247 op, left, right, ..
3248 } = value
3249 {
3250 if let Expr::PlayerVar {
3251 player: left_player,
3252 name: left_name,
3253 ..
3254 } = left.as_ref()
3255 {
3256 if left_name == name && left_player.as_ref() == player.as_ref() {
3257 if let Some(modify_op) = modify_op_from_str(op) {
3258 let val = self.lower_value(right)?;
3259 return Ok(self.wir.actions.push(Action::ModifyPlayerVariable {
3260 player: player_val,
3261 variable,
3262 op: modify_op,
3263 value: val,
3264 span: self.wir_span(span)?,
3265 target_span: self.wir_span(*target_span)?,
3266 }));
3267 }
3268 }
3269 }
3270 }
3271 let val = self.lower_value(value)?;
3272 Ok(self.wir.actions.push(Action::SetPlayerVariable {
3273 player: player_val,
3274 variable,
3275 value: val,
3276 span: self.wir_span(span)?,
3277 target_span: self.wir_span(*target_span)?,
3278 }))
3279 }
3280 Expr::Index {
3281 array,
3282 index,
3283 span: target_span,
3284 } => match array.as_ref() {
3285 Expr::GlobalVar {
3286 name,
3287 span: arr_span,
3288 } => {
3289 let variable = *self.globals.get(name).ok_or_else(|| {
3290 self.unsupported(format!("unknown global variable '{name}'"), *arr_span)
3291 })?;
3292 let var_node = self.wir.values.push(ValueNode::new(
3293 Value::GlobalVariable(variable),
3294 self.wir_span(*arr_span)?,
3295 ));
3296 let index_val = self.lower_value(index)?;
3297 if let Expr::Binary {
3298 op, left, right, ..
3299 } = value
3300 {
3301 if let Expr::Index {
3302 array: left_arr,
3303 index: left_idx,
3304 ..
3305 } = left.as_ref()
3306 {
3307 if left_arr.as_ref() == array.as_ref()
3308 && left_idx.as_ref() == index.as_ref()
3309 {
3310 if let Some(op_id) = modify_catalog_name_from_str(op) {
3311 let op_node = self.wir.values.push(ValueNode::new(
3312 Value::Call {
3313 name: op_id.to_string(),
3314 args: Vec::new(),
3315 },
3316 None,
3317 ));
3318 let right_val = self.lower_value(right)?;
3319 return Ok(self.wir.actions.push(Action::Call {
3320 name: "modifyGlobalVariableAtIndex".to_string(),
3321 args: vec![var_node, index_val, op_node, right_val],
3322 span: self.wir_span(span)?,
3323 }));
3324 }
3325 }
3326 }
3327 }
3328 let val = self.lower_value(value)?;
3329 Ok(self.wir.actions.push(Action::Call {
3330 name: "setGlobalVariableAtIndex".to_string(),
3331 args: vec![var_node, index_val, val],
3332 span: self.wir_span(span)?,
3333 }))
3334 }
3335 Expr::PlayerVar {
3336 player,
3337 name,
3338 span: arr_span,
3339 ..
3340 } => {
3341 let player_val = self.lower_value(player)?;
3342 let variable = *self.players.get(name).ok_or_else(|| {
3343 self.unsupported(format!("unknown player variable '{name}'"), *arr_span)
3344 })?;
3345 let var_node = self.wir.values.push(ValueNode::new(
3346 Value::PlayerVariable {
3347 player: player_val,
3348 variable,
3349 },
3350 self.wir_span(*arr_span)?,
3351 ));
3352 let index_val = self.lower_value(index)?;
3353 if let Expr::Binary {
3354 op, left, right, ..
3355 } = value
3356 {
3357 if let Expr::Index {
3358 array: left_arr,
3359 index: left_idx,
3360 ..
3361 } = left.as_ref()
3362 {
3363 if left_arr.as_ref() == array.as_ref()
3364 && left_idx.as_ref() == index.as_ref()
3365 {
3366 if let Some(op_id) = modify_catalog_name_from_str(op) {
3367 let op_node = self.wir.values.push(ValueNode::new(
3368 Value::Call {
3369 name: op_id.to_string(),
3370 args: Vec::new(),
3371 },
3372 None,
3373 ));
3374 let right_val = self.lower_value(right)?;
3375 return Ok(self.wir.actions.push(Action::Call {
3380 name: "modifyPlayerVariableAtIndex".to_string(),
3381 args: vec![var_node, index_val, op_node, right_val],
3382 span: self.wir_span(span)?,
3383 }));
3384 }
3385 }
3386 }
3387 }
3388 let val = self.lower_value(value)?;
3389 Ok(self.wir.actions.push(Action::Call {
3393 name: "setPlayerVariableAtIndex".to_string(),
3394 args: vec![var_node, index_val, val],
3395 span: self.wir_span(span)?,
3396 }))
3397 }
3398 _ => Err(self.unsupported(
3399 "indexing assignment is only representable for global or player variables",
3400 *target_span,
3401 )),
3402 },
3403 _ => Err(self.unsupported(
3404 "only global-variable, player-variable, or index assignment is currently representable in canonical WIR",
3405 span,
3406 )),
3407 }
3408 }
3409
3410 fn lower_nested_indexed_assign(
3411 &mut self,
3412 root: &Expr,
3413 indices: &[&Expr],
3414 target: &Expr,
3415 value: &Expr,
3416 span: Option<HirSpan>,
3417 ) -> Result<wir::ActionId, IntegrationError> {
3418 let (action_name, root_value) = match root {
3419 Expr::GlobalVar {
3420 name,
3421 span: target_span,
3422 } => {
3423 let variable = *self.globals.get(name).ok_or_else(|| {
3424 self.unsupported(format!("unknown global variable '{name}'"), *target_span)
3425 })?;
3426 let root_value = self.wir.values.push(ValueNode::new(
3427 Value::GlobalVariable(variable),
3428 self.wir_span(*target_span)?,
3429 ));
3430 ("setGlobalVariableAtIndex", root_value)
3431 }
3432 Expr::PlayerVar {
3433 player,
3434 name,
3435 span: target_span,
3436 ..
3437 } => {
3438 let player_value = self.lower_value(player)?;
3439 let variable = *self.players.get(name).ok_or_else(|| {
3440 self.unsupported(format!("unknown player variable '{name}'"), *target_span)
3441 })?;
3442 let root_value = self.wir.values.push(ValueNode::new(
3443 Value::PlayerVariable {
3444 player: player_value,
3445 variable,
3446 },
3447 self.wir_span(*target_span)?,
3448 ));
3449 ("setPlayerVariableAtIndex", root_value)
3450 }
3451 _ => {
3452 return Err(self.unsupported(
3453 "indexing assignment is only representable for global or player variables",
3454 target.span().copied(),
3455 ));
3456 }
3457 };
3458
3459 let outer_index = self.lower_value(indices[0])?;
3460 let outer_array = self.lower_indexed_read(root_value, indices[0], outer_index)?;
3461 let replacement =
3462 self.rebuild_indexed_value(outer_array, &indices[1..], target, value, span)?;
3463 Ok(self.wir.actions.push(Action::Call {
3464 name: action_name.to_string(),
3465 args: vec![root_value, outer_index, replacement],
3466 span: self.wir_span(span)?,
3467 }))
3468 }
3469
3470 fn rebuild_indexed_value(
3471 &mut self,
3472 array: wir::ValueId,
3473 indices: &[&Expr],
3474 target: &Expr,
3475 value: &Expr,
3476 span: Option<HirSpan>,
3477 ) -> Result<wir::ValueId, IntegrationError> {
3478 let index = indices
3479 .first()
3480 .copied()
3481 .expect("nested indexed assignment has an inner index");
3482 let index_value = self.lower_value(index)?;
3483 let replacement = if indices.len() == 1 {
3484 if let Expr::Binary {
3485 op, left, right, ..
3486 } = value
3487 && left.as_ref() == target
3488 && let Some(call_name) = modify_catalog_name_from_str(op)
3489 {
3490 let current = self.lower_indexed_read(array, index, index_value)?;
3491 let right = self.lower_value(right)?;
3492 self.push_call(call_name, vec![current, right])
3493 } else {
3494 self.lower_value(value)?
3495 }
3496 } else {
3497 let child = self.lower_indexed_read(array, index, index_value)?;
3498 self.rebuild_indexed_value(child, &indices[1..], target, value, span)?
3499 };
3500 self.replace_array_element(array, index_value, replacement, span)
3501 }
3502
3503 fn lower_indexed_read(
3504 &mut self,
3505 array: wir::ValueId,
3506 index: &Expr,
3507 index_value: wir::ValueId,
3508 ) -> Result<wir::ValueId, IntegrationError> {
3509 if matches!(index, Expr::Number { value, .. } if *value == 0.0) {
3510 Ok(self.push_call("firstOf", vec![array]))
3511 } else {
3512 Ok(self.push_call("valueInArray", vec![array, index_value]))
3513 }
3514 }
3515
3516 fn replace_array_element(
3517 &mut self,
3518 array: wir::ValueId,
3519 index: wir::ValueId,
3520 replacement: wir::ValueId,
3521 span: Option<HirSpan>,
3522 ) -> Result<wir::ValueId, IntegrationError> {
3523 let zero = self.push_number(0.0, "0");
3524 let one = self.push_number(1.0, "1");
3525 let end = self.push_call("add", vec![index, one]);
3526 let maximum = self.push_number(999_999_999_999.0, "999999999999");
3527 let prefix = self.push_call("slice", vec![array, zero, index]);
3528 let middle = self.lower_array(vec![replacement], span)?;
3529 let suffix = self.push_call("slice", vec![array, end, maximum]);
3530 let with_replacement = self.push_call("appendToArray", vec![prefix, middle]);
3531 Ok(self.push_call("appendToArray", vec![with_replacement, suffix]))
3532 }
3533
3534 fn lower_action_call(
3535 &mut self,
3536 name: &str,
3537 args: &[Expr],
3538 span: Option<HirSpan>,
3539 ) -> Result<wir::ActionId, IntegrationError> {
3540 if name == "chaseAtRate" {
3541 let args = args
3542 .iter()
3543 .map(|expr| self.lower_value(expr))
3544 .collect::<Result<Vec<_>, _>>()?;
3545 return Ok(self.wir.actions.push(Action::Call {
3546 name: name.to_string(),
3547 args,
3548 span: self.wir_span(span)?,
3549 }));
3550 }
3551 let function = self
3552 .compiler
3553 .manifest
3554 .resolve_function(name)
3555 .ok_or_else(|| self.unsupported(format!("unknown action '{name}'"), span))?;
3556 if !matches!(function.kind, FunctionKind::Action) {
3557 return Err(self.unsupported(format!("'{name}' is not a generic OPY action"), span));
3558 }
3559 if matches!(
3560 function.id.as_str(),
3561 "hudHeader" | "hudSubheader" | "hudSubtext"
3562 ) {
3563 let text_slot = match function.id.as_str() {
3564 "hudHeader" => 1,
3565 "hudSubheader" => 2,
3566 "hudSubtext" => 3,
3567 _ => unreachable!(),
3568 };
3569 return self.lower_hud_text(args, span, text_slot, &function.id);
3570 }
3571 if function.id == "createDummy" && args.len() == 4 {
3572 let mut lowered = args
3573 .iter()
3574 .map(|expr| self.lower_value(expr))
3575 .collect::<Result<Vec<_>, _>>()?;
3576 let mut zero_vector = Vec::with_capacity(3);
3577 for value in [0.0, 0.0, 0.0] {
3578 zero_vector.push(self.push_value(Value::Number {
3579 value,
3580 text: "0".to_string(),
3581 }));
3582 }
3583 lowered.push(self.push_call("vector", zero_vector));
3584 return Ok(self.wir.actions.push(Action::Call {
3585 name: "createDummyBot".to_string(),
3586 args: lowered,
3587 span: self.wir_span(span)?,
3588 }));
3589 }
3590 let catalog_id = function.catalog_id.as_ref().ok_or_else(|| {
3591 self.unsupported(
3592 format!(
3593 "action '{}' requires a special lowering not in #46",
3594 function.id
3595 ),
3596 span,
3597 )
3598 })?;
3599 let args = args
3600 .iter()
3601 .map(|expr| self.lower_value(expr))
3602 .collect::<Result<Vec<_>, _>>()?;
3603 Ok(self.wir.actions.push(Action::Call {
3604 name: catalog_id.clone(),
3605 args,
3606 span: self.wir_span(span)?,
3607 }))
3608 }
3609
3610 fn lower_hud_text(
3611 &mut self,
3612 args: &[Expr],
3613 span: Option<HirSpan>,
3614 text_slot: usize,
3615 function_name: &str,
3616 ) -> Result<wir::ActionId, IntegrationError> {
3617 let [
3618 visible_to,
3619 text,
3620 position,
3621 sort_order,
3622 color,
3623 reevaluation,
3624 spectators,
3625 ] = args
3626 else {
3627 return Err(self.unsupported(
3628 format!("{function_name} requires exactly seven bound arguments"),
3629 span,
3630 ));
3631 };
3632 let visible_to = self.lower_hud_visible_to(visible_to)?;
3633 let mut text_slots = [
3634 self.push_value(Value::Null),
3635 self.push_value(Value::Null),
3636 self.push_value(Value::Null),
3637 ];
3638 let text_value = self.lower_text_value(text)?;
3639 text_slots[text_slot - 1] = self.push_call("customString", vec![text_value]);
3640 let mut colors = [
3641 self.push_value(Value::Null),
3642 self.push_value(Value::Null),
3643 self.push_value(Value::Null),
3644 ];
3645 colors[text_slot - 1] = self.lower_value(color)?;
3646 let args = vec![
3647 visible_to,
3648 text_slots[0],
3649 text_slots[1],
3650 text_slots[2],
3651 self.lower_value(position)?,
3652 self.lower_value(sort_order)?,
3653 colors[0],
3654 colors[1],
3655 colors[2],
3656 self.lower_value(reevaluation)?,
3657 self.lower_value(spectators)?,
3658 ];
3659 Ok(self.wir.actions.push(Action::Call {
3660 name: "createHudText".to_string(),
3661 args,
3662 span: self.wir_span(span)?,
3663 }))
3664 }
3665
3666 fn lower_hud_visible_to(&mut self, expr: &Expr) -> Result<wir::ValueId, IntegrationError> {
3667 if let Expr::Call { name, args, .. } = expr {
3668 if name == "getAllPlayers" && args.is_empty() {
3669 let all_teams = self.push_value(Value::Enum {
3670 value_type: "Team".to_string(),
3671 value: "ALL".to_string(),
3672 });
3673 return Ok(self.push_call("allPlayers", vec![all_teams]));
3674 }
3675 }
3676 self.lower_value(expr)
3677 }
3678
3679 fn lower_receiver_action_call(
3680 &mut self,
3681 receiver: &Expr,
3682 name: &str,
3683 args: &[Expr],
3684 span: Option<HirSpan>,
3685 ) -> Result<wir::ActionId, IntegrationError> {
3686 let function = self
3687 .compiler
3688 .manifest
3689 .resolve_member(name)
3690 .ok_or_else(|| self.unsupported(format!("unknown member action '{name}'"), span))?;
3691 if !matches!(function.kind, FunctionKind::MemberAction) {
3692 return Err(self.unsupported(format!("'{name}' is not a member action"), span));
3693 }
3694
3695 if function.id == "append" {
3698 let [value] = args else {
3699 return Err(self.unsupported("append requires exactly one argument", span));
3700 };
3701 let value = self.lower_value(value)?;
3702 return match receiver {
3703 Expr::GlobalVar {
3704 name,
3705 span: target_span,
3706 } => {
3707 let variable = *self.globals.get(name).ok_or_else(|| {
3708 self.unsupported(format!("unknown global variable '{name}'"), *target_span)
3709 })?;
3710 Ok(self.wir.actions.push(Action::ModifyGlobalVariable {
3711 variable,
3712 op: wir::ModifyOp::AppendToArray,
3713 value,
3714 span: self.wir_span(span)?,
3715 target_span: self.wir_span(*target_span)?,
3716 }))
3717 }
3718 Expr::PlayerVar {
3719 player,
3720 name,
3721 span: target_span,
3722 ..
3723 } => {
3724 let variable = *self.players.get(name).ok_or_else(|| {
3725 self.unsupported(format!("unknown player variable '{name}'"), *target_span)
3726 })?;
3727 let player = self.lower_value(player)?;
3728 Ok(self.wir.actions.push(Action::ModifyPlayerVariable {
3729 player,
3730 variable,
3731 op: wir::ModifyOp::AppendToArray,
3732 value,
3733 span: self.wir_span(span)?,
3734 target_span: self.wir_span(*target_span)?,
3735 }))
3736 }
3737 _ => Err(self.unsupported(
3738 "append requires a global or player variable receiver",
3739 receiver.span().copied().or(span),
3740 )),
3741 };
3742 }
3743
3744 let catalog_id = function.catalog_id.as_ref().ok_or_else(|| {
3745 self.unsupported(
3746 format!(
3747 "member action '{}' has no canonical catalog identity",
3748 function.id
3749 ),
3750 span,
3751 )
3752 })?;
3753 let mut lowered = Vec::with_capacity(args.len() + 1);
3754 lowered.push(self.lower_value(receiver)?);
3755 lowered.extend(
3756 args.iter()
3757 .map(|arg| self.lower_value(arg))
3758 .collect::<Result<Vec<_>, _>>()?,
3759 );
3760 Ok(self.wir.actions.push(Action::Call {
3761 name: catalog_id.clone(),
3762 args: lowered,
3763 span: self.wir_span(span)?,
3764 }))
3765 }
3766
3767 fn lower_value(&mut self, expr: &Expr) -> Result<wir::ValueId, IntegrationError> {
3768 let span = expr.span().copied();
3769 let value = match expr {
3770 Expr::Number { value, text, .. } => Value::Number {
3771 value: *value,
3772 text: canonical_number_text(*value, text),
3773 },
3774 Expr::String { value, .. } => {
3775 return self.lower_custom_string(value.clone(), span);
3776 }
3777 Expr::Bool { value, .. } => Value::Bool(*value),
3778 Expr::Null { .. } => Value::Null,
3779 Expr::Local { name, .. } => {
3780 let binding = self.array_bindings.iter().rev().find(|binding| {
3781 binding.element == *name || binding.index.as_deref() == Some(name)
3782 });
3783 match binding {
3784 Some(binding) if binding.element == *name => {
3785 return Ok(self.push_call("currentArrayElement", Vec::new()));
3786 }
3787 Some(_) => return Ok(self.push_call("currentArrayIndex", Vec::new())),
3788 None => {
3789 return Err(self.unsupported(
3790 format!("local '{name}' is not inside a supported array callback"),
3791 span,
3792 ));
3793 }
3794 }
3795 }
3796 Expr::Type { .. } => {
3797 return Err(self.unsupported(
3798 "type expressions are only valid as createWorkshopSetting type arguments",
3799 span,
3800 ));
3801 }
3802 Expr::GlobalVar { name, .. } => {
3803 let id = *self.globals.get(name).ok_or_else(|| {
3804 self.unsupported(format!("unknown global variable '{name}'"), span)
3805 })?;
3806 Value::GlobalVariable(id)
3807 }
3808 Expr::PlayerVar { player, name, .. } => {
3809 let player = self.lower_value(player)?;
3810 let id = *self.players.get(name).ok_or_else(|| {
3811 self.unsupported(format!("unknown player variable '{name}'"), span)
3812 })?;
3813 Value::PlayerVariable {
3814 player,
3815 variable: id,
3816 }
3817 }
3818 Expr::EventPlayer { .. } => Value::EventPlayer,
3819 Expr::HostPlayer { .. } => Value::Call {
3820 name: "hostPlayer".to_string(),
3821 args: Vec::new(),
3822 },
3823 Expr::Enum {
3824 value_type, value, ..
3825 } => {
3826 if self
3827 .compiler
3828 .catalog
3829 .enum_spelling(value_type, &Locale::new("en-US"), value)
3830 .is_none()
3831 {
3832 return Err(self.unsupported(
3833 format!("unknown catalog enum member '{value_type}.{value}'"),
3834 span,
3835 ));
3836 }
3837 Value::Enum {
3838 value_type: value_type.clone(),
3839 value: value.clone(),
3840 }
3841 }
3842 Expr::Array { elements, .. } => {
3843 let elements = elements
3844 .iter()
3845 .map(|element| self.lower_value(element))
3846 .collect::<Result<Vec<_>, _>>()?;
3847 return self.lower_array(elements, span);
3848 }
3849 Expr::Vector { x, y, z, .. } => Value::Call {
3850 name: "vector".to_string(),
3851 args: vec![
3852 self.lower_value(x)?,
3853 self.lower_value(y)?,
3854 self.lower_value(z)?,
3855 ],
3856 },
3857 Expr::Constant { name, .. } => {
3858 let const_expr = *self
3859 .constants
3860 .get(name)
3861 .ok_or_else(|| self.unsupported(format!("unknown constant '{name}'"), span))?;
3862 return self.lower_value(const_expr);
3863 }
3864 Expr::Index { array, index, .. } => {
3865 if let Expr::Dict { entries, .. } = array.as_ref()
3866 && is_literal_key(index)
3867 && entries.iter().all(|entry| is_literal_key(&entry.key))
3868 {
3869 if let Some(value) = entries
3870 .iter()
3871 .find(|entry| literal_key_matches(&entry.key, index))
3872 .map(|entry| &entry.value)
3873 {
3874 return self.lower_value(value);
3875 }
3876 return Ok(self
3877 .wir
3878 .values
3879 .push(ValueNode::new(Value::Null, self.wir_span(span)?)));
3880 }
3881 if matches!(index.as_ref(), Expr::Number { value, .. } if *value == 0.0) {
3885 Value::Call {
3886 name: "firstOf".to_string(),
3887 args: vec![self.lower_value(array)?],
3888 }
3889 } else {
3890 Value::Call {
3891 name: "valueInArray".to_string(),
3892 args: vec![self.lower_value(array)?, self.lower_value(index)?],
3893 }
3894 }
3895 }
3896 Expr::Format { text, args, .. } => {
3897 if let Some(value) = fold_literal_format(text, args) {
3898 return self.lower_custom_string(value, span);
3899 }
3900 let text_node = self.wir.values.push(ValueNode::new(
3901 Value::String(canonical_format_text(text)),
3902 self.wir_span(span)?,
3903 ));
3904 let mut call_args = vec![text_node];
3905 for arg in args {
3906 call_args.push(self.lower_value(arg)?);
3907 }
3908 Value::Call {
3909 name: "customString".to_string(),
3910 args: call_args,
3911 }
3912 }
3913 Expr::Conditional {
3914 then_value,
3915 condition,
3916 else_value,
3917 ..
3918 } => Value::Call {
3919 name: "ifThenElse".to_string(),
3920 args: vec![
3921 self.lower_value(condition)?,
3922 self.lower_value(then_value)?,
3923 self.lower_value(else_value)?,
3924 ],
3925 },
3926 Expr::Binary {
3927 op, left, right, ..
3928 } => {
3929 let left = self.lower_value(left)?;
3930 let right = self.lower_value(right)?;
3931 if let Some(value) = self.fold_numeric_binary(op, left, right) {
3932 Value::Number {
3933 value,
3934 text: computed_number_text(value),
3935 }
3936 } else {
3937 match op.as_str() {
3938 "==" | "!=" | "<" | "<=" | ">" | ">=" => Value::Call {
3939 name: op.clone(),
3940 args: vec![left, right],
3941 },
3942 "+" => Value::Call {
3943 name: "add".to_string(),
3944 args: vec![left, right],
3945 },
3946 "-" => Value::Call {
3947 name: "subtract".to_string(),
3948 args: vec![left, right],
3949 },
3950 "*" => Value::Call {
3951 name: "multiply".to_string(),
3952 args: vec![left, right],
3953 },
3954 "/" => Value::Call {
3955 name: "divide".to_string(),
3956 args: vec![left, right],
3957 },
3958 "%" => Value::Call {
3959 name: "modulo".to_string(),
3960 args: vec![left, right],
3961 },
3962 "**" => Value::Call {
3963 name: "raiseToPower".to_string(),
3964 args: vec![left, right],
3965 },
3966 "and" => Value::Call {
3967 name: "and".to_string(),
3968 args: vec![left, right],
3969 },
3970 "or" => Value::Call {
3971 name: "or".to_string(),
3972 args: vec![left, right],
3973 },
3974 "in" => Value::Call {
3975 name: "arrayContains".to_string(),
3976 args: vec![right, left],
3977 },
3978 "not in" => {
3979 let wir_span = self.wir_span(span)?;
3980 let contains = self.wir.values.push(ValueNode::new(
3981 Value::Call {
3982 name: "arrayContains".to_string(),
3983 args: vec![right, left],
3984 },
3985 wir_span,
3986 ));
3987 Value::Call {
3988 name: "not".to_string(),
3989 args: vec![contains],
3990 }
3991 }
3992 _ => {
3993 return Err(self.unsupported(
3994 format!(
3995 "binary operator '{op}' is not currently representable in canonical WIR"
3996 ),
3997 span,
3998 ));
3999 }
4000 }
4001 }
4002 }
4003 Expr::Unary { op, operand, .. } => match op.as_str() {
4004 "not" => {
4005 if let Expr::Binary {
4010 op: comparison,
4011 left,
4012 right,
4013 ..
4014 } = operand.as_ref()
4015 {
4016 if let Some(negated) = negated_comparison(comparison) {
4017 Value::Call {
4018 name: negated.to_string(),
4019 args: vec![self.lower_value(left)?, self.lower_value(right)?],
4020 }
4021 } else {
4022 Value::Call {
4023 name: "not".to_string(),
4024 args: vec![self.lower_value(operand)?],
4025 }
4026 }
4027 } else {
4028 Value::Call {
4029 name: "not".to_string(),
4030 args: vec![self.lower_value(operand)?],
4031 }
4032 }
4033 }
4034 "-" => Value::Call {
4035 name: "-".to_string(),
4036 args: vec![self.lower_value(operand)?],
4037 },
4038 "+" => return self.lower_value(operand),
4039 _ => {
4040 return Err(self.unsupported(
4041 format!(
4042 "unary operator '{op}' is not currently representable in canonical WIR"
4043 ),
4044 span,
4045 ));
4046 }
4047 },
4048 Expr::Call { name, args, .. } => {
4049 if name == "createWorkshopSetting" {
4050 return self.lower_workshop_setting(args, span);
4051 }
4052 if matches!(name.as_str(), "attacker" | "victim") && args.is_empty() {
4053 return Ok(self.push_call(name, Vec::new()));
4054 }
4055 if name == "localPlayer" && args.is_empty() {
4056 return Ok(self.push_call(name, Vec::new()));
4057 }
4058 if name == "ruleCondition" {
4059 if !args.is_empty() {
4060 return Err(
4061 self.unsupported("ruleCondition does not accept arguments", span)
4062 );
4063 }
4064 let conditions = self.current_rule_conditions.clone().ok_or_else(|| {
4065 self.unsupported("ruleCondition is only valid inside a rule", span)
4066 })?;
4067 let Some((first, rest)) = conditions.split_first() else {
4068 return Ok(self.push_value(Value::Bool(true)));
4069 };
4070 let mut combined = *first;
4071 for condition in rest {
4072 combined = self.push_call("and", vec![combined, *condition]);
4073 }
4074 return Ok(combined);
4075 }
4076 if name == "vect" && args.len() == 3 {
4077 Value::Vector {
4078 x: self.lower_value(&args[0])?,
4079 y: self.lower_value(&args[1])?,
4080 z: self.lower_value(&args[2])?,
4081 }
4082 } else if matches!(name.as_str(), "all" | "any") {
4083 let call_name = if name == "all" {
4084 "isTrueForAll"
4085 } else {
4086 "isTrueForAny"
4087 };
4088 let [array] = args.as_slice() else {
4089 return Err(self.unsupported(
4090 format!("{name} requires exactly one array argument"),
4091 span,
4092 ));
4093 };
4094 let (array, condition) = match array {
4095 Expr::Comprehension {
4096 element,
4097 variable,
4098 index,
4099 iterable,
4100 ..
4101 } => {
4102 if index.is_some() {
4103 return Err(self.unsupported(
4104 format!("{name} does not support an index binder"),
4105 span,
4106 ));
4107 }
4108 let iterable = self.lower_value(iterable)?;
4109 self.array_bindings.push(ArrayBinding {
4110 element: variable.clone(),
4111 index: None,
4112 });
4113 let condition = self.lower_value(element);
4114 self.array_bindings.pop();
4115 (iterable, condition?)
4116 }
4117 array => (
4118 self.lower_value(array)?,
4119 self.push_call("currentArrayElement", Vec::new()),
4120 ),
4121 };
4122 Value::Call {
4123 name: call_name.to_string(),
4124 args: vec![array, condition],
4125 }
4126 } else if matches!(name.as_str(), "ceil" | "floor" | "round") {
4127 let [value] = args.as_slice() else {
4128 return Err(self.unsupported(
4129 format!("{name} requires exactly one numeric argument"),
4130 span,
4131 ));
4132 };
4133 let rounding = match name.as_str() {
4134 "ceil" => "UP",
4135 "floor" => "DOWN",
4136 "round" => "NEAREST",
4137 _ => unreachable!(),
4138 };
4139 let rounding = self.push_value(Value::Enum {
4140 value_type: "Rounding".to_string(),
4141 value: rounding.to_string(),
4142 });
4143 Value::Call {
4144 name: "roundToInteger".to_string(),
4145 args: vec![self.lower_value(value)?, rounding],
4146 }
4147 } else if name == "sorted" {
4148 let (array, key) = match args.as_slice() {
4149 [array] => (
4150 self.lower_value(array)?,
4151 self.push_call("currentArrayElement", Vec::new()),
4152 ),
4153 [
4154 array,
4155 Expr::Lambda {
4156 params, body, span, ..
4157 },
4158 ] => {
4159 let array = self.lower_value(array)?;
4160 let key = self.lower_array_callback(params, body, *span)?;
4161 (array, key)
4162 }
4163 _ => {
4164 return Err(self.unsupported(
4165 "sorted requires an array and an optional lambda key",
4166 span,
4167 ));
4168 }
4169 };
4170 Value::Call {
4171 name: "sortedArray".to_string(),
4172 args: vec![array, key],
4173 }
4174 } else {
4175 let function = self
4176 .compiler
4177 .manifest
4178 .resolve_function(name)
4179 .ok_or_else(|| self.unsupported(format!("unknown value '{name}'"), span))?;
4180 if !matches!(function.kind, FunctionKind::Value) {
4181 return Err(
4182 self.unsupported(format!("'{name}' is not a generic OPY value"), span)
4183 );
4184 }
4185 let catalog_id = function.catalog_id.as_ref().ok_or_else(|| {
4186 self.unsupported(
4187 format!(
4188 "value '{}' requires a special lowering not in #46",
4189 function.id
4190 ),
4191 span,
4192 )
4193 })?;
4194 Value::Call {
4195 name: catalog_id.clone(),
4196 args: args
4197 .iter()
4198 .map(|arg| self.lower_value(arg))
4199 .collect::<Result<Vec<_>, _>>()?,
4200 }
4201 }
4202 }
4203 Expr::ReceiverCall {
4204 receiver,
4205 name,
4206 args,
4207 ..
4208 } => {
4209 let function = self.compiler.manifest.resolve_member(name).ok_or_else(|| {
4210 self.unsupported(format!("unknown member value '{name}'"), span)
4211 })?;
4212 if !matches!(function.kind, FunctionKind::MemberValue) {
4213 return Err(self.unsupported(format!("'{name}' is not a member value"), span));
4214 }
4215 if matches!(function.id.as_str(), "concat" | "exclude") {
4216 let [value] = args.as_slice() else {
4217 return Err(self.unsupported(
4218 format!("{} requires exactly one argument", function.id),
4219 span,
4220 ));
4221 };
4222 Value::Call {
4223 name: if function.id == "concat" {
4224 "appendToArray"
4225 } else {
4226 "removeFromArray"
4227 }
4228 .to_string(),
4229 args: vec![self.lower_value(receiver)?, self.lower_value(value)?],
4230 }
4231 } else {
4232 let catalog_id = function.catalog_id.as_ref().ok_or_else(|| {
4233 self.unsupported(
4234 format!(
4235 "member value '{}' has no canonical catalog identity",
4236 function.id
4237 ),
4238 span,
4239 )
4240 })?;
4241 let mut lowered = Vec::with_capacity(args.len() + 1);
4242 lowered.push(self.lower_value(receiver)?);
4243 lowered.extend(
4244 args.iter()
4245 .map(|arg| self.lower_value(arg))
4246 .collect::<Result<Vec<_>, _>>()?,
4247 );
4248 Value::Call {
4249 name: catalog_id.clone(),
4250 args: lowered,
4251 }
4252 }
4253 }
4254 Expr::Member {
4255 receiver, member, ..
4256 } => {
4257 let receiver = self.lower_value(receiver)?;
4258 if let Some(name) = match member.as_str() {
4259 "x" => Some("__xComponentOf__"),
4260 "y" => Some("__yComponentOf__"),
4261 "z" => Some("__zComponentOf__"),
4262 _ => None,
4263 } {
4264 Value::Call {
4265 name: name.to_string(),
4266 args: vec![receiver],
4267 }
4268 } else {
4269 let member = self.wir.values.push(ValueNode::new(
4270 Value::String(member.clone()),
4271 self.wir_span(span)?,
4272 ));
4273 Value::Call {
4274 name: "memberAccess".to_string(),
4275 args: vec![receiver, member],
4276 }
4277 }
4278 }
4279 Expr::Comprehension {
4280 element,
4281 variable,
4282 index,
4283 iterable,
4284 condition,
4285 span: comprehension_span,
4286 ..
4287 } => {
4288 if condition.is_some() && index.is_some() {
4289 return Err(self.unsupported(
4290 "comprehensions with both a filter and an index binder are not currently representable in canonical WIR",
4291 *comprehension_span,
4292 ));
4293 }
4294 let iterable = self.lower_value(iterable)?;
4295 let binding = ArrayBinding {
4296 element: variable.clone(),
4297 index: index.clone(),
4298 };
4299 self.array_bindings.push(binding);
4300 let predicate = condition
4301 .as_deref()
4302 .map(|condition| self.lower_value(condition));
4303 let element = self.lower_value(element);
4304 self.array_bindings.pop();
4305 let element = element?;
4306 let iterable = if let Some(predicate) = predicate {
4307 let predicate = predicate?;
4308 self.push_call("filteredArray", vec![iterable, predicate])
4309 } else {
4310 iterable
4311 };
4312 Value::Call {
4313 name: "mappedArray".to_string(),
4314 args: vec![iterable, element],
4315 }
4316 }
4317 Expr::Lambda { span, .. } => {
4318 return Err(self.unsupported(
4319 "lambda expressions are only representable as supported array operation arguments",
4320 *span,
4321 ));
4322 }
4323 Expr::StringModifier {
4324 modifier,
4325 value,
4326 span,
4327 } => {
4328 let value = match modifier.as_str() {
4329 "b" => big_letters(value),
4330 "c" => case_sensitive(value),
4331 "w" => fullwidth(value),
4332 _ => {
4333 return Err(self.unsupported(
4334 format!(
4335 "string modifier '{modifier}' is not currently representable in canonical WIR"
4336 ),
4337 *span,
4338 ));
4339 }
4340 };
4341 return self.lower_custom_string(value, *span);
4342 }
4343 _ => {
4344 return Err(self.unsupported(
4345 format!(
4346 "expression '{}' is not currently representable in canonical WIR",
4347 expr.kind_name()
4348 ),
4349 span,
4350 ));
4351 }
4352 };
4353 Ok(self
4354 .wir
4355 .values
4356 .push(ValueNode::new(value, self.wir_span(span)?)))
4357 }
4358
4359 fn lower_array_callback(
4360 &mut self,
4361 params: &[String],
4362 body: &Expr,
4363 span: Option<HirSpan>,
4364 ) -> Result<wir::ValueId, IntegrationError> {
4365 if !(1..=2).contains(¶ms.len()) {
4366 return Err(self.unsupported(
4367 "array callbacks require one element parameter and at most one index parameter",
4368 span,
4369 ));
4370 }
4371 if params.windows(2).any(|pair| pair[0] == pair[1]) {
4372 return Err(
4373 self.unsupported("array callback parameters must have distinct names", span)
4374 );
4375 }
4376 self.array_bindings.push(ArrayBinding {
4377 element: params[0].clone(),
4378 index: params.get(1).cloned(),
4379 });
4380 let result = self.lower_value(body);
4381 self.array_bindings.pop();
4382 result
4383 }
4384
4385 fn lower_workshop_setting(
4386 &mut self,
4387 args: &[Expr],
4388 span: Option<HirSpan>,
4389 ) -> Result<wir::ValueId, IntegrationError> {
4390 let [
4391 Expr::Type {
4392 name: setting_type,
4393 args: type_args,
4394 span: type_span,
4395 },
4396 category,
4397 setting_name,
4398 default,
4399 sort_order,
4400 ] = args
4401 else {
4402 return Err(self.unsupported(
4403 "createWorkshopSetting requires a type and four value arguments",
4404 span,
4405 ));
4406 };
4407
4408 let catalog_name = match (setting_type.as_str(), type_args.as_slice()) {
4409 ("bool", []) => "createWorkshopSettingBool",
4410 ("int", [_, _]) => "createWorkshopSettingInt",
4411 ("float", [_, _]) => "createWorkshopSettingFloat",
4412 ("int", []) | ("float", []) => {
4413 return Err(self.unsupported(
4414 format!("createWorkshopSetting type '{setting_type}' requires a numeric range"),
4415 type_span.or(span),
4416 ));
4417 }
4418 _ => {
4419 return Err(self.unsupported(
4420 format!("unsupported createWorkshopSetting type '{setting_type}'"),
4421 type_span.or(span),
4422 ));
4423 }
4424 };
4425
4426 let category = match category {
4429 Expr::String { value, .. } if value.is_empty() => {
4430 self.push_value(Value::String("\u{3000}".to_string()))
4431 }
4432 _ => self.lower_value(category)?,
4433 };
4434 let mut lowered = vec![
4435 category,
4436 self.lower_value(setting_name)?,
4437 self.lower_value(default)?,
4438 ];
4439 if let [minimum, maximum] = type_args.as_slice() {
4440 lowered.push(self.lower_value(minimum)?);
4441 lowered.push(self.lower_value(maximum)?);
4442 }
4443 lowered.push(self.lower_value(sort_order)?);
4444 Ok(self.push_call(catalog_name, lowered))
4445 }
4446
4447 fn wir_span(&self, span: Option<HirSpan>) -> Result<Option<WorkshopSpan>, IntegrationError> {
4448 let Some(span) = span else {
4449 return Ok(None);
4450 };
4451 let file = *self.files.get(&span.file).ok_or_else(|| {
4452 IntegrationError::new(
4453 "source-file",
4454 format!("HIR span references unknown source file id {}", span.file),
4455 Some(span),
4456 )
4457 })?;
4458 Ok(Some(WorkshopSpan::new(
4459 file,
4460 WorkshopPosition::new(span.start.line, span.start.col),
4461 WorkshopPosition::new(span.end.line, span.end.col),
4462 )))
4463 }
4464
4465 fn hir_span_from_workshop(&self, span: WorkshopSpan) -> Option<HirSpan> {
4466 let file = *self.wir_to_hir_files.get(span.file.index())?;
4467 Some(HirSpan {
4468 file,
4469 start: hir::Position {
4470 line: span.start.line,
4471 col: span.start.col,
4472 },
4473 end: hir::Position {
4474 line: span.end.line,
4475 col: span.end.col,
4476 },
4477 })
4478 }
4479
4480 fn unsupported(&self, message: impl Into<String>, span: Option<HirSpan>) -> IntegrationError {
4481 IntegrationError::new("unsupported-integration-surface", message, span)
4482 }
4483}
4484
4485fn implicit_default_variables(
4489 hir: &hir::Program,
4490) -> (
4491 BTreeMap<String, Option<HirSpan>>,
4492 BTreeMap<String, Option<HirSpan>>,
4493) {
4494 let declared_globals = hir
4495 .declarations
4496 .iter()
4497 .filter_map(|declaration| match declaration {
4498 hir::Declaration::GlobalVariable { name, .. } => Some(name.as_str()),
4499 _ => None,
4500 })
4501 .collect::<HashSet<_>>();
4502 let declared_players = hir
4503 .declarations
4504 .iter()
4505 .filter_map(|declaration| match declaration {
4506 hir::Declaration::PlayerVariable { name, .. } => Some(name.as_str()),
4507 _ => None,
4508 })
4509 .collect::<HashSet<_>>();
4510 let mut globals = BTreeMap::new();
4511 let mut players = BTreeMap::new();
4512 for declaration in &hir.declarations {
4513 let initializer = match declaration {
4514 hir::Declaration::GlobalVariable { initializer, .. }
4515 | hir::Declaration::PlayerVariable { initializer, .. } => initializer.as_ref(),
4516 hir::Declaration::Constant { value, .. } => Some(value),
4517 _ => None,
4518 };
4519 if let Some(expr) = initializer {
4520 collect_implicit_expr(
4521 expr,
4522 &declared_globals,
4523 &declared_players,
4524 &mut globals,
4525 &mut players,
4526 );
4527 }
4528 }
4529 for entry in &hir.rules {
4530 match entry {
4531 RuleEntry::Rule(rule) => {
4532 for condition in &rule.conditions {
4533 collect_implicit_expr(
4534 condition,
4535 &declared_globals,
4536 &declared_players,
4537 &mut globals,
4538 &mut players,
4539 );
4540 }
4541 collect_implicit_stmts(
4542 &rule.actions,
4543 &declared_globals,
4544 &declared_players,
4545 &mut globals,
4546 &mut players,
4547 );
4548 }
4549 RuleEntry::SubroutineDef { body, .. } => collect_implicit_stmts(
4550 body,
4551 &declared_globals,
4552 &declared_players,
4553 &mut globals,
4554 &mut players,
4555 ),
4556 }
4557 }
4558 (globals, players)
4559}
4560
4561fn collect_implicit_stmts(
4562 statements: &[Stmt],
4563 declared_globals: &HashSet<&str>,
4564 declared_players: &HashSet<&str>,
4565 globals: &mut BTreeMap<String, Option<HirSpan>>,
4566 players: &mut BTreeMap<String, Option<HirSpan>>,
4567) {
4568 for statement in statements {
4569 match statement {
4570 Stmt::Expr { expr, .. } => {
4571 collect_implicit_expr(expr, declared_globals, declared_players, globals, players)
4572 }
4573 Stmt::Assign { target, value, .. } => {
4574 collect_implicit_expr(target, declared_globals, declared_players, globals, players);
4575 collect_implicit_expr(value, declared_globals, declared_players, globals, players);
4576 }
4577 Stmt::Delete { target, .. } => {
4578 collect_implicit_expr(target, declared_globals, declared_players, globals, players);
4579 }
4580 Stmt::If {
4581 branches, r#else, ..
4582 } => {
4583 for branch in branches {
4584 collect_implicit_expr(
4585 &branch.condition,
4586 declared_globals,
4587 declared_players,
4588 globals,
4589 players,
4590 );
4591 collect_implicit_stmts(
4592 &branch.body,
4593 declared_globals,
4594 declared_players,
4595 globals,
4596 players,
4597 );
4598 }
4599 if let Some(default_body) = r#else {
4600 collect_implicit_stmts(
4601 default_body,
4602 declared_globals,
4603 declared_players,
4604 globals,
4605 players,
4606 );
4607 }
4608 }
4609 Stmt::For {
4610 variable,
4611 iterable,
4612 body,
4613 ..
4614 } => {
4615 collect_implicit_expr(
4616 variable,
4617 declared_globals,
4618 declared_players,
4619 globals,
4620 players,
4621 );
4622 collect_implicit_expr(
4623 iterable,
4624 declared_globals,
4625 declared_players,
4626 globals,
4627 players,
4628 );
4629 collect_implicit_stmts(body, declared_globals, declared_players, globals, players);
4630 }
4631 Stmt::While {
4632 condition, body, ..
4633 }
4634 | Stmt::DoWhile {
4635 condition, body, ..
4636 } => {
4637 collect_implicit_expr(
4638 condition,
4639 declared_globals,
4640 declared_players,
4641 globals,
4642 players,
4643 );
4644 collect_implicit_stmts(body, declared_globals, declared_players, globals, players);
4645 }
4646 Stmt::Switch { value, arms, .. } => {
4647 collect_implicit_expr(value, declared_globals, declared_players, globals, players);
4648 for arm in arms {
4649 match arm {
4650 SwitchArm::Case { value, body, .. } => {
4651 collect_implicit_expr(
4652 value,
4653 declared_globals,
4654 declared_players,
4655 globals,
4656 players,
4657 );
4658 collect_implicit_stmts(
4659 body,
4660 declared_globals,
4661 declared_players,
4662 globals,
4663 players,
4664 );
4665 }
4666 SwitchArm::Default { body, .. } => {
4667 collect_implicit_stmts(
4668 body,
4669 declared_globals,
4670 declared_players,
4671 globals,
4672 players,
4673 );
4674 }
4675 }
4676 }
4677 }
4678 Stmt::Goto { offset, .. } => {
4679 if let Some(offset) = offset {
4680 collect_implicit_expr(
4681 offset,
4682 declared_globals,
4683 declared_players,
4684 globals,
4685 players,
4686 );
4687 }
4688 }
4689 Stmt::Break { .. }
4690 | Stmt::Return { .. }
4691 | Stmt::Continue { .. }
4692 | Stmt::Label { .. }
4693 | Stmt::CallSubroutine { .. }
4694 | Stmt::Pass { .. } => {}
4695 }
4696 }
4697}
4698
4699fn collect_implicit_expr(
4700 expr: &Expr,
4701 declared_globals: &HashSet<&str>,
4702 declared_players: &HashSet<&str>,
4703 globals: &mut BTreeMap<String, Option<HirSpan>>,
4704 players: &mut BTreeMap<String, Option<HirSpan>>,
4705) {
4706 match expr {
4707 Expr::GlobalVar { name, span } => {
4708 if !declared_globals.contains(name.as_str()) && default_var_index(name).is_some() {
4709 globals.entry(name.clone()).or_insert(*span);
4710 }
4711 }
4712 Expr::Array { elements, .. } => {
4713 for element in elements {
4714 collect_implicit_expr(
4715 element,
4716 declared_globals,
4717 declared_players,
4718 globals,
4719 players,
4720 );
4721 }
4722 }
4723 Expr::Dict { entries, .. } => {
4724 for entry in entries {
4725 collect_implicit_expr(
4726 &entry.key,
4727 declared_globals,
4728 declared_players,
4729 globals,
4730 players,
4731 );
4732 collect_implicit_expr(
4733 &entry.value,
4734 declared_globals,
4735 declared_players,
4736 globals,
4737 players,
4738 );
4739 }
4740 }
4741 Expr::Comprehension {
4742 element,
4743 iterable,
4744 condition,
4745 ..
4746 } => {
4747 collect_implicit_expr(
4748 element,
4749 declared_globals,
4750 declared_players,
4751 globals,
4752 players,
4753 );
4754 collect_implicit_expr(
4755 iterable,
4756 declared_globals,
4757 declared_players,
4758 globals,
4759 players,
4760 );
4761 if let Some(condition) = condition {
4762 collect_implicit_expr(
4763 condition,
4764 declared_globals,
4765 declared_players,
4766 globals,
4767 players,
4768 );
4769 }
4770 }
4771 Expr::Lambda { body, .. } => {
4772 collect_implicit_expr(body, declared_globals, declared_players, globals, players)
4773 }
4774 Expr::Type { args, .. } => {
4775 for arg in args {
4776 collect_implicit_expr(arg, declared_globals, declared_players, globals, players);
4777 }
4778 }
4779 Expr::Vector { x, y, z, .. } => {
4780 collect_implicit_expr(x, declared_globals, declared_players, globals, players);
4781 collect_implicit_expr(y, declared_globals, declared_players, globals, players);
4782 collect_implicit_expr(z, declared_globals, declared_players, globals, players);
4783 }
4784 Expr::PlayerVar {
4785 player,
4786 name,
4787 member_span,
4788 span,
4789 } => {
4790 if !declared_players.contains(name.as_str()) && default_var_index(name).is_some() {
4791 players.entry(name.clone()).or_insert(member_span.or(*span));
4792 }
4793 collect_implicit_expr(player, declared_globals, declared_players, globals, players);
4794 }
4795 Expr::Member {
4796 receiver,
4797 member,
4798 span,
4799 ..
4800 } => {
4801 if !declared_players.contains(member.as_str()) && default_var_index(member).is_some() {
4802 players.entry(member.clone()).or_insert(*span);
4803 }
4804 collect_implicit_expr(
4805 receiver,
4806 declared_globals,
4807 declared_players,
4808 globals,
4809 players,
4810 );
4811 }
4812 Expr::Call { args, .. } | Expr::MacroCall { args, .. } => {
4813 for arg in args {
4814 collect_implicit_expr(arg, declared_globals, declared_players, globals, players);
4815 }
4816 }
4817 Expr::ReceiverCall { receiver, args, .. } => {
4818 collect_implicit_expr(
4819 receiver,
4820 declared_globals,
4821 declared_players,
4822 globals,
4823 players,
4824 );
4825 for arg in args {
4826 collect_implicit_expr(arg, declared_globals, declared_players, globals, players);
4827 }
4828 }
4829 Expr::Binary { left, right, .. } => {
4830 collect_implicit_expr(left, declared_globals, declared_players, globals, players);
4831 collect_implicit_expr(right, declared_globals, declared_players, globals, players);
4832 }
4833 Expr::Conditional {
4834 then_value,
4835 condition,
4836 else_value,
4837 ..
4838 } => {
4839 collect_implicit_expr(
4840 then_value,
4841 declared_globals,
4842 declared_players,
4843 globals,
4844 players,
4845 );
4846 collect_implicit_expr(
4847 condition,
4848 declared_globals,
4849 declared_players,
4850 globals,
4851 players,
4852 );
4853 collect_implicit_expr(
4854 else_value,
4855 declared_globals,
4856 declared_players,
4857 globals,
4858 players,
4859 );
4860 }
4861 Expr::Unary { operand, .. } => collect_implicit_expr(
4862 operand,
4863 declared_globals,
4864 declared_players,
4865 globals,
4866 players,
4867 ),
4868 Expr::Index { array, index, .. } => {
4869 collect_implicit_expr(array, declared_globals, declared_players, globals, players);
4870 collect_implicit_expr(index, declared_globals, declared_players, globals, players);
4871 }
4872 Expr::Format { args, .. } => {
4873 for arg in args {
4874 collect_implicit_expr(arg, declared_globals, declared_players, globals, players);
4875 }
4876 }
4877 Expr::Number { .. }
4878 | Expr::String { .. }
4879 | Expr::Bool { .. }
4880 | Expr::Null { .. }
4881 | Expr::StringModifier { .. }
4882 | Expr::Local { .. }
4883 | Expr::Enum { .. }
4884 | Expr::EventPlayer { .. }
4885 | Expr::HostPlayer { .. }
4886 | Expr::Constant { .. }
4887 | Expr::MacroParam { .. } => {}
4888 }
4889}
4890
4891fn allocate_indices(
4892 entries: &[(Option<u32>, Option<HirSpan>)],
4893 pre_reserved: &HashSet<u32>,
4894 kind: &str,
4895) -> Result<Vec<u32>, IntegrationError> {
4896 let mut reserved = pre_reserved.clone();
4897 for (index, span) in entries {
4898 let Some(index) = index else {
4899 continue;
4900 };
4901 if !reserved.insert(*index) {
4902 return Err(IntegrationError::new(
4903 "index-collision",
4904 format!("duplicate explicit {kind} index {index}"),
4905 *span,
4906 ));
4907 }
4908 }
4909
4910 let mut next = 0;
4915 let mut allocated = Vec::with_capacity(entries.len());
4916 for (index, span) in entries {
4917 let assigned = if let Some(index) = index {
4918 *index
4919 } else {
4920 while reserved.contains(&next) {
4921 next = next.checked_add(1).ok_or_else(|| {
4922 IntegrationError::new(
4923 "index-exhausted",
4924 format!("no available {kind} index remains"),
4925 *span,
4926 )
4927 })?;
4928 }
4929 reserved.insert(next);
4930 let assigned = next;
4931 next = next.checked_add(1).ok_or_else(|| {
4932 IntegrationError::new(
4933 "index-exhausted",
4934 format!("no available {kind} index remains"),
4935 *span,
4936 )
4937 })?;
4938 assigned
4939 };
4940 allocated.push(assigned);
4941 }
4942 Ok(allocated)
4943}
4944
4945fn player_event_kind(name: &str) -> Option<PlayerEventKind> {
4946 Some(match name {
4947 "playerDealtDamage" => PlayerEventKind::DealtDamage,
4948 "playerDealtFinalBlow" => PlayerEventKind::DealtFinalBlow,
4949 "playerDealtHealing" => PlayerEventKind::DealtHealing,
4950 "playerDied" => PlayerEventKind::Died,
4951 "playerEarnedElimination" => PlayerEventKind::EarnedElimination,
4952 "playerJoined" => PlayerEventKind::Joined,
4953 "playerLeft" => PlayerEventKind::Left,
4954 "playerReceivedHealing" => PlayerEventKind::ReceivedHealing,
4955 "playerTookDamage" => PlayerEventKind::TookDamage,
4956 _ => return None,
4957 })
4958}
4959
4960fn is_zero_initializer(expr: &hir::Expr) -> bool {
4961 match expr {
4962 hir::Expr::Number { text, value, .. } => text == "0" && *value == 0.0,
4963 hir::Expr::Null { .. } => true,
4964 _ => false,
4965 }
4966}
4967
4968fn literal_key_matches(left: &hir::Expr, right: &hir::Expr) -> bool {
4969 match (left, right) {
4970 (hir::Expr::Number { value: left, .. }, hir::Expr::Number { value: right, .. }) => {
4971 left == right
4972 }
4973 (hir::Expr::String { value: left, .. }, hir::Expr::String { value: right, .. }) => {
4974 left == right
4975 }
4976 (hir::Expr::Bool { value: left, .. }, hir::Expr::Bool { value: right, .. }) => {
4977 left == right
4978 }
4979 (hir::Expr::Null { .. }, hir::Expr::Null { .. }) => true,
4980 _ => false,
4981 }
4982}
4983
4984fn indexed_target_parts<'a>(
4985 target: &'a hir::Expr,
4986 indices: &mut Vec<&'a hir::Expr>,
4987) -> Option<&'a hir::Expr> {
4988 match target {
4989 hir::Expr::Index { array, index, .. } => {
4990 indices.push(index);
4991 indexed_target_parts(array, indices)
4992 }
4993 hir::Expr::GlobalVar { .. } | hir::Expr::PlayerVar { .. } => Some(target),
4994 _ => None,
4995 }
4996}
4997
4998fn is_literal_key(expr: &hir::Expr) -> bool {
4999 matches!(
5000 expr,
5001 hir::Expr::Number { .. }
5002 | hir::Expr::String { .. }
5003 | hir::Expr::Bool { .. }
5004 | hir::Expr::Null { .. }
5005 )
5006}
5007
5008fn translation_locale(language: &str) -> Option<&'static str> {
5009 Some(match language {
5010 "de" => "de-DE",
5011 "en" => "en-US",
5012 "es" | "es_mx" => "es-MX",
5013 "es_es" => "es-ES",
5014 "fr" => "fr-FR",
5015 "it" => "it-IT",
5016 "ja" => "ja-JP",
5017 "ko" => "ko-KR",
5018 "pl" => "pl-PL",
5019 "pt" => "pt-BR",
5020 "ru" => "ru-RU",
5021 "th" => "th-TH",
5022 "tr" => "tr-TR",
5023 "zh" | "zh_cn" => "zh-CN",
5024 "zh_tw" => "zh-TW",
5025 _ => return None,
5026 })
5027}
5028
5029fn big_letters(value: &str) -> String {
5030 let mut output = String::with_capacity(value.len());
5031 let mut converted = false;
5032 for character in value.chars() {
5033 if !converted {
5034 if let Some(mapped) = big_letter(character) {
5035 output.push(mapped);
5036 converted = true;
5037 continue;
5038 }
5039 }
5040 output.push(character);
5041 }
5042 output
5043}
5044
5045fn big_letter(character: char) -> Option<char> {
5046 Some(match character {
5047 'a' | 'A' => 'Α',
5048 'b' | 'B' => 'Β',
5049 'e' | 'E' => 'Ε',
5050 'h' | 'H' => 'Η',
5051 'i' | 'I' => 'Ι',
5052 'k' | 'K' => 'Κ',
5053 'm' | 'M' => 'Μ',
5054 'n' | 'N' => 'Ν',
5055 'o' | 'O' => 'Ο',
5056 'p' | 'P' => 'Ρ',
5057 't' | 'T' => 'Τ',
5058 'x' | 'X' => 'Χ',
5059 'y' | 'Y' => 'Υ',
5060 'z' | 'Z' => 'Ζ',
5061 '.' => '\u{2024}',
5062 ' ' => '\u{2028}',
5063 _ => return None,
5064 })
5065}
5066
5067fn fullwidth(value: &str) -> String {
5068 value
5069 .chars()
5070 .map(|character| match character {
5071 ' ' => '\u{2001}',
5072 '\u{00a5}' => '\u{ffe5}',
5073 '\u{20a9}' => '\u{ffe6}',
5074 '\u{00a2}' => '\u{ffe0}',
5075 '\u{00a3}' => '\u{ffe1}',
5076 '\u{00af}' => '\u{ffe3}',
5077 '\u{00ac}' => '\u{ffe2}',
5078 '\u{00a6}' => '\u{ffe4}',
5079 character if ('!'..='~').contains(&character) => {
5080 char::from_u32(character as u32 + 65248).unwrap_or(character)
5081 }
5082 _ => character,
5083 })
5084 .collect()
5085}
5086
5087fn case_sensitive(value: &str) -> String {
5088 let mut output = value.replace('æ', "\u{04d5}").replace("nj", "\u{01cc}");
5089 output = output.replace(" a ", " a ");
5090 output
5091 .chars()
5092 .map(|character| match character {
5093 'a' => 'ạ',
5094 'b' => 'ḅ',
5095 'c' => 'ƈ',
5096 'd' => 'ḍ',
5097 'e' => 'ẹ',
5098 'f' => 'ƒ',
5099 'g' => 'ǥ',
5100 'h' => '\u{04bb}',
5101 'i' => 'і',
5102 'j' => 'ј',
5103 'k' => 'ḳ',
5104 'l' => 'I',
5105 'm' => 'ṃ',
5106 'n' => 'ṇ',
5107 'o' => 'ο',
5108 'p' => 'ṗ',
5109 'q' => 'ǫ',
5110 'r' => 'ṛ',
5111 's' => 'ѕ',
5112 't' => 'ṭ',
5113 'u' => 'υ',
5114 'v' => 'ν',
5115 'w' => 'ẉ',
5116 'x' => '\u{04b3}',
5117 'y' => 'ỵ',
5118 'z' => 'ẓ',
5119 _ => character,
5120 })
5121 .collect()
5122}
5123
5124fn canonical_number_text(value: f64, text: &str) -> String {
5125 if text.starts_with("0x") || text.starts_with("0X") {
5126 value.to_string()
5127 } else {
5128 text.to_string()
5129 }
5130}
5131
5132fn computed_number_text(value: f64) -> String {
5133 if value.fract() == 0.0 {
5134 format!("{value:.0}")
5135 } else {
5136 value.to_string()
5137 }
5138}
5139
5140fn canonical_format_text(text: &str) -> String {
5141 let mut output = String::with_capacity(text.len());
5142 let mut chars = text.chars().peekable();
5143 let mut index = 0;
5144 while let Some(character) = chars.next() {
5145 if character == '{' && chars.peek() == Some(&'}') {
5146 chars.next();
5147 output.push('{');
5148 output.push_str(&index.to_string());
5149 output.push('}');
5150 index += 1;
5151 } else {
5152 output.push(character);
5153 }
5154 }
5155 output
5156}
5157
5158fn fold_literal_format(text: &str, args: &[hir::Expr]) -> Option<String> {
5159 let values = args
5160 .iter()
5161 .map(|arg| match arg {
5162 hir::Expr::Number { text, value, .. } => Some(canonical_number_text(*value, text)),
5163 hir::Expr::String { value, .. } => Some(value.clone()),
5164 hir::Expr::Bool { value, .. } => Some(value.to_string()),
5165 hir::Expr::Null { .. } => Some("null".to_string()),
5166 _ => None,
5167 })
5168 .collect::<Option<Vec<_>>>()?;
5169 let mut output = canonical_format_text(text);
5170 for (index, value) in values.iter().enumerate() {
5171 output = output.replace(&format!("{{{index}}}"), value);
5172 }
5173 Some(output)
5174}
5175
5176fn debug_expr_text(expr: &Expr) -> String {
5177 match expr {
5178 Expr::Number { text, .. } => text.clone(),
5179 Expr::String { value, .. } => {
5180 format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
5181 }
5182 Expr::Bool { value, .. } => value.to_string(),
5183 Expr::Null { .. } => "null".to_string(),
5184 Expr::Array { elements, .. } => format!(
5185 "[{}]",
5186 elements
5187 .iter()
5188 .map(debug_expr_text)
5189 .collect::<Vec<_>>()
5190 .join(", ")
5191 ),
5192 Expr::Dict { entries, .. } => format!(
5193 "{{{}}}",
5194 entries
5195 .iter()
5196 .map(|entry| format!(
5197 "{}: {}",
5198 debug_expr_text(&entry.key),
5199 debug_expr_text(&entry.value)
5200 ))
5201 .collect::<Vec<_>>()
5202 .join(", ")
5203 ),
5204 Expr::Comprehension {
5205 element,
5206 variable,
5207 iterable,
5208 condition,
5209 ..
5210 } => {
5211 let condition = condition
5212 .as_deref()
5213 .map(|condition| format!(" if {}", debug_expr_text(condition)))
5214 .unwrap_or_default();
5215 format!(
5216 "[{} for {} in {}{}]",
5217 debug_expr_text(element),
5218 variable,
5219 debug_expr_text(iterable),
5220 condition
5221 )
5222 }
5223 Expr::Lambda { params, body, .. } => {
5224 format!("lambda {}: {}", params.join(", "), debug_expr_text(body))
5225 }
5226 Expr::StringModifier {
5227 modifier, value, ..
5228 } => format!("{}\"{}\"", modifier, value),
5229 Expr::Local { name, .. }
5230 | Expr::GlobalVar { name, .. }
5231 | Expr::Constant { name, .. }
5232 | Expr::MacroParam { name, .. } => name.clone(),
5233 Expr::Type { name, args, .. } => {
5234 if args.is_empty() {
5235 name.clone()
5236 } else {
5237 format!(
5238 "{}[{}]",
5239 name,
5240 args.iter()
5241 .map(debug_expr_text)
5242 .collect::<Vec<_>>()
5243 .join(": ")
5244 )
5245 }
5246 }
5247 Expr::Vector { x, y, z, .. } => format!(
5248 "vect({}, {}, {})",
5249 debug_expr_text(x),
5250 debug_expr_text(y),
5251 debug_expr_text(z)
5252 ),
5253 Expr::Enum {
5254 value_type, value, ..
5255 } => format!("{}.{}", value_type, value),
5256 Expr::PlayerVar { player, name, .. } => {
5257 format!("{}.{}", debug_expr_text(player), name)
5258 }
5259 Expr::Member {
5260 receiver, member, ..
5261 } => format!("{}.{}", debug_expr_text(receiver), member),
5262 Expr::EventPlayer { .. } => "eventPlayer".to_string(),
5263 Expr::HostPlayer { .. } => "hostPlayer".to_string(),
5264 Expr::Call { name, args, .. } if name == "sorted" && args.len() == 2 => {
5265 format!(
5266 "sorted({}, key = {})",
5267 debug_expr_text(&args[0]),
5268 debug_expr_text(&args[1])
5269 )
5270 }
5271 Expr::Call { name, args, .. } | Expr::MacroCall { name, args, .. } => format!(
5272 "{}({})",
5273 name,
5274 args.iter()
5275 .map(debug_expr_text)
5276 .collect::<Vec<_>>()
5277 .join(", ")
5278 ),
5279 Expr::ReceiverCall {
5280 receiver,
5281 name,
5282 args,
5283 ..
5284 } => format!(
5285 "{}.{}({})",
5286 debug_expr_text(receiver),
5287 name,
5288 args.iter()
5289 .map(debug_expr_text)
5290 .collect::<Vec<_>>()
5291 .join(", ")
5292 ),
5293 Expr::Binary {
5294 left, op, right, ..
5295 } => format!(
5296 "{} {} {}",
5297 debug_expr_text(left),
5298 op,
5299 debug_expr_text(right)
5300 ),
5301 Expr::Conditional {
5302 then_value,
5303 condition,
5304 else_value,
5305 ..
5306 } => format!(
5307 "{} if {} else {}",
5308 debug_expr_text(then_value),
5309 debug_expr_text(condition),
5310 debug_expr_text(else_value)
5311 ),
5312 Expr::Unary { op, operand, .. } => format!("{} {}", op, debug_expr_text(operand)),
5313 Expr::Index { array, index, .. } => {
5314 format!("{}[{}]", debug_expr_text(array), debug_expr_text(index))
5315 }
5316 Expr::Format { text, args, .. } => format!(
5317 "\"{}\".format({})",
5318 text,
5319 args.iter()
5320 .map(debug_expr_text)
5321 .collect::<Vec<_>>()
5322 .join(", ")
5323 ),
5324 }
5325}
5326
5327fn canonical_debug_text(text: &str) -> String {
5328 text.chars()
5329 .map(|character| match character {
5330 'a' => 'ạ',
5331 'b' => 'ḅ',
5332 'c' => 'ƈ',
5333 'd' => 'ḍ',
5334 'e' => 'ẹ',
5335 'f' => 'ƒ',
5336 'g' => 'ǥ',
5337 'h' => 'һ',
5338 'i' => 'і',
5339 'j' => 'ј',
5340 'k' => 'ḳ',
5341 'l' => 'I',
5342 'm' => 'ṃ',
5343 'n' => 'ṇ',
5344 'o' => 'ο',
5345 'p' => 'ṗ',
5346 'q' => 'ǫ',
5347 'r' => 'ṛ',
5348 's' => 'ѕ',
5349 't' => 'ṭ',
5350 'u' => 'υ',
5351 'v' => 'ν',
5352 'w' => 'ẉ',
5353 'x' => 'ҳ',
5354 'y' => 'ỵ',
5355 'z' => 'ẓ',
5356 _ => character,
5357 })
5358 .collect()
5359}
5360
5361fn negated_comparison(op: &str) -> Option<&'static str> {
5362 Some(match op {
5363 "==" => "!=",
5364 "!=" => "==",
5365 "<" => ">=",
5366 ">" => "<=",
5367 "<=" => ">",
5368 ">=" => "<",
5369 _ => return None,
5370 })
5371}
5372
5373fn modify_op_from_str(op: &str) -> Option<wir::ModifyOp> {
5374 match op {
5375 "+" => Some(wir::ModifyOp::Add),
5376 "-" => Some(wir::ModifyOp::Subtract),
5377 "*" => Some(wir::ModifyOp::Multiply),
5378 "/" => Some(wir::ModifyOp::Divide),
5379 "%" => Some(wir::ModifyOp::Modulo),
5380 "**" => Some(wir::ModifyOp::RaiseToPower),
5381 _ => None,
5382 }
5383}
5384
5385fn modify_catalog_name_from_str(op: &str) -> Option<&'static str> {
5386 match op {
5387 "+" => Some("add"),
5388 "-" => Some("subtract"),
5389 "*" => Some("multiply"),
5390 "/" => Some("divide"),
5391 "%" => Some("modulo"),
5392 "**" => Some("raiseToPower"),
5393 _ => None,
5394 }
5395}
5396
5397fn hir_span_from_diag(span: crate::diag::Span) -> HirSpan {
5398 HirSpan {
5399 file: span.file,
5400 start: hir::Position {
5401 line: span.start.line,
5402 col: span.start.col,
5403 },
5404 end: hir::Position {
5405 line: span.end.line,
5406 col: span.end.col,
5407 },
5408 }
5409}
5410
5411fn workshop_error_span(error: &workshop_rs::WorkshopError) -> Option<WorkshopSpan> {
5412 match error {
5413 workshop_rs::WorkshopError::Unknown { span, .. }
5414 | workshop_rs::WorkshopError::Malformed { span, .. }
5415 | workshop_rs::WorkshopError::Unsupported { span, .. } => *span,
5416 workshop_rs::WorkshopError::Catalog(_)
5417 | workshop_rs::WorkshopError::MissingMapping { .. } => None,
5418 }
5419}
5420
5421#[cfg(test)]
5422mod tests {
5423 use super::{
5424 COMPILE_SCHEMA_VERSION, CompileFailureClass, CompileStatus, Compiler, WORKSHOP_RS_VERSION,
5425 cross_check_manifest,
5426 };
5427 use crate::manifest::Manifest;
5428 use std::path::Path;
5429 use workshop_rs::catalog::{Catalog, Locale};
5430
5431 #[test]
5432 fn public_contract_is_pinned_and_manifest_links_are_checked() {
5433 let compiler = Compiler::new().expect("released workshop contract must load");
5434 let identity = compiler.catalog_identity();
5435 assert_eq!(identity.implementation_version, WORKSHOP_RS_VERSION);
5436 assert!(compiler.link_report().catalog_ids_checked > 0);
5437 assert!(compiler.link_report().domains_checked > 0);
5438 }
5439
5440 #[test]
5441 fn compile_report_is_versioned_and_contains_reproducibility_identity() {
5442 let compiler = Compiler::new().unwrap();
5443 let report = compiler.compile_source_report_with_locale(
5444 "rule \"report\":\n @Event global\n disableInspector()\n",
5445 "report.opy",
5446 Path::new("."),
5447 &Locale::new("en-US"),
5448 );
5449 assert_eq!(report.schema_version, COMPILE_SCHEMA_VERSION);
5450 assert_eq!(report.compiler.name, "opy-rs");
5451 assert_eq!(report.catalog.implementation_version, WORKSHOP_RS_VERSION);
5452 assert_eq!(report.compile.status, CompileStatus::Success);
5453 assert_eq!(report.compile.exit_code, 0);
5454 assert!(report.compile.diagnostics.is_empty());
5455 assert_eq!(
5456 report.compile.workshop,
5457 report
5458 .compile
5459 .workshop_exact
5460 .trim_end_matches('\n')
5461 .to_owned()
5462 + "\n"
5463 );
5464 assert!(serde_json::to_value(report).unwrap()["catalog"]["catalog-version"].is_string());
5465 }
5466
5467 #[test]
5468 fn compile_report_preserves_frontend_failure_class_and_source_path() {
5469 let compiler = Compiler::new().unwrap();
5470 let report = compiler.compile_source_report_with_locale(
5471 "rule \"broken\":\n @Event global\n missing()\n",
5472 "broken.opy",
5473 Path::new("."),
5474 &Locale::new("en-US"),
5475 );
5476 assert_eq!(report.compile.status, CompileStatus::Failure);
5477 assert_eq!(
5478 report.compile.failure_class,
5479 Some(CompileFailureClass::Frontend)
5480 );
5481 assert_eq!(report.compile.exit_code, 1);
5482 let diagnostic = &report.compile.diagnostics[0];
5483 assert_eq!(diagnostic.code, "unknown-action");
5484 assert_eq!(diagnostic.span.as_ref().unwrap().path, "broken.opy");
5485 }
5486
5487 #[test]
5488 fn compile_report_preserves_integration_failure_class_and_source_path() {
5489 let compiler = Compiler::new().unwrap();
5490 let report = compiler.compile_source_report_with_locale(
5491 "rule \"broken\":\n @Event global\n {\"a\": 1}[\"b\"] = 3\n",
5492 "broken.opy",
5493 Path::new("."),
5494 &Locale::new("en-US"),
5495 );
5496 assert_eq!(report.compile.status, CompileStatus::Failure);
5497 assert_eq!(
5498 report.compile.failure_class,
5499 Some(CompileFailureClass::Integration)
5500 );
5501 assert_eq!(
5502 report.compile.diagnostics[0].span.as_ref().unwrap().path,
5503 "broken.opy"
5504 );
5505 }
5506
5507 #[test]
5508 fn compile_report_preserves_frontend_warnings_on_integration_failure() {
5509 let compiler = Compiler::new().unwrap();
5510 let root = Path::new(env!("CARGO_MANIFEST_DIR"))
5511 .join("../../compatibility/fixtures/synthetic/preprocessing");
5512 let report = compiler.compile_source_report_with_locale(
5513 concat!(
5514 "#!include \"shared.opy\"\n",
5515 "#!include \"shared.opy\"\n",
5516 "rule \"broken\":\n",
5517 " @Event global\n",
5518 " {\"a\": 1}[\"b\"] = 3\n",
5519 ),
5520 "broken.opy",
5521 &root,
5522 &Locale::new("en-US"),
5523 );
5524 assert_eq!(report.compile.status, CompileStatus::Failure);
5525 assert_eq!(
5526 report.compile.failure_class,
5527 Some(CompileFailureClass::Integration)
5528 );
5529 assert_eq!(report.compile.diagnostics.len(), 2);
5530 assert_eq!(
5531 report.compile.diagnostics[0].severity,
5532 crate::tooling::DiagnosticSeverity::Warning
5533 );
5534 assert_eq!(report.compile.diagnostics[0].code, "w_already_imported");
5535 assert_eq!(
5536 report.compile.diagnostics[1].severity,
5537 crate::tooling::DiagnosticSeverity::Error
5538 );
5539 assert_eq!(
5540 report.compile.diagnostics[1].span.as_ref().unwrap().path,
5541 "broken.opy"
5542 );
5543 }
5544
5545 #[test]
5546 fn vertical_slice_preserves_source_files_spans_and_emits_workshop() {
5547 let compiler = Compiler::new().unwrap();
5548 let hir = crate::compile(
5549 "globalvar A\nrule \"issue 35 integration\":\n @Event global\n A = 1\n disableInspector()\n",
5550 "issue-35-integration.opy",
5551 Path::new("."),
5552 )
5553 .unwrap();
5554 let artifact = compiler.compile_hir(&hir).unwrap();
5555 assert_eq!(
5556 artifact
5557 .wir
5558 .files
5559 .get(workshop_rs::source::FileId::from_index(0))
5560 .unwrap()
5561 .path,
5562 "issue-35-integration.opy"
5563 );
5564 let rule = artifact
5565 .wir
5566 .rules
5567 .get(workshop_rs::wir::RuleId::from_index(0))
5568 .unwrap();
5569 assert_eq!(rule.span.unwrap().file.index(), 0);
5570 assert_eq!(rule.name_span.unwrap().start.line, 2);
5571 assert!(artifact.emitted.contains("Disable Inspector Recording;"));
5572 assert_eq!(artifact.catalog_identity.implementation_version, "0.1.16");
5573 }
5574
5575 #[test]
5576 fn stale_catalog_links_fail_explicitly() {
5577 let manifest = Manifest::builtin().unwrap().clone();
5578 let mut stale = manifest;
5579 stale.functions[0].catalog_id = Some("missing-catalog-id".to_string());
5580 let error = cross_check_manifest(&stale, &Catalog::builtin().unwrap()).unwrap_err();
5581 assert_eq!(error.diagnostic.code, "catalog-link-missing");
5582 }
5583
5584 #[test]
5585 fn while_lowering_is_source_attributed() {
5586 let compiler = Compiler::new().unwrap();
5587 let hir = crate::compile(
5588 "rule \"while\":\n @Event global\n while true:\n disableInspector()\n",
5589 "while.opy",
5590 Path::new("."),
5591 )
5592 .unwrap();
5593 let artifact = compiler.compile_hir(&hir).unwrap();
5594 let rule = artifact
5595 .wir
5596 .rules
5597 .get(workshop_rs::wir::RuleId::from_index(0))
5598 .unwrap();
5599 assert!(matches!(
5600 artifact.wir.actions.get(rule.actions[0]),
5601 Some(workshop_rs::wir::Action::While { .. })
5602 ));
5603 assert!(artifact.emitted.contains("While(True);"));
5604 }
5605
5606 #[test]
5607 fn structural_subroutines_lower_to_canonical_wir() {
5608 let compiler = Compiler::new().unwrap();
5609 let hir = crate::compile(
5610 "globalvar score\nsubroutine showStatus\ndef showStatus():\n @Name \"Friendly\"\n @SuppressWarnings unusedVariable\n disableInspector()\nrule \"caller\":\n @Event global\n showStatus()\n",
5611 "structure.opy",
5612 Path::new("."),
5613 )
5614 .unwrap();
5615 let artifact = compiler.compile_hir(&hir).unwrap();
5616 let subroutine = artifact
5617 .wir
5618 .subroutines
5619 .get(workshop_rs::wir::SubroutineId::from_index(0))
5620 .unwrap();
5621 assert_eq!(subroutine.name, "showStatus");
5622 assert_eq!(subroutine.index, 0);
5623 assert_eq!(subroutine.name_span.unwrap().start.line, 2);
5624 assert_eq!(artifact.wir.rules.len(), 2);
5625 let subroutine_rule = artifact
5626 .wir
5627 .rules
5628 .get(workshop_rs::wir::RuleId::from_index(0))
5629 .unwrap();
5630 let workshop_rs::wir::Event::Subroutine(subroutine_id) = subroutine_rule.event else {
5631 panic!("expected a subroutine event");
5632 };
5633 assert_eq!(
5634 artifact.wir.subroutines.get(subroutine_id).unwrap().name,
5635 "showStatus"
5636 );
5637 assert!(matches!(
5638 artifact
5639 .wir
5640 .actions
5641 .get(workshop_rs::wir::ActionId::from_index(1))
5642 .unwrap(),
5643 workshop_rs::wir::Action::CallSubroutine { .. }
5644 ));
5645 assert!(artifact.emitted.contains("Subroutine Friendly"));
5646 }
5647
5648 #[test]
5649 fn player_event_filters_resolve_through_canonical_catalog() {
5650 let compiler = Compiler::new().unwrap();
5651 let hir = crate::compile(
5652 "rule \"joined\":\n @Event playerJoined\n @Team 1\n @Slot 2\n disableInspector()\n",
5653 "filters.opy",
5654 Path::new("."),
5655 )
5656 .unwrap();
5657 let artifact = compiler.compile_hir(&hir).unwrap();
5658 assert!(matches!(
5659 &artifact
5660 .wir
5661 .rules
5662 .get(workshop_rs::wir::RuleId::from_index(0))
5663 .unwrap()
5664 .event,
5665 workshop_rs::wir::Event::Player {
5666 kind: workshop_rs::wir::PlayerEventKind::Joined,
5667 team: workshop_rs::wir::EventTeam::Team1,
5668 target: workshop_rs::wir::EventTarget::Slot(2),
5669 }
5670 ));
5671 assert!(artifact.emitted.contains("Player Joined Match;"));
5672 }
5673
5674 #[test]
5675 fn explicit_indices_are_reserved_before_deterministic_allocation() {
5676 let compiler = Compiler::new().unwrap();
5677 let hir = crate::compile(
5678 "globalvar first\nglobalvar reserved 0\nglobalvar next\nrule \"indices\":\n @Event global\n disableInspector()\n",
5679 "indices.opy",
5680 Path::new("."),
5681 )
5682 .unwrap();
5683 let artifact = compiler.compile_hir(&hir).unwrap();
5684 let by_name = artifact
5685 .wir
5686 .global_variables
5687 .iter()
5688 .map(|variable| (variable.name.as_str(), variable.index))
5689 .collect::<std::collections::BTreeMap<_, _>>();
5690 assert_eq!(
5691 by_name,
5692 std::collections::BTreeMap::from([("first", 1), ("reserved", 0), ("next", 2)])
5693 );
5694 let indices = artifact
5696 .wir
5697 .global_variables
5698 .iter()
5699 .map(|variable| variable.index)
5700 .collect::<Vec<_>>();
5701 assert_eq!(indices, vec![0, 1, 2]);
5702 }
5703
5704 #[test]
5705 fn implicit_default_variables_use_reference_fixed_slots() {
5706 let compiler = Compiler::new().unwrap();
5707 let hir = crate::compile(
5708 r#"
5709globalvar timer
5710globalvar extra 5
5711
5712rule "implicit":
5713 @Event global
5714 A = timer + 1
5715 B = A
5716 B += 2
5717 A[0] = 7
5718 DX = B * A
5719"#,
5720 "implicit.opy",
5721 Path::new("."),
5722 )
5723 .unwrap();
5724 let artifact = compiler.compile_hir(&hir).unwrap();
5725 let globals = artifact
5726 .wir
5727 .global_variables
5728 .iter()
5729 .map(|variable| (variable.name.clone(), variable.index))
5730 .collect::<Vec<_>>();
5731 assert_eq!(
5736 globals,
5737 vec![
5738 ("A".to_string(), 0),
5739 ("B".to_string(), 1),
5740 ("timer".to_string(), 2),
5741 ("extra".to_string(), 5),
5742 ("DX".to_string(), 127),
5743 ]
5744 );
5745 assert!(
5746 artifact
5747 .emitted
5748 .contains("Set Global Variable(A, Add(Global.timer, 1));")
5749 );
5750 assert!(
5751 artifact
5752 .emitted
5753 .contains("Set Global Variable(B, Global.A);")
5754 );
5755 assert!(
5756 artifact
5757 .emitted
5758 .contains("Modify Global Variable(B, Add, 2);")
5759 );
5760 assert!(
5761 artifact
5762 .emitted
5763 .contains("Set Global Variable At Index(A, 0, 7);")
5764 );
5765 assert!(
5766 artifact
5767 .emitted
5768 .contains("Set Global Variable(DX, Multiply(Global.B, Global.A));")
5769 );
5770 }
5771
5772 #[test]
5773 fn implicit_default_variable_slot_collision_is_source_attributed() {
5774 let compiler = Compiler::new().unwrap();
5775 let hir = crate::compile(
5776 "globalvar x 0\nrule \"collision\":\n @Event global\n x = 1\n A = 2\n",
5777 "collision.opy",
5778 Path::new("."),
5779 )
5780 .unwrap();
5781 let error = match compiler.compile_hir(&hir) {
5782 Ok(_) => panic!("slot collision unexpectedly succeeded"),
5783 Err(error) => error,
5784 };
5785 assert_eq!(error.diagnostic.code, "index-collision");
5786 assert_eq!(error.diagnostic.span.unwrap().start.line, 5);
5787 assert!(error.diagnostic.message.contains("'A' and 'x'"));
5788 }
5789
5790 #[test]
5791 fn implicit_default_player_variables_use_independent_reference_slots() {
5792 let compiler = Compiler::new().unwrap();
5793 let hir = crate::compile(
5794 r#"
5795playervar declaredPlayer
5796
5797rule "implicit player variables":
5798 @Event eachPlayer
5799 A = 1
5800 eventPlayer.A = 1
5801 eventPlayer.A += 2
5802 eventPlayer.E = eventPlayer.A
5803 eventPlayer.DX = eventPlayer.E
5804 eventPlayer.declaredPlayer = eventPlayer.A
5805"#,
5806 "implicit-player.opy",
5807 Path::new("."),
5808 )
5809 .unwrap();
5810 let artifact = compiler.compile_hir(&hir).unwrap();
5811 let globals = artifact
5812 .wir
5813 .global_variables
5814 .iter()
5815 .map(|variable| (variable.name.as_str(), variable.index))
5816 .collect::<std::collections::BTreeMap<_, _>>();
5817 let players = artifact
5818 .wir
5819 .player_variables
5820 .iter()
5821 .map(|variable| (variable.name.as_str(), variable.index))
5822 .collect::<std::collections::BTreeMap<_, _>>();
5823 assert_eq!(globals.get("A"), Some(&0));
5824 assert_eq!(players.get("A"), Some(&0));
5825 assert_eq!(players.get("declaredPlayer"), Some(&1));
5826 assert_eq!(players.get("E"), Some(&4));
5827 assert_eq!(players.get("DX"), Some(&127));
5828 assert!(
5829 artifact
5830 .emitted
5831 .contains("Set Player Variable(Event Player, A, 1);")
5832 );
5833 assert!(
5834 artifact
5835 .emitted
5836 .contains("Modify Player Variable(Event Player, A, Add, 2);")
5837 );
5838 assert!(
5839 artifact
5840 .emitted
5841 .contains("Set Player Variable(Event Player, E, (Event Player).A);")
5842 );
5843 }
5844
5845 #[test]
5846 fn implicit_default_player_slot_collision_is_source_attributed() {
5847 let compiler = Compiler::new().unwrap();
5848 let hir = crate::compile(
5849 "playervar declared 0\nrule \"collision\":\n @Event eachPlayer\n eventPlayer.A = 1\n",
5850 "player-collision.opy",
5851 Path::new("."),
5852 )
5853 .unwrap();
5854 let error = match compiler.compile_hir(&hir) {
5855 Ok(_) => panic!("player slot collision unexpectedly succeeded"),
5856 Err(error) => error,
5857 };
5858 assert_eq!(error.diagnostic.code, "index-collision");
5859 assert!(
5860 error
5861 .diagnostic
5862 .message
5863 .contains("player variables 'A' and 'declared'")
5864 );
5865 assert_eq!(error.diagnostic.span.unwrap().start.line, 4);
5866 }
5867
5868 #[test]
5869 fn power_augmented_assignment_lowers_from_source() {
5870 let compiler = Compiler::new().unwrap();
5871 let hir = crate::compile(
5872 "globalvar g\nrule \"power\":\n @Event global\n g = 2\n g **= 3\n",
5873 "power.opy",
5874 Path::new("."),
5875 )
5876 .unwrap();
5877 let artifact = compiler.compile_hir(&hir).unwrap();
5878 assert!(artifact.emitted.contains("Set Global Variable(g, 2);"));
5879 assert!(
5880 artifact
5881 .emitted
5882 .contains("Modify Global Variable(g, Raise To Power, 3);")
5883 );
5884 }
5885
5886 #[test]
5887 fn opy_hex_numbers_are_normalized_at_the_wir_boundary() {
5888 let compiler = Compiler::new().unwrap();
5889 let hir = crate::compile(
5890 "globalvar large = 0x124BC\nglobalvar small = 0x124\nglobalvar scientific = 1e10\n",
5891 "numbers.opy",
5892 Path::new("."),
5893 )
5894 .unwrap();
5895 let artifact = compiler.compile_hir(&hir).unwrap();
5896 assert!(
5897 artifact
5898 .emitted
5899 .contains("Set Global Variable(large, 74940);")
5900 );
5901 assert!(
5902 artifact
5903 .emitted
5904 .contains("Set Global Variable(small, 292);")
5905 );
5906 assert!(
5907 artifact
5908 .emitted
5909 .contains("Set Global Variable(scientific, 1e10);")
5910 );
5911 assert!(!artifact.emitted.contains("0x124BC"));
5912 assert!(!artifact.emitted.contains("0x124"));
5913 }
5914
5915 #[test]
5916 fn literal_dict_lookup_lowers_to_the_selected_value() {
5917 let compiler = Compiler::new().unwrap();
5918 let hir = crate::compile(
5919 "globalvar total\nrule \"negative\":\n @Event global\n total = {\"a\": 1, \"b\": 2}[\"a\"]\n",
5920 "negative.opy",
5921 Path::new("."),
5922 )
5923 .unwrap();
5924 let artifact = compiler
5925 .compile_hir(&hir)
5926 .expect("literal dict lookup should lower");
5927 assert!(artifact.emitted.contains("Set Global Variable(total, 1);"));
5928 }
5929
5930 #[test]
5931 fn auto_allocation_fills_free_slots_below_early_explicit_indices() {
5932 let compiler = Compiler::new().unwrap();
5933 let hir = crate::compile(
5934 r#"
5935globalvar reserved 5
5936globalvar auto1
5937globalvar auto2
5938
5939rule "allocation":
5940 @Event global
5941 auto1 = 1
5942 auto2 = 2
5943 B = 3
5944"#,
5945 "allocation.opy",
5946 Path::new("."),
5947 )
5948 .unwrap();
5949 let artifact = compiler.compile_hir(&hir).unwrap();
5950 let by_name = artifact
5951 .wir
5952 .global_variables
5953 .iter()
5954 .map(|variable| (variable.name.clone(), variable.index))
5955 .collect::<std::collections::BTreeMap<_, _>>();
5956 assert_eq!(
5961 by_name,
5962 std::collections::BTreeMap::from([
5963 ("B".to_string(), 1),
5964 ("auto1".to_string(), 0),
5965 ("auto2".to_string(), 2),
5966 ("reserved".to_string(), 5),
5967 ])
5968 );
5969 }
5970
5971 #[test]
5972 fn power_expressions_lower_through_the_canonical_contract() {
5973 let compiler = Compiler::new().unwrap();
5974 let hir = crate::compile(
5975 "globalvar a = [2, 4]\nglobalvar out\nrule \"power\":\n @Event global\n out = a ** 2\n a **= 2\n a[0] **= 2\n",
5976 "power.opy",
5977 Path::new("."),
5978 )
5979 .unwrap();
5980 let artifact = compiler.compile_hir(&hir).unwrap();
5981 assert!(
5982 artifact
5983 .emitted
5984 .contains("Set Global Variable(out, Raise To Power(Global.a, 2));")
5985 );
5986 assert!(
5987 artifact
5988 .emitted
5989 .contains("Modify Global Variable(a, Raise To Power, 2);")
5990 );
5991 assert!(
5992 artifact
5993 .emitted
5994 .contains("Modify Global Variable At Index(a, 0, Raise To Power, 2);")
5995 );
5996 }
5997
5998 #[test]
5999 fn unsupported_rule_metadata_is_explicit_and_source_attributed() {
6000 let compiler = Compiler::new().unwrap();
6001 let hir = crate::compile(
6002 "rule \"metadata\":\n @Event global\n @NewPage \"section\"\n disableInspector()\n",
6003 "metadata.opy",
6004 Path::new("."),
6005 )
6006 .unwrap();
6007 let error = match compiler.compile_hir(&hir) {
6008 Ok(_) => panic!("unsupported metadata unexpectedly succeeded"),
6009 Err(error) => error,
6010 };
6011 assert_eq!(error.diagnostic.code, "unsupported-integration-surface");
6012 assert_eq!(error.diagnostic.span.unwrap().start.line, 3);
6013 }
6014
6015 #[test]
6016 fn issue_40_oracle_fixture_and_wir_lowering_agree() {
6017 let compiler = Compiler::new().unwrap();
6018 let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
6019 .join("../../compatibility/fixtures/synthetic/issue-40-structural");
6020 let source = std::fs::read_to_string(fixture.join("source.opy")).unwrap();
6021 let hir = crate::compile(&source, "source.opy", &fixture).unwrap();
6022 let artifact = compiler.compile_hir(&hir).unwrap();
6023 let oracle: serde_json::Value =
6024 serde_json::from_str(&std::fs::read_to_string(fixture.join("oracle.json")).unwrap())
6025 .unwrap();
6026 let oracle_workshop = oracle["compile"]["workshop"].as_str().unwrap();
6027 let oracle_wir = workshop_rs::parser::parse(
6028 oracle_workshop,
6029 &Catalog::builtin().unwrap(),
6030 &Locale::new("en-US"),
6031 )
6032 .unwrap();
6033 assert!(workshop_rs::roundtrip::equivalent(
6034 &artifact.wir,
6035 &oracle_wir
6036 ));
6037
6038 assert!(oracle_workshop.contains("0: reserved"));
6039 assert!(oracle_workshop.contains("1: first"));
6040 assert!(oracle_workshop.contains("2: explicit"));
6041 assert!(oracle_workshop.contains("3: next"));
6042 assert!(oracle_workshop.contains("0: helper"));
6043 assert!(oracle_workshop.contains("Subroutine;\n helper;"));
6044 assert!(oracle_workshop.contains("Player Joined Match;\n Team 1;\n Slot 2;"));
6045
6046 let indices = artifact
6047 .wir
6048 .global_variables
6049 .iter()
6050 .map(|variable| variable.index)
6051 .collect::<Vec<_>>();
6052 assert_eq!(indices, vec![0, 1, 2, 3]);
6053 assert_eq!(
6054 artifact.wir.subroutines.iter().next().unwrap().name,
6055 "helper"
6056 );
6057 assert!(artifact.emitted.contains("[Source] renamed helper"));
6058 assert!(matches!(
6059 artifact
6060 .wir
6061 .rules
6062 .get(workshop_rs::wir::RuleId::from_index(1))
6063 .unwrap()
6064 .event,
6065 workshop_rs::wir::Event::Player {
6066 kind: workshop_rs::wir::PlayerEventKind::Joined,
6067 team: workshop_rs::wir::EventTeam::Team1,
6068 target: workshop_rs::wir::EventTarget::Slot(2),
6069 }
6070 ));
6071 }
6072
6073 #[test]
6074 fn assignments_and_modifications_lower_to_canonical_wir() {
6075 let compiler = Compiler::new().unwrap();
6076 let hir = crate::compile(
6077 r#"
6078globalvar g1
6079globalvar g2
6080playervar p1
6081playervar p2 = [1, 2, 3]
6082
6083rule "assignments":
6084 @Event eachPlayer
6085 g1 = 10
6086 g1 += 5
6087 g1 -= 2
6088 g1 *= 3
6089 g1 /= 2
6090 g1 %= 4
6091 g2 = [1, 2, 3]
6092 g2[0] = 99
6093 g2[1] += 1
6094 eventPlayer.p1 = 42
6095 eventPlayer.p1 += 8
6096 eventPlayer.p1 *= 2
6097 eventPlayer.p2[2] = 7
6098 eventPlayer.p2[0] -= 3
6099"#,
6100 "assign.opy",
6101 Path::new("."),
6102 )
6103 .unwrap();
6104 let artifact = compiler.compile_hir(&hir).unwrap();
6105 assert!(artifact.emitted.contains("Set Global Variable(g1, 10);"));
6106 assert!(
6107 artifact
6108 .emitted
6109 .contains("Modify Global Variable(g1, Add, 5);")
6110 );
6111 assert!(
6112 artifact
6113 .emitted
6114 .contains("Modify Global Variable(g1, Subtract, 2);")
6115 );
6116 assert!(
6117 artifact
6118 .emitted
6119 .contains("Modify Global Variable(g1, Multiply, 3);")
6120 );
6121 assert!(
6122 artifact
6123 .emitted
6124 .contains("Modify Global Variable(g1, Divide, 2);")
6125 );
6126 assert!(
6127 artifact
6128 .emitted
6129 .contains("Modify Global Variable(g1, Modulo, 4);")
6130 );
6131 assert!(
6132 artifact
6133 .emitted
6134 .contains("Set Global Variable At Index(g2, 0, 99);")
6135 );
6136 assert!(
6137 artifact
6138 .emitted
6139 .contains("Modify Global Variable At Index(g2, 1, Add, 1);")
6140 );
6141 assert!(
6142 artifact
6143 .emitted
6144 .contains("Set Player Variable(Event Player, p1, 42);")
6145 );
6146 assert!(
6147 artifact
6148 .emitted
6149 .contains("Modify Player Variable(Event Player, p1, Add, 8);")
6150 );
6151 assert!(
6152 artifact
6153 .emitted
6154 .contains("Modify Player Variable(Event Player, p1, Multiply, 2);")
6155 );
6156 assert!(
6157 artifact
6158 .emitted
6159 .contains("Set Player Variable At Index((Event Player).p2, 2, 7);")
6160 );
6161 assert!(
6162 artifact
6163 .emitted
6164 .contains("Modify Player Variable At Index((Event Player).p2, 0, Subtract, 3);")
6165 );
6166
6167 let rule = artifact
6171 .wir
6172 .rules
6173 .get(workshop_rs::wir::RuleId::from_index(1))
6174 .unwrap();
6175 let direct = artifact.wir.actions.get(rule.actions[0]).unwrap();
6176 match direct {
6177 workshop_rs::wir::Action::SetGlobalVariable {
6178 span,
6179 target_span,
6180 variable,
6181 ..
6182 } => {
6183 assert_eq!(span.unwrap().start.line, 9);
6184 assert_eq!(target_span.unwrap().start.line, 9);
6185 assert_eq!(
6186 artifact.wir.global_variables.get(*variable).unwrap().name,
6187 "g1"
6188 );
6189 }
6190 other => panic!("expected a direct global assignment, got {other:?}"),
6191 }
6192 let indexed = artifact.wir.actions.get(rule.actions[7]).unwrap();
6193 match indexed {
6194 workshop_rs::wir::Action::Call { span, .. } => {
6195 assert_eq!(span.unwrap().start.line, 16);
6196 }
6197 other => panic!("expected an indexed assignment call, got {other:?}"),
6198 }
6199 }
6200
6201 #[test]
6202 fn expressions_and_values_lower_to_canonical_wir() {
6203 let compiler = Compiler::new().unwrap();
6204 let hir = crate::compile(
6205 r#"
6206enum Consts:
6207 BASE
6208
6209globalvar total
6210globalvar arr = [1, 2, 3]
6211globalvar pos = vect(1, 2, 3)
6212
6213rule "expressions":
6214 @Event global
6215 @Condition total == 0
6216 @Condition not (pos == vect(0, 0, 0))
6217 @Condition 2 in arr
6218 total = Consts.BASE + arr[1] * 2 - (10 / 2) + (5 % 2)
6219 print("Total: {}".format(total))
6220 debug(pos)
6221"#,
6222 "expr.opy",
6223 Path::new("."),
6224 )
6225 .unwrap();
6226 let artifact = compiler.compile_hir(&hir).unwrap();
6227 assert!(artifact.emitted.contains("Global.total == 0;"));
6228 assert!(artifact.emitted.contains("Global.pos != Vector(0, 0, 0);"));
6231 assert!(
6232 artifact
6233 .emitted
6234 .contains("Array Contains(Global.arr, 2) == True;")
6235 );
6236 assert!(
6237 artifact
6238 .emitted
6239 .contains("Custom String(\"Total: {0}\", Global.total)")
6240 );
6241 }
6242
6243 #[test]
6244 fn pass_is_supported_as_source_level_noop() {
6245 let compiler = Compiler::new().unwrap();
6246 let hir = crate::compile(
6247 r#"
6248subroutine emptySub
6249
6250def emptySub():
6251 pass
6252
6253rule "empty rule":
6254 @Event global
6255 pass
6256"#,
6257 "pass.opy",
6258 Path::new("."),
6259 )
6260 .unwrap();
6261 let artifact = compiler.compile_hir(&hir).unwrap();
6262 let rule0 = artifact
6263 .wir
6264 .rules
6265 .get(workshop_rs::wir::RuleId::from_index(0))
6266 .unwrap();
6267 assert!(rule0.actions.is_empty());
6268 let rule1 = artifact
6269 .wir
6270 .rules
6271 .get(workshop_rs::wir::RuleId::from_index(1))
6272 .unwrap();
6273 assert!(rule1.actions.is_empty());
6274 }
6275
6276 #[test]
6277 fn variable_initializers_synthesize_initialize_rules() {
6278 let compiler = Compiler::new().unwrap();
6279 let hir = crate::compile(
6280 r#"
6281globalvar j = 5
6282globalvar h = 0
6283globalvar k = 0.0
6284playervar p = 7
6285playervar q = 0
6286
6287rule "main":
6288 @Event global
6289 disableInspector()
6290"#,
6291 "init.opy",
6292 Path::new("."),
6293 )
6294 .unwrap();
6295 let artifact = compiler.compile_hir(&hir).unwrap();
6296 assert_eq!(
6297 artifact
6298 .wir
6299 .rules
6300 .get(workshop_rs::wir::RuleId::from_index(0))
6301 .unwrap()
6302 .name,
6303 "Initialize global variables"
6304 );
6305 assert_eq!(
6306 artifact
6307 .wir
6308 .rules
6309 .get(workshop_rs::wir::RuleId::from_index(1))
6310 .unwrap()
6311 .name,
6312 "Initialize player variables"
6313 );
6314 assert_eq!(
6315 artifact
6316 .wir
6317 .rules
6318 .get(workshop_rs::wir::RuleId::from_index(2))
6319 .unwrap()
6320 .name,
6321 "main"
6322 );
6323 assert!(artifact.emitted.contains("Set Global Variable(j, 5);"));
6324 assert!(artifact.emitted.contains("Set Global Variable(k, 0.0);"));
6325 assert!(!artifact.emitted.contains("Set Global Variable(h,"));
6326 assert!(
6327 artifact
6328 .emitted
6329 .contains("Set Player Variable(Event Player, p, 7);")
6330 );
6331 assert!(
6332 !artifact
6333 .emitted
6334 .contains("Set Player Variable(Event Player, q,")
6335 );
6336 }
6337
6338 #[test]
6339 fn settings_lower_through_workshop_owned_emission() {
6340 let compiler = Compiler::new().unwrap();
6341 let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
6342 .join("../../compatibility/fixtures/synthetic/settings");
6343 let source = std::fs::read_to_string(fixture.join("source.opy")).unwrap();
6344 let hir = crate::compile(&source, "source.opy", &fixture).unwrap();
6345 let artifact = compiler.compile_hir(&hir).unwrap();
6346 let oracle: serde_json::Value =
6347 serde_json::from_str(&std::fs::read_to_string(fixture.join("oracle.json")).unwrap())
6348 .unwrap();
6349 let expected = oracle["compile"]["workshop"]
6350 .as_str()
6351 .unwrap()
6352 .split("\n\nrule")
6353 .next()
6354 .unwrap();
6355 let actual = artifact.emitted.split("\n\nrule").next().unwrap();
6356 let oracle_wir = workshop_rs::parser::parse(
6357 oracle["compile"]["workshop"].as_str().unwrap(),
6358 &Catalog::builtin().unwrap(),
6359 &Locale::new("en-US"),
6360 )
6361 .unwrap();
6362 assert!(workshop_rs::roundtrip::equivalent(
6363 &artifact.wir,
6364 &oracle_wir
6365 ));
6366 assert_eq!(
6367 normalize_workshop_structural_whitespace(actual),
6368 normalize_workshop_structural_whitespace(expected)
6369 );
6370 }
6371
6372 #[test]
6373 fn unsupported_locale_has_no_fabricated_source_span() {
6374 let compiler = Compiler::new().unwrap();
6375 let hir = crate::compile(
6376 "#!translations en\nrule \"r\":\n @Event global\n pass\n",
6377 "locale.opy",
6378 Path::new("."),
6379 )
6380 .unwrap();
6381 let error = match compiler.compile_hir_with_locale(&hir, &Locale::new("xx-XX")) {
6382 Ok(_) => panic!("unsupported locale unexpectedly compiled"),
6383 Err(error) => error,
6384 };
6385 assert_eq!(error.diagnostic.code, "locale-unsupported");
6386 assert_eq!(error.diagnostic.span, None);
6387 }
6388
6389 #[test]
6390 fn locale_selection_emits_catalog_localized_workshop() {
6391 let compiler = Compiler::new().unwrap();
6392 let hir = crate::compile(
6393 "rule \"locale\":\n @Event global\n disableInspector()\n",
6394 "locale.opy",
6395 Path::new("."),
6396 )
6397 .unwrap();
6398 let artifact = compiler
6399 .compile_hir_with_locale(&hir, &Locale::new("zh-CN"))
6400 .unwrap();
6401 assert!(artifact.emitted.contains("规则 (\"locale\")"));
6402 assert!(artifact.emitted.contains("禁用查看器录制"));
6403 }
6404
6405 #[test]
6406 fn unsupported_backend_directives_fail_at_their_source_anchor() {
6407 let compiler = Compiler::new().unwrap();
6408 let hir = crate::compile(
6409 "#!replace0ByCapturePercentage\nrule \"r\":\n @Event global\n pass\n",
6410 "directives.opy",
6411 Path::new("."),
6412 )
6413 .unwrap();
6414 let error = match compiler.compile_hir(&hir) {
6415 Ok(_) => panic!("backend directive unexpectedly compiled"),
6416 Err(error) => error,
6417 };
6418 assert_eq!(error.diagnostic.code, "backend-directive-unsupported");
6419 assert_eq!(error.diagnostic.span.unwrap().start.line, 1);
6420 }
6421
6422 #[test]
6423 fn optimizer_directives_remain_non_blocking_presentation_controls() {
6424 let compiler = Compiler::new().unwrap();
6425 let hir = crate::compile(
6426 "#!disableOptimizations\nrule \"r\":\n @Event global\n pass\n",
6427 "optimization.opy",
6428 Path::new("."),
6429 )
6430 .unwrap();
6431 compiler.compile_hir(&hir).unwrap();
6432 }
6433
6434 #[test]
6435 fn replacement_directive_records_are_checked_even_if_final_state_is_restored() {
6436 let compiler = Compiler::new().unwrap();
6437 let mut hir = crate::compile(
6438 "#!replace0ByCapturePercentage\nrule \"r\":\n @Event global\n pass\n",
6439 "directives.opy",
6440 Path::new("."),
6441 )
6442 .unwrap();
6443 hir.preprocessing.replacements.clear();
6444 let error = match compiler.compile_hir(&hir) {
6445 Ok(_) => panic!("replacement directive unexpectedly compiled"),
6446 Err(error) => error,
6447 };
6448 assert_eq!(error.diagnostic.code, "backend-directive-unsupported");
6449 assert_eq!(error.diagnostic.span.unwrap().start.line, 1);
6450 }
6451
6452 #[test]
6453 fn active_replacement_state_is_checked_without_directive_history() {
6454 let compiler = Compiler::new().unwrap();
6455 let mut hir = crate::compile(
6456 "#!replace0ByCapturePercentage\nrule \"r\":\n @Event global\n pass\n",
6457 "directives.opy",
6458 Path::new("."),
6459 )
6460 .unwrap();
6461 hir.preprocessing.directives.clear();
6462 hir.preprocessing.replacements[0].span = None;
6463 let error = match compiler.compile_hir(&hir) {
6464 Ok(_) => panic!("active replacement state unexpectedly compiled"),
6465 Err(error) => error,
6466 };
6467 assert_eq!(error.diagnostic.code, "backend-directive-unsupported");
6468 assert_eq!(error.diagnostic.span, None);
6469 }
6470
6471 #[test]
6472 fn post_compile_hook_receives_exact_emitted_workshop() {
6473 let compiler = Compiler::new().unwrap();
6474 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/macros");
6475 let source = "#!postCompileHook \"hook.js\"\n\nrule \"setup\":\n pass\n";
6476 let artifact = compiler
6477 .compile_source_with_locale(source, "hook.opy", &root, &Locale::new("en-US"))
6478 .unwrap();
6479 assert!(artifact.emitted.contains("rule (\"setup\")"));
6480 assert!(artifact.final_output.contains("rule (\"transformed\")"));
6481 assert_ne!(artifact.final_output, artifact.emitted);
6482 }
6483
6484 #[test]
6485 fn post_compile_hook_failure_keeps_script_provenance_and_directive_anchor() {
6486 let compiler = Compiler::new().unwrap();
6487 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/macros");
6488 let source = "#!postCompileHook \"hook-boom.js\"\n\nrule \"setup\":\n pass\n";
6489 let error = match compiler.compile_source_with_locale(
6490 source,
6491 "hook.opy",
6492 &root,
6493 &Locale::new("en-US"),
6494 ) {
6495 Ok(_) => panic!("failing post-compile hook unexpectedly compiled"),
6496 Err(error) => error,
6497 };
6498 assert_eq!(error.diagnostic.code, "post-compile-hook");
6499 assert_eq!(error.diagnostic.span.unwrap().start.line, 1);
6500 let script = error.diagnostic.script.unwrap();
6501 assert_eq!(script.source_name.as_deref(), Some("hook-boom.js"));
6502 assert_eq!(script.line, Some(1));
6503 assert!(script.stack.unwrap().contains("hook-boom.js:1"));
6504 }
6505
6506 fn normalize_workshop_structural_whitespace(text: &str) -> String {
6507 let mut normalized = String::with_capacity(text.len());
6508 let mut quote = None;
6509 let mut escaped = false;
6510 for character in text.chars() {
6511 if let Some(delimiter) = quote {
6512 normalized.push(character);
6513 if escaped {
6514 escaped = false;
6515 } else if character == '\\' {
6516 escaped = true;
6517 } else if character == delimiter {
6518 quote = None;
6519 }
6520 } else if matches!(character, '\"' | '\'') {
6521 quote = Some(character);
6522 normalized.push(character);
6523 } else if !character.is_whitespace() {
6524 normalized.push(character);
6525 }
6526 }
6527 normalized
6528 }
6529
6530 #[test]
6531 fn settings_whitespace_normalization_preserves_quoted_values() {
6532 assert_ne!(
6533 normalize_workshop_structural_whitespace("Description: \"a b\""),
6534 normalize_workshop_structural_whitespace("Description: \"ab\"")
6535 );
6536 }
6537}