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